Files
gt-agent-company/src/pages/agent-system/assets/index.vue
sexygoat 3e42c4e98c
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 1m47s
feat: 资产管理新增字段
2026-05-20 11:06:57 +08:00

947 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getUserInfo } from '@/api/auth'
import type { UserInfo } from '@/api/auth'
import { getEnterpriseCards, getEnterpriseDevices } from '@/api/enterprise'
import { getCarriers } from '@/api/carrier'
import type { CarrierInfo } from '@/api/carrier'
import { stopAsset, startAsset } from '@/api/assets'
import Skeleton from '@/components/Skeleton.vue'
import SimplePicker from '@/components/SimplePicker.vue'
import ShopCascadePicker from '@/components/ShopCascadePicker.vue'
import { get } from '@/utils/request'
// 卡片信息接口
interface CardInfo {
id : number
iccid : string
imsi ?: string
msisdn ?: string
virtual_no ?: string
device_virtual_no ?: string | null
virtual_number ?: string // 企业端字段
status ?: number
status_name ?: string
activation_status ?: number
network_status ?: number
network_status_name ?: string
real_name_status ?: number
carrier_name ?: string
carrier_type ?: string
operator ?: string // 企业端字段
supplier ?: string
batch_no ?: string
series_name ?: string
shop_name ?: string
data_usage_mb ?: number
current_month_usage_mb ?: number
accumulated_recharge ?: number
created_at ?: string
updated_at ?: string
}
// 设备信息接口
interface DeviceInfo {
id ?: number
device_id ?: number // 企业端字段
device_name ?: string
device_model ?: string
device_type ?: string
manufacturer ?: string
imei ?: string
sn ?: string
virtual_no ?: string
status ?: number
status_name ?: string
online_status ?: number
bound_card_count ?: number
card_count ?: number // 企业端字段
max_sim_slots ?: number
switch_mode ?: string
series_name ?: string
shop_name ?: string
software_version ?: string
accumulated_recharge ?: number
activated_at ?: string
authorized_at ?: string // 企业端字段
last_online_time ?: string
created_at ?: string
updated_at ?: string
}
// 用户信息
const userInfo = ref<UserInfo | null>(null)
const sessionButtonCodes = ref<string[]>([])
const ASSET_ACTION_BUTTON_CODES = {
cardStop: 'asset:card_stop_h5',
cardStart: 'asset:card_start_h5',
deviceStop: 'asset:device_stop_h5',
deviceStart: 'asset:device_start_h5',
} as const
type CardSearchType = 'iccid' | 'virtual_no' | 'msisdn'
type DeviceSearchType = 'device_name' | 'virtual_no' | 'imei'
interface SearchTypeOption {
label : string
value : string
}
interface StandaloneCardsParams {
page : number
page_size : number
carrier_id ?: number
status ?: number
iccid ?: string
virtual_no ?: string
msisdn ?: string
shop_id ?: number
}
interface DevicesParams {
page : number
page_size : number
status ?: number
device_name ?: string
virtual_no ?: string
imei ?: string
shop_id ?: number
}
const cardSearchTypeOptions : SearchTypeOption[] = [
{ label: 'ICCID', value: 'iccid' },
{ label: '虚拟号', value: 'virtual_no' },
{ label: '接入号', value: 'msisdn' },
]
const deviceSearchTypeOptions : SearchTypeOption[] = [
{ label: '设备号', value: 'device_name' },
{ label: '虚拟号', value: 'virtual_no' },
{ label: 'IMEI', value: 'imei' },
]
// 获取单卡列表(代理端)
function getStandaloneCards(params : StandaloneCardsParams) {
return get<{ items : CardInfo[], page : number, size : number, total : number }>('/api/admin/iot-cards/standalone', { params })
}
// 获取设备列表(代理端)
function getDevices(params : DevicesParams) {
return get<{ items : DeviceInfo[], page : number, size : number, total : number }>('/api/admin/devices', { params })
}
// 企业ID (企业端使用)
const enterpriseId = ref<number>(0)
// 资产类型
type AssetType = 'card' | 'device'
// 统一的资产项
interface AssetItem {
id : string
type : AssetType
name : string
status : string
statusColor : string
detail : string
bindingStatus ?: string
raw : CardInfo | DeviceInfo
}
interface SelectedShopFilter {
id : number
shop_name : string
}
// 当前标签
const activeTab = ref<'card' | 'device'>('card')
// 资产列表
const cardList = ref<CardInfo[]>([])
const deviceList = ref<DeviceInfo[]>([])
// 加载状态
const loading = ref(false)
// 分页
const cardPage = ref(1)
const devicePage = ref(1)
const cardHasMore = ref(true)
const deviceHasMore = ref(true)
const pageSize = 20
// 搜索和筛选
const searchKeyword = ref('')
const carrierFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
const selectedShopFilter = ref<SelectedShopFilter | null>(null)
const cardSearchType = ref<CardSearchType>('iccid')
const deviceSearchType = ref<DeviceSearchType>('device_name')
const showCarrierPicker = ref(false)
const showSearchTypePicker = ref(false)
const showStatusPicker = ref(false)
const showShopPicker = ref(false)
// 运营商列表
const carrierList = ref<CarrierInfo[]>([])
const isAgent = computed(() => userInfo.value?.user_type === 3)
const currentSearchType = computed(() => activeTab.value === 'card' ? cardSearchType.value : deviceSearchType.value)
const searchTypeOptions = computed(() => activeTab.value === 'card' ? cardSearchTypeOptions : deviceSearchTypeOptions)
const selectedSearchTypeLabel = computed(() => {
return searchTypeOptions.value.find(option => option.value === currentSearchType.value)?.label
|| (activeTab.value === 'card' ? 'ICCID' : '设备号')
})
const isDefaultSearchType = computed(() => {
return activeTab.value === 'card' ? cardSearchType.value === 'iccid' : deviceSearchType.value === 'device_name'
})
const searchPlaceholder = computed(() => {
if (activeTab.value === 'card') {
if (cardSearchType.value === 'virtual_no') return '搜索虚拟号'
if (cardSearchType.value === 'msisdn') return '搜索接入号'
return '搜索ICCID'
}
if (deviceSearchType.value === 'imei') return '搜索IMEI'
if (deviceSearchType.value === 'virtual_no') return '搜索虚拟号'
return '搜索设备号'
})
const canShowDeviceStopButton = computed(() => hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.deviceStop))
const canShowDeviceStartButton = computed(() => hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.deviceStart))
function hasStatusCode(error : unknown) {
return typeof error === 'object' && error !== null && 'statusCode' in error
}
function getSessionButtonCodes() {
const cachedUserInfo = uni.getStorageSync('user_info')
if (!cachedUserInfo || typeof cachedUserInfo !== 'object' || !Array.isArray((cachedUserInfo as UserInfo).buttons)) {
return []
}
return (cachedUserInfo as UserInfo).buttons!.filter((code) => {
return typeof code === 'string' && code.trim().length > 0
})
}
function hasActionButtonPermission(code : string) {
return sessionButtonCodes.value.includes(code)
}
function canShowCardStopButton(card : CardInfo) {
return card.network_status !== 0 && hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.cardStop)
}
function canShowCardStartButton(card : CardInfo) {
return card.network_status !== 1 && hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.cardStart)
}
// 运营商选择器选项
const carrierOptions = computed(() => [
{ label: '全部运营商', value: undefined },
...carrierList.value.map(c => ({ label: c.carrier_name, value: c.id }))
])
// 状态选择器选项
const statusOptions = [
{ label: '全部状态', value: undefined },
{ label: '在库', value: 1 },
{ label: '已分销', value: 2 },
{ label: '已激活', value: 3 },
{ label: '已停用', value: 4 }
]
function getCardBindingStatus(card : CardInfo) {
return card.device_virtual_no?.trim() ? '已绑定设备' : '未绑定设备'
}
function getCardBindingTagClass(bindingStatus ?: string) {
return bindingStatus === '已绑定设备'
? 'bg-[#e8f5e9] text-[#2e7d32]'
: 'bg-[#fff7e6] text-[#d46b08]'
}
function applyCardSearchKeyword(params : StandaloneCardsParams, keyword : string) {
if (cardSearchType.value === 'virtual_no') {
params.virtual_no = keyword
return
}
if (cardSearchType.value === 'msisdn') {
params.msisdn = keyword
return
}
params.iccid = keyword
}
function applyDeviceSearchKeyword(params : DevicesParams, keyword : string) {
if (deviceSearchType.value === 'imei') {
params.imei = keyword
return
}
if (deviceSearchType.value === 'device_name') {
params.device_name = keyword
return
}
params.virtual_no = keyword
}
// 合并后的资产列表
const assetList = computed<AssetItem[]>(() => {
const cards : AssetItem[] = cardList.value.map(card => ({
id: String(card.id),
type: 'card' as AssetType,
name: card.iccid,
// 兼容企业端和代理端的状态字段
status: card.network_status_name || (card.network_status === 1 ? '正常' : card.network_status === 0 ? '停机' : card.status_name || '未知'),
statusColor: (card.network_status === 1) ? '#52c41a' : (card.network_status === 0) ? '#999' : '#ff4d4f',
// 兼容企业端和代理端的字段
detail: `${[
`虚拟号: ${card.virtual_no || card.virtual_number || '-'}`,
`运营商: ${card.carrier_name || card.operator || '-'}`,
].filter(Boolean).join(' | ')}`,
bindingStatus: isAgent.value ? getCardBindingStatus(card) : undefined,
raw: card,
}))
const devices : AssetItem[] = deviceList.value.map(device => ({
id: String(device.id || device.device_id),
type: 'device' as AssetType,
name: device.device_name || '-',
// 兼容企业端和代理端的状态字段
status: device.status_name || (device.authorized_at ? '已授权' : device.online_status === 1 ? '在线' : device.online_status === 2 ? '离线' : '未知'),
statusColor: (device.authorized_at || device.online_status === 1) ? '#52c41a' : (device.online_status === 2) ? '#999' : '#ff4d4f',
// 兼容企业端和代理端的字段
detail: `虚拟号: ${device.virtual_no || '-'} | 型号: ${device.device_model || '-'} | 卡数: ${device.bound_card_count || device.card_count || 0}`,
raw: device,
}))
if (activeTab.value === 'card') return cards
if (activeTab.value === 'device') return devices
return cards
})
// 获取企业ID企业端使用
async function getEnterpriseId() {
if (enterpriseId.value) return enterpriseId.value
const cachedUserInfo = uni.getStorageSync('user_info')
if (cachedUserInfo?.enterprise_id) {
enterpriseId.value = cachedUserInfo.enterprise_id
return enterpriseId.value
}
const apiUserInfo = await getUserInfo()
if (!apiUserInfo.enterprise_id) {
throw new Error('未找到企业ID')
}
enterpriseId.value = apiUserInfo.enterprise_id
return enterpriseId.value
}
// 加载IoT卡列表兼容企业端和代理端
async function loadCards(isRefresh = false) {
if (loading.value || (!cardHasMore.value && !isRefresh)) return
loading.value = true
try {
// 如果是刷新,重置页码
if (isRefresh) {
cardPage.value = 1
cardList.value = []
cardHasMore.value = true
}
const params : StandaloneCardsParams = {
page: cardPage.value,
page_size: pageSize,
}
// 添加筛选条件
if (carrierFilter.value !== undefined) {
params.carrier_id = carrierFilter.value
}
if (statusFilter.value !== undefined) {
params.status = statusFilter.value
}
if (searchKeyword.value.trim()) {
applyCardSearchKeyword(params, searchKeyword.value.trim())
}
if (userInfo.value?.user_type === 3 && selectedShopFilter.value) {
params.shop_id = selectedShopFilter.value.id
}
let cardsData : any
// 根据用户类型调用不同接口
if (userInfo.value?.user_type === 3) {
// 代理端:调用代理接口
cardsData = await getStandaloneCards(params)
} else {
// 企业端:调用企业接口
const id = await getEnterpriseId()
cardsData = await getEnterpriseCards(id, params)
}
const newItems = cardsData?.items || []
if (cardPage.value === 1) {
cardList.value = newItems
} else {
cardList.value.push(...newItems)
}
// 判断是否还有更多数据
cardHasMore.value = newItems.length >= pageSize
cardPage.value++
}
catch (error : any) {
console.error('加载IoT卡列表失败:', error)
if (error instanceof Error && error.message && !hasStatusCode(error)) {
uni.$u.toast(error.message)
}
}
finally {
loading.value = false
}
}
// 加载设备列表(兼容企业端和代理端)
async function loadDevices(isRefresh = false) {
if (loading.value || (!deviceHasMore.value && !isRefresh)) return
loading.value = true
try {
// 如果是刷新,重置页码
if (isRefresh) {
devicePage.value = 1
deviceList.value = []
deviceHasMore.value = true
}
const params : DevicesParams = {
page: devicePage.value,
page_size: pageSize,
}
if (searchKeyword.value.trim()) {
applyDeviceSearchKeyword(params, searchKeyword.value.trim())
}
if (statusFilter.value !== undefined) {
params.status = statusFilter.value
}
if (userInfo.value?.user_type === 3 && selectedShopFilter.value) {
params.shop_id = selectedShopFilter.value.id
}
let devicesData : any
// 根据用户类型调用不同接口
if (userInfo.value?.user_type === 3) {
// 代理端:调用代理接口
devicesData = await getDevices(params)
} else {
// 企业端:调用企业接口
const id = await getEnterpriseId()
devicesData = await getEnterpriseDevices(id, params)
}
const newItems = devicesData?.items || []
if (devicePage.value === 1) {
deviceList.value = newItems
} else {
deviceList.value.push(...newItems)
}
// 判断是否还有更多数据
deviceHasMore.value = newItems.length >= pageSize
devicePage.value++
}
catch (error : any) {
console.error('加载设备列表失败:', error)
if (error instanceof Error && error.message && !hasStatusCode(error)) {
uni.$u.toast(error.message)
}
}
finally {
loading.value = false
}
}
// 加载更多
function loadMore() {
if (activeTab.value === 'card') {
loadCards()
} else {
loadDevices()
}
}
// 刷新列表
function refreshList() {
if (activeTab.value === 'card') {
loadCards(true)
} else {
loadDevices(true)
}
}
// 加载运营商列表
async function loadCarriers() {
try {
const response = await getCarriers({ page: 1, size: 100 })
carrierList.value = response?.items || []
} catch (error) {
console.error('加载运营商列表失败:', error)
}
}
// 选择运营商
function onCarrierConfirm(option : { label : string, value : any }) {
carrierFilter.value = option.value
refreshList()
}
// 选择状态
function onStatusConfirm(option : { label : string, value : any }) {
statusFilter.value = option.value
refreshList()
}
function onSearchTypeConfirm(option : SearchTypeOption) {
if (activeTab.value === 'card') {
cardSearchType.value = option.value as CardSearchType
} else {
deviceSearchType.value = option.value as DeviceSearchType
}
refreshList()
}
// 获取当前选中的运营商名称
const selectedCarrierName = computed(() => {
if (carrierFilter.value === undefined) return '运营商'
const option = carrierOptions.value.find(o => o.value === carrierFilter.value)
return option?.label || '运营商'
})
// 获取当前选中的状态名称
const selectedStatusName = computed(() => {
if (statusFilter.value === undefined) return '状态'
const option = statusOptions.find(o => o.value === statusFilter.value)
return option?.label || '状态'
})
const selectedShopName = computed(() => selectedShopFilter.value?.shop_name || '店铺')
const activeTabHasMore = computed(() => activeTab.value === 'card' ? cardHasMore.value : deviceHasMore.value)
function onShopConfirm(shop : SelectedShopFilter | null) {
if (!shop) return
selectedShopFilter.value = shop
refreshList()
}
// 查看资产详情
function viewAssetDetail(asset : AssetItem) {
if (asset.type === 'card') {
const card = asset.raw as CardInfo
uni.navigateTo({
url: `/pages/agent-system/asset-detail/index?iccid=${card.iccid}`,
})
} else {
const device = asset.raw as DeviceInfo
uni.navigateTo({
url: `/pages/agent-system/asset-detail/index?virtual_no=${device.virtual_no}`,
})
}
}
// 切换标签
function changeTab(tab : 'card' | 'device') {
if (activeTab.value === tab) return
activeTab.value = tab
refreshList()
}
// 设备停机
async function handleStopDevice(asset : AssetItem, event : Event) {
event.stopPropagation()
const device = asset.raw as DeviceInfo
const identifier = device.virtual_no
if (!identifier) {
uni.$u.toast('设备虚拟号不存在')
return
}
uni.showModal({
title: '确认停机',
content: `确定要停机设备"${device.device_name}"吗?这将停机设备下所有已实名卡。`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '停机中...' })
try {
await stopAsset(identifier)
uni.hideLoading()
// 显示停机结果 (统一接口不返回详情,仅提示成功)
uni.showModal({
title: '停机成功',
content: '设备已停机',
showCancel: false,
success: () => {
// 刷新列表
refreshList()
}
})
} catch (error : any) {
uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('设备停机失败:', error)
}
}
}
})
}
// 设备复机
async function handleStartDevice(asset : AssetItem, event : Event) {
event.stopPropagation()
const device = asset.raw as DeviceInfo
const identifier = device.virtual_no
if (!identifier) {
uni.$u.toast('设备虚拟号不存在')
return
}
uni.showModal({
title: '确认复机',
content: `确定要复机设备"${device.device_name}"吗?这将复机设备下所有已实名卡。`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '复机中...' })
try {
await startAsset(identifier)
uni.hideLoading()
uni.$u.toast('复机成功')
// 刷新列表
refreshList()
} catch (error : any) {
uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('设备复机失败:', error)
}
}
}
})
}
// 单卡停机
async function handleStopCard(asset : AssetItem, event : Event) {
event.stopPropagation()
const card = asset.raw as CardInfo
if (!card.iccid) {
uni.$u.toast('ICCID不存在')
return
}
uni.showModal({
title: '确认停机',
content: `确定要停机卡"${card.iccid}"吗?`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '停机中...' })
try {
await stopAsset(card.iccid)
uni.hideLoading()
uni.$u.toast('停机成功')
// 刷新列表
refreshList()
} catch (error : any) {
uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('单卡停机失败:', error)
}
}
}
})
}
// 单卡复机
async function handleStartCard(asset : AssetItem, event : Event) {
event.stopPropagation()
const card = asset.raw as CardInfo
if (!card.iccid) {
uni.$u.toast('ICCID不存在')
return
}
uni.showModal({
title: '确认复机',
content: `确定要复机卡"${card.iccid}"吗?`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '复机中...' })
try {
await startAsset(card.iccid)
uni.hideLoading()
uni.$u.toast('复机成功')
// 刷新列表
refreshList()
} catch (error : any) {
uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('单卡复机失败:', error)
}
}
}
})
}
const statusBarHeight = ref(0)
onMounted(async () => {
// 1. 获取状态栏高度
statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight || 20
sessionButtonCodes.value = getSessionButtonCodes()
// 2. 加载用户信息(判断是企业端还是代理端)
try {
const user = await getUserInfo()
userInfo.value = user
} catch (error) {
console.error('获取用户信息失败:', error)
}
// 3. 加载运营商列表
loadCarriers()
// 4. 默认加载IoT卡列表
loadCards()
})
</script>
<template>
<view class="bg-page h-screen flex flex-col overflow-hidden">
<!-- 搜索区域 -->
<view class="bg-white px-4 pt-4 pb-3 shrink-0 shadow-[0_4px_18px_rgba(15,23,42,0.06)] z-10">
<!-- 搜索框 -->
<view class="relative flex items-center">
<input v-model="searchKeyword"
class="flex-1 h-10 bg-[#f1f5f9] rounded-full pl-4 pr-12 text-sm text-[#1e293b] placeholder-[#94a3b8]"
:placeholder="searchPlaceholder" @confirm="refreshList" />
<view class="absolute right-1 flex items-center">
<view v-if="searchKeyword" class="w-8 h-8 flex items-center justify-center text-[#94a3b8]"
@click="searchKeyword = ''">
<i class="i-mdi-close-circle text-lg" />
</view>
<view class="w-8 h-8 flex items-center justify-center text-brand" @click="refreshList">
<i class="i-mdi-magnify text-xl" />
</view>
</view>
</view>
<!-- 筛选和类型切换 -->
<view class="flex items-center gap-2 mt-3 flex-wrap">
<view class="flex bg-[#f1f5f9] rounded-full flex-shrink-0">
<view class="px-3 py-1.5 text-12px font-medium transition-all rounded-full"
:class="activeTab === 'card' ? 'bg-brand text-white' : 'text-[#64748b]'" @click="changeTab('card')">
IoT卡
</view>
<view class="px-3 py-1.5 text-12px font-medium transition-all rounded-full"
:class="activeTab === 'device' ? 'bg-brand text-white' : 'text-[#64748b]'" @click="changeTab('device')">
设备
</view>
</view>
<view class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0"
@click="showSearchTypePicker = true">
<text class="text-12px" :class="isDefaultSearchType ? 'text-[#64748b]' : 'text-brand font-medium'">
{{ selectedSearchTypeLabel }}
</text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8]" />
</view>
<view class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0 max-w-36"
@click="showCarrierPicker = true">
<text class="text-12px truncate"
:class="carrierFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ selectedCarrierName }}
</text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8] flex-shrink-0" />
</view>
<view class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0"
@click="showStatusPicker = true">
<text class="text-12px" :class="statusFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ selectedStatusName }}
</text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8]" />
</view>
<view v-if="isAgent"
class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0 max-w-36"
@click="showShopPicker = true">
<text class="text-12px truncate" :class="selectedShopFilter ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ selectedShopName }}
</text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8] flex-shrink-0" />
</view>
<view v-if="carrierFilter !== undefined || statusFilter !== undefined || selectedShopFilter"
class="flex items-center gap-1 px-2 py-1.5 rounded-full bg-[#ff4d4f]/10 text-[#ff4d4f] flex-shrink-0"
@click="carrierFilter = undefined; statusFilter = undefined; selectedShopFilter = null; refreshList()">
<i class="i-mdi-close text-12px" />
<text class="text-12px font-medium">清除</text>
</view>
</view>
</view>
<!-- 列表内容区域 -->
<view class="safe-area-bottom flex-1 min-h-0 overflow-y-auto">
<!-- 加载状态 -->
<Skeleton v-if="loading && assetList.length === 0" type="list" />
<!-- 资产列表 -->
<view v-else class="min-h-full">
<view v-if="assetList.length > 0" class="px-4 pt-3 pb-4">
<view v-for="asset in assetList" :key="`${asset.type}-${asset.id}`"
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3 cursor-pointer"
@click="viewAssetDetail(asset)">
<!-- 顶部区域左侧状态+名称右侧类型标签 -->
<view class="flex items-start justify-between mb-3">
<view class="flex items-center gap-2 flex-1 min-w-0">
<view class="w-2 h-2 rounded-full mt-1 flex-shrink-0" :style="{ backgroundColor: asset.statusColor }" />
<text class="text-15px font-600 text-[#212121] truncate">
{{ asset.name }}
</text>
</view>
<view class="px-2 py-1 rounded-6px text-11px font-500 flex-shrink-0 ml-2" :style="{
backgroundColor: asset.type === 'card' ? '#e6f4ff' : '#f5f5f5',
color: asset.type === 'card' ? 'var(--brand-primary)' : '#666',
}">
{{ asset.type === 'card' ? 'IoT卡' : '设备' }}
</view>
</view>
<!-- 详情信息 -->
<view class="mb-3">
<text class="text-12px text-[#999]">
{{ asset.detail }}
</text>
<view v-if="asset.type === 'card' && asset.bindingStatus" class="mt-2">
<text :class="[
'inline-flex px-2 py-1 rounded-full text-11px font-600',
getCardBindingTagClass(asset.bindingStatus),
]">
{{ asset.bindingStatus }}
</text>
</view>
</view>
<!-- 底部区域状态标签 + 操作按钮 -->
<view class="flex items-center justify-between pt-3 border-t border-[#f5f5f5]">
<view class="flex items-center gap-1">
<view class="w-1.5 h-1.5 rounded-full" :style="{ backgroundColor: asset.statusColor }" />
<text class="text-13px font-500" :style="{ color: asset.statusColor }">
{{ asset.status }}
</text>
</view>
<view class="flex items-center gap-2">
<!-- IoT卡操作按钮 -->
<template v-if="asset.type === 'card'">
<view
v-if="canShowCardStopButton(asset.raw as CardInfo)"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
@click="handleStopCard(asset, $event)">
停机
</view>
<view
v-if="canShowCardStartButton(asset.raw as CardInfo)"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
@click="handleStartCard(asset, $event)">
复机
</view>
</template>
<!-- 设备操作按钮 -->
<template v-else>
<view
v-if="canShowDeviceStopButton"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
@click="handleStopDevice(asset, $event)">
停机
</view>
<view
v-if="canShowDeviceStartButton"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
@click="handleStartDevice(asset, $event)">
复机
</view>
</template>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="activeTabHasMore" class="py-4 flex justify-center">
<view
class="min-w-32 px-5 py-2.5 rounded-full text-sm font-medium transition-all flex items-center justify-center"
:class="loading
? 'bg-[#f1f5f9] text-[#94a3b8]'
: 'bg-brand text-white shadow-sm shadow-brand/30 active:scale-95'" @click="!loading && loadMore()">
<template v-if="loading">
<i class="i-mdi-loading animate-spin text-base mr-1.5" />
<text>加载中...</text>
</template>
<template v-else>
<text>加载更多</text>
</template>
</view>
</view>
<view v-else class="py-4 text-center">
<text class="text-sm text-[#94a3b8]">没有更多了</text>
</view>
</view>
<!-- 空状态 -->
<view v-else class="px-4 pt-3 pb-4">
<view class="bg-white rounded-2xl py-12 flex flex-col items-center">
<i class="i-mdi-package-variant-closed text-6xl text-[#cbd5e1] mb-3" />
<text class="text-base text-[#64748b] mb-1">暂无数据</text>
<text class="text-sm text-[#94a3b8]">{{ searchKeyword ? '未找到相关结果' : '暂无资产数据' }}</text>
</view>
</view>
</view>
</view>
<!-- 运营商选择器 -->
<SimplePicker v-model:show="showCarrierPicker" :options="carrierOptions" :selected-value="carrierFilter"
auto-confirm title="选择运营商"
@confirm="onCarrierConfirm" />
<!-- 搜索字段选择器 -->
<SimplePicker v-model:show="showSearchTypePicker" :options="searchTypeOptions" :selected-value="currentSearchType"
auto-confirm title="选择搜索字段"
@confirm="onSearchTypeConfirm" />
<!-- 状态选择器 -->
<SimplePicker v-model:show="showStatusPicker" :options="statusOptions" :selected-value="statusFilter"
auto-confirm title="选择状态" @confirm="onStatusConfirm" />
<ShopCascadePicker v-model:show="showShopPicker" :selected-shop="selectedShopFilter" title="选择店铺"
@confirm="onShopConfirm" />
</view>
</template>