fix: ui
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 1m4s

This commit is contained in:
sexygoat
2026-04-25 15:00:56 +08:00
parent daa3d7ec8f
commit 398d5494c8

View File

@@ -1,148 +1,148 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { getUserInfo } from '@/api/auth' import { getUserInfo } from '@/api/auth'
import type { UserInfo } from '@/api/auth' import type { UserInfo } from '@/api/auth'
import { getEnterpriseCards, getEnterpriseDevices } from '@/api/enterprise' import { getEnterpriseCards, getEnterpriseDevices } from '@/api/enterprise'
import { getCarriers } from '@/api/carrier' import { getCarriers } from '@/api/carrier'
import type { CarrierInfo } from '@/api/carrier' import type { CarrierInfo } from '@/api/carrier'
import { stopAsset, startAsset } from '@/api/assets' import { stopAsset, startAsset } from '@/api/assets'
import Skeleton from '@/components/Skeleton.vue' import Skeleton from '@/components/Skeleton.vue'
import SimplePicker from '@/components/SimplePicker.vue' import SimplePicker from '@/components/SimplePicker.vue'
import { formatTime, formatDataSize } from '@/utils/format' import { formatTime, formatDataSize } from '@/utils/format'
import { get } from '@/utils/request' import { get } from '@/utils/request'
// 卡片信息接口 // 卡片信息接口
interface CardInfo { interface CardInfo {
id: number id : number
iccid: string iccid : string
imsi?: string imsi ?: string
msisdn?: string msisdn ?: string
virtual_no?: string virtual_no ?: string
virtual_number?: string // 企业端字段 virtual_number ?: string // 企业端字段
status?: number status ?: number
status_name?: string status_name ?: string
activation_status?: number activation_status ?: number
network_status?: number network_status ?: number
network_status_name?: string network_status_name ?: string
real_name_status?: number real_name_status ?: number
carrier_name?: string carrier_name ?: string
carrier_type?: string carrier_type ?: string
operator?: string // 企业端字段 operator ?: string // 企业端字段
supplier?: string supplier ?: string
batch_no?: string batch_no ?: string
series_name?: string series_name ?: string
shop_name?: string shop_name ?: string
data_usage_mb?: number data_usage_mb ?: number
current_month_usage_mb?: number current_month_usage_mb ?: number
accumulated_recharge?: number accumulated_recharge ?: number
created_at?: string created_at ?: string
updated_at?: string updated_at ?: string
} }
// 设备信息接口 // 设备信息接口
interface DeviceInfo { interface DeviceInfo {
id?: number id ?: number
device_id?: number // 企业端字段 device_id ?: number // 企业端字段
device_name?: string device_name ?: string
device_model?: string device_model ?: string
device_type?: string device_type ?: string
manufacturer?: string manufacturer ?: string
imei?: string imei ?: string
sn?: string sn ?: string
virtual_no?: string virtual_no ?: string
status?: number status ?: number
status_name?: string status_name ?: string
online_status?: number online_status ?: number
bound_card_count?: number bound_card_count ?: number
card_count?: number // 企业端字段 card_count ?: number // 企业端字段
max_sim_slots?: number max_sim_slots ?: number
switch_mode?: string switch_mode ?: string
series_name?: string series_name ?: string
shop_name?: string shop_name ?: string
software_version?: string software_version ?: string
accumulated_recharge?: number accumulated_recharge ?: number
activated_at?: string activated_at ?: string
authorized_at?: string // 企业端字段 authorized_at ?: string // 企业端字段
last_online_time?: string last_online_time ?: string
created_at?: string created_at ?: string
updated_at?: string updated_at ?: string
} }
// 用户信息 // 用户信息
const userInfo = ref<UserInfo | null>(null) const userInfo = ref<UserInfo | null>(null)
// 获取单卡列表(代理端) // 获取单卡列表(代理端)
function getStandaloneCards(params: any) { function getStandaloneCards(params : any) {
return get<{ items: CardInfo[], page: number, size: number, total: number }>('/api/admin/iot-cards/standalone', { params }) return get<{ items : CardInfo[], page : number, size : number, total : number }>('/api/admin/iot-cards/standalone', { params })
} }
// 获取设备列表(代理端) // 获取设备列表(代理端)
function getDevices(params: any) { function getDevices(params : any) {
return get<{ items: DeviceInfo[], page: number, size: number, total: number }>('/api/admin/devices', { params }) return get<{ items : DeviceInfo[], page : number, size : number, total : number }>('/api/admin/devices', { params })
} }
// 企业ID (企业端使用) // 企业ID (企业端使用)
const enterpriseId = ref<number>(0) const enterpriseId = ref<number>(0)
// 资产类型 // 资产类型
type AssetType = 'card' | 'device' type AssetType = 'card' | 'device'
// 统一的资产项 // 统一的资产项
interface AssetItem { interface AssetItem {
id: string id : string
type: AssetType type : AssetType
name: string name : string
status: string status : string
statusColor: string statusColor : string
detail: string detail : string
raw: CardInfo | DeviceInfo raw : CardInfo | DeviceInfo
} }
// 当前标签 // 当前标签
const activeTab = ref<'card' | 'device'>('card') const activeTab = ref<'card' | 'device'>('card')
// 资产列表 // 资产列表
const cardList = ref<CardInfo[]>([]) const cardList = ref<CardInfo[]>([])
const deviceList = ref<DeviceInfo[]>([]) const deviceList = ref<DeviceInfo[]>([])
// 加载状态 // 加载状态
const loading = ref(false) const loading = ref(false)
// 分页 // 分页
const cardPage = ref(1) const cardPage = ref(1)
const devicePage = ref(1) const devicePage = ref(1)
const cardHasMore = ref(true) const cardHasMore = ref(true)
const deviceHasMore = ref(true) const deviceHasMore = ref(true)
const pageSize = 20 const pageSize = 20
// 搜索和筛选 // 搜索和筛选
const searchType = ref<'iccid' | 'virtual'>('iccid') const searchType = ref<'iccid' | 'virtual'>('iccid')
const searchKeyword = ref('') const searchKeyword = ref('')
const carrierFilter = ref<number | undefined>(undefined) const carrierFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined) const statusFilter = ref<number | undefined>(undefined)
const showCarrierPicker = ref(false) const showCarrierPicker = ref(false)
const showStatusPicker = ref(false) const showStatusPicker = ref(false)
const showSearchTypePicker = ref(false) const showSearchTypePicker = ref(false)
// 运营商列表 // 运营商列表
const carrierList = ref<CarrierInfo[]>([]) const carrierList = ref<CarrierInfo[]>([])
// 运营商选择器选项 // 运营商选择器选项
const carrierOptions = computed(() => [ const carrierOptions = computed(() => [
{ label: '全部运营商', value: undefined }, { label: '全部运营商', value: undefined },
...carrierList.value.map(c => ({ label: c.carrier_name, value: c.id })) ...carrierList.value.map(c => ({ label: c.carrier_name, value: c.id }))
]) ])
// 状态选择器选项 // 状态选择器选项
const statusOptions = [ const statusOptions = [
{ label: '全部状态', value: undefined }, { label: '全部状态', value: undefined },
{ label: '在库', value: 1 }, { label: '在库', value: 1 },
{ label: '已分销', value: 2 }, { label: '已分销', value: 2 },
{ label: '已激活', value: 3 }, { label: '已激活', value: 3 },
{ label: '已停用', value: 4 } { label: '已停用', value: 4 }
] ]
// 搜索类型选项 // 搜索类型选项
const searchTypeOptions = [ const searchTypeOptions = [
{ label: 'ICCID', value: 'iccid' }, { label: 'ICCID', value: 'iccid' },
{ label: '虚拟号', value: 'virtual' } { label: '虚拟号', value: 'virtual' }
] ]
// 合并后的资产列表 // 合并后的资产列表
const assetList = computed<AssetItem[]>(() => { const assetList = computed<AssetItem[]>(() => {
const cards: AssetItem[] = cardList.value.map(card => ({ const cards : AssetItem[] = cardList.value.map(card => ({
id: String(card.id), id: String(card.id),
type: 'card' as AssetType, type: 'card' as AssetType,
name: card.iccid, name: card.iccid,
@@ -154,7 +154,7 @@ const assetList = computed<AssetItem[]>(() => {
raw: card, raw: card,
})) }))
const devices: AssetItem[] = deviceList.value.map(device => ({ const devices : AssetItem[] = deviceList.value.map(device => ({
id: String(device.id || device.device_id), id: String(device.id || device.device_id),
type: 'device' as AssetType, type: 'device' as AssetType,
name: device.device_name || '-', name: device.device_name || '-',
@@ -169,11 +169,11 @@ const assetList = computed<AssetItem[]>(() => {
if (activeTab.value === 'card') return cards if (activeTab.value === 'card') return cards
if (activeTab.value === 'device') return devices if (activeTab.value === 'device') return devices
return cards return cards
}) })
// 获取企业ID企业端使用 // 获取企业ID企业端使用
async function getEnterpriseId() { async function getEnterpriseId() {
if (enterpriseId.value) return enterpriseId.value if (enterpriseId.value) return enterpriseId.value
const cachedUserInfo = uni.getStorageSync('user_info') const cachedUserInfo = uni.getStorageSync('user_info')
@@ -188,10 +188,10 @@ async function getEnterpriseId() {
} }
enterpriseId.value = apiUserInfo.enterprise_id enterpriseId.value = apiUserInfo.enterprise_id
return enterpriseId.value return enterpriseId.value
} }
// 加载IoT卡列表兼容企业端和代理端 // 加载IoT卡列表兼容企业端和代理端
async function loadCards(isRefresh = false) { async function loadCards(isRefresh = false) {
if (loading.value || (!cardHasMore.value && !isRefresh)) return if (loading.value || (!cardHasMore.value && !isRefresh)) return
loading.value = true loading.value = true
@@ -203,7 +203,7 @@ async function loadCards(isRefresh = false) {
cardHasMore.value = true cardHasMore.value = true
} }
const params: any = { const params : any = {
page: cardPage.value, page: cardPage.value,
page_size: pageSize, page_size: pageSize,
} }
@@ -223,7 +223,7 @@ async function loadCards(isRefresh = false) {
} }
} }
let cardsData: any let cardsData : any
// 根据用户类型调用不同接口 // 根据用户类型调用不同接口
if (userInfo.value?.user_type === 3) { if (userInfo.value?.user_type === 3) {
@@ -247,7 +247,7 @@ async function loadCards(isRefresh = false) {
cardHasMore.value = newItems.length >= pageSize cardHasMore.value = newItems.length >= pageSize
cardPage.value++ cardPage.value++
} }
catch (error: any) { catch (error : any) {
console.error('加载IoT卡列表失败:', error) console.error('加载IoT卡列表失败:', error)
if (error instanceof Error && error.message && !error.statusCode) { if (error instanceof Error && error.message && !error.statusCode) {
uni.$u.toast(error.message) uni.$u.toast(error.message)
@@ -256,10 +256,10 @@ async function loadCards(isRefresh = false) {
finally { finally {
loading.value = false loading.value = false
} }
} }
// 加载设备列表(兼容企业端和代理端) // 加载设备列表(兼容企业端和代理端)
async function loadDevices(isRefresh = false) { async function loadDevices(isRefresh = false) {
if (loading.value || (!deviceHasMore.value && !isRefresh)) return if (loading.value || (!deviceHasMore.value && !isRefresh)) return
loading.value = true loading.value = true
@@ -271,7 +271,7 @@ async function loadDevices(isRefresh = false) {
deviceHasMore.value = true deviceHasMore.value = true
} }
const params: any = { const params : any = {
page: devicePage.value, page: devicePage.value,
page_size: pageSize, page_size: pageSize,
} }
@@ -283,7 +283,7 @@ async function loadDevices(isRefresh = false) {
params.status = statusFilter.value params.status = statusFilter.value
} }
let devicesData: any let devicesData : any
// 根据用户类型调用不同接口 // 根据用户类型调用不同接口
if (userInfo.value?.user_type === 3) { if (userInfo.value?.user_type === 3) {
@@ -307,7 +307,7 @@ async function loadDevices(isRefresh = false) {
deviceHasMore.value = newItems.length >= pageSize deviceHasMore.value = newItems.length >= pageSize
devicePage.value++ devicePage.value++
} }
catch (error: any) { catch (error : any) {
console.error('加载设备列表失败:', error) console.error('加载设备列表失败:', error)
if (error instanceof Error && error.message && !error.statusCode) { if (error instanceof Error && error.message && !error.statusCode) {
uni.$u.toast(error.message) uni.$u.toast(error.message)
@@ -316,69 +316,69 @@ async function loadDevices(isRefresh = false) {
finally { finally {
loading.value = false loading.value = false
} }
} }
// 加载更多 // 加载更多
function loadMore() { function loadMore() {
if (activeTab.value === 'card') { if (activeTab.value === 'card') {
loadCards() loadCards()
} else { } else {
loadDevices() loadDevices()
} }
} }
// 刷新列表 // 刷新列表
function refreshList() { function refreshList() {
if (activeTab.value === 'card') { if (activeTab.value === 'card') {
loadCards(true) loadCards(true)
} else { } else {
loadDevices(true) loadDevices(true)
} }
} }
// 加载运营商列表 // 加载运营商列表
async function loadCarriers() { async function loadCarriers() {
try { try {
const response = await getCarriers({ page: 1, page_size: 100 }) const response = await getCarriers({ page: 1, page_size: 100 })
carrierList.value = response?.items || [] carrierList.value = response?.items || []
} catch (error) { } catch (error) {
console.error('加载运营商列表失败:', error) console.error('加载运营商列表失败:', error)
} }
} }
// 选择运营商 // 选择运营商
function onCarrierConfirm(option: { label: string, value: any }) { function onCarrierConfirm(option : { label : string, value : any }) {
carrierFilter.value = option.value carrierFilter.value = option.value
refreshList() refreshList()
} }
// 选择状态 // 选择状态
function onStatusConfirm(option: { label: string, value: any }) { function onStatusConfirm(option : { label : string, value : any }) {
statusFilter.value = option.value statusFilter.value = option.value
refreshList() refreshList()
} }
// 选择搜索类型 // 选择搜索类型
function onSearchTypeConfirm(option: { label: string, value: any }) { function onSearchTypeConfirm(option : { label : string, value : any }) {
searchType.value = option.value searchType.value = option.value
} }
// 获取当前选中的运营商名称 // 获取当前选中的运营商名称
const selectedCarrierName = computed(() => { const selectedCarrierName = computed(() => {
if (carrierFilter.value === undefined) return '运营商' if (carrierFilter.value === undefined) return '运营商'
const option = carrierOptions.value.find(o => o.value === carrierFilter.value) const option = carrierOptions.value.find(o => o.value === carrierFilter.value)
return option?.label || '运营商' return option?.label || '运营商'
}) })
// 获取当前选中的状态名称 // 获取当前选中的状态名称
const selectedStatusName = computed(() => { const selectedStatusName = computed(() => {
if (statusFilter.value === undefined) return '状态' if (statusFilter.value === undefined) return '状态'
const option = statusOptions.find(o => o.value === statusFilter.value) const option = statusOptions.find(o => o.value === statusFilter.value)
return option?.label || '状态' return option?.label || '状态'
}) })
// 查看资产详情 // 查看资产详情
function viewAssetDetail(asset: AssetItem) { function viewAssetDetail(asset : AssetItem) {
if (asset.type === 'card') { if (asset.type === 'card') {
const card = asset.raw as CardInfo const card = asset.raw as CardInfo
uni.navigateTo({ uni.navigateTo({
@@ -390,16 +390,13 @@ function viewAssetDetail(asset: AssetItem) {
url: `/pages/agent-system/asset-search/index?virtual_no=${device.virtual_no}`, url: `/pages/agent-system/asset-search/index?virtual_no=${device.virtual_no}`,
}) })
} }
} }
// 切换标签 // 切换标签
function changeTab(tab: 'card' | 'device') { function changeTab(tab : 'card' | 'device') {
if (activeTab.value === tab) return if (activeTab.value === tab) return
activeTab.value = tab activeTab.value = tab
searchKeyword.value = ''
carrierFilter.value = undefined
statusFilter.value = undefined
// 切换后加载对应数据 // 切换后加载对应数据
if (tab === 'card' && cardList.value.length === 0) { if (tab === 'card' && cardList.value.length === 0) {
@@ -407,10 +404,10 @@ function changeTab(tab: 'card' | 'device') {
} else if (tab === 'device' && deviceList.value.length === 0) { } else if (tab === 'device' && deviceList.value.length === 0) {
loadDevices(true) loadDevices(true)
} }
} }
// 设备停机 // 设备停机
async function handleStopDevice(asset: AssetItem, event: Event) { async function handleStopDevice(asset : AssetItem, event : Event) {
event.stopPropagation() event.stopPropagation()
const device = asset.raw as DeviceInfo const device = asset.raw as DeviceInfo
@@ -440,7 +437,7 @@ async function handleStopDevice(asset: AssetItem, event: Event) {
refreshList() refreshList()
} }
}) })
} catch (error: any) { } catch (error : any) {
uni.hideLoading() uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示 // 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('设备停机失败:', error) console.error('设备停机失败:', error)
@@ -448,10 +445,10 @@ async function handleStopDevice(asset: AssetItem, event: Event) {
} }
} }
}) })
} }
// 设备复机 // 设备复机
async function handleStartDevice(asset: AssetItem, event: Event) { async function handleStartDevice(asset : AssetItem, event : Event) {
event.stopPropagation() event.stopPropagation()
const device = asset.raw as DeviceInfo const device = asset.raw as DeviceInfo
@@ -473,7 +470,7 @@ async function handleStartDevice(asset: AssetItem, event: Event) {
uni.$u.toast('复机成功') uni.$u.toast('复机成功')
// 刷新列表 // 刷新列表
refreshList() refreshList()
} catch (error: any) { } catch (error : any) {
uni.hideLoading() uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示 // 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('设备复机失败:', error) console.error('设备复机失败:', error)
@@ -481,10 +478,10 @@ async function handleStartDevice(asset: AssetItem, event: Event) {
} }
} }
}) })
} }
// 单卡停机 // 单卡停机
async function handleStopCard(asset: AssetItem, event: Event) { async function handleStopCard(asset : AssetItem, event : Event) {
event.stopPropagation() event.stopPropagation()
const card = asset.raw as CardInfo const card = asset.raw as CardInfo
@@ -506,7 +503,7 @@ async function handleStopCard(asset: AssetItem, event: Event) {
uni.$u.toast('停机成功') uni.$u.toast('停机成功')
// 刷新列表 // 刷新列表
refreshList() refreshList()
} catch (error: any) { } catch (error : any) {
uni.hideLoading() uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示 // 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('单卡停机失败:', error) console.error('单卡停机失败:', error)
@@ -514,10 +511,10 @@ async function handleStopCard(asset: AssetItem, event: Event) {
} }
} }
}) })
} }
// 单卡复机 // 单卡复机
async function handleStartCard(asset: AssetItem, event: Event) { async function handleStartCard(asset : AssetItem, event : Event) {
event.stopPropagation() event.stopPropagation()
const card = asset.raw as CardInfo const card = asset.raw as CardInfo
@@ -539,7 +536,7 @@ async function handleStartCard(asset: AssetItem, event: Event) {
uni.$u.toast('复机成功') uni.$u.toast('复机成功')
// 刷新列表 // 刷新列表
refreshList() refreshList()
} catch (error: any) { } catch (error : any) {
uni.hideLoading() uni.hideLoading()
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示 // 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
console.error('单卡复机失败:', error) console.error('单卡复机失败:', error)
@@ -547,18 +544,18 @@ async function handleStartCard(asset: AssetItem, event: Event) {
} }
} }
}) })
} }
// 滚动事件处理 // 滚动事件处理
function onScroll(e: any) { function onScroll(e : any) {
const { scrollTop, scrollHeight, clientHeight } = e.target const { scrollTop, scrollHeight, clientHeight } = e.target
// 当滚动到底部附近时加载更多 // 当滚动到底部附近时加载更多
if (scrollHeight - scrollTop - clientHeight < 100) { if (scrollHeight - scrollTop - clientHeight < 100) {
loadMore() loadMore()
} }
} }
onMounted(async () => { onMounted(async () => {
// 1. 加载用户信息(判断是企业端还是代理端) // 1. 加载用户信息(判断是企业端还是代理端)
try { try {
const user = await getUserInfo() const user = await getUserInfo()
@@ -572,35 +569,25 @@ onMounted(async () => {
// 3. 默认加载IoT卡列表 // 3. 默认加载IoT卡列表
loadCards() loadCards()
}) })
</script> </script>
<template> <template>
<view class="bg-page safe-area-bottom min-h-screen flex flex-col"> <view class="bg-page min-h-screen">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<view class="bg-white px-4 pt-4 pb-3 mb-3"> <view class="bg-white px-4 pb-3 mb-3 fixed top-0 left-0 right-0 z-50"
style="padding-top: constant(safe-area-inset-top); padding-top: env(safe-area-inset-top);">
<!-- 搜索框 --> <!-- 搜索框 -->
<view class="relative flex items-center"> <view class="relative flex items-center">
<input <input v-model="searchKeyword"
v-model="searchKeyword"
class="flex-1 h-10 bg-[#f1f5f9] rounded-full pl-4 pr-12 text-sm text-[#1e293b] placeholder-[#94a3b8]" class="flex-1 h-10 bg-[#f1f5f9] rounded-full pl-4 pr-12 text-sm text-[#1e293b] placeholder-[#94a3b8]"
:placeholder="searchType === 'iccid' ? '搜索ICCID' : '搜索虚拟号'" :placeholder="searchType === 'iccid' ? '搜索ICCID' : '搜索虚拟号'" @confirm="refreshList" />
@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]"
<view @click="searchKeyword = ''">
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" /> <i class="i-mdi-close-circle text-lg" />
</view> </view>
<view <view class="w-8 h-8 flex items-center justify-center text-brand" @click="refreshList">
class="w-8 h-8 flex items-center justify-center text-brand"
@click="refreshList"
>
<i class="i-mdi-magnify text-xl" /> <i class="i-mdi-magnify text-xl" />
</view> </view>
</view> </view>
@@ -610,97 +597,73 @@ onMounted(async () => {
<view class="flex items-center gap-2 mt-3 flex-wrap"> <view class="flex items-center gap-2 mt-3 flex-wrap">
<view class="flex bg-[#f1f5f9] rounded-full flex-shrink-0"> <view class="flex bg-[#f1f5f9] rounded-full flex-shrink-0">
<view <view class="px-3 py-1.5 text-12px font-medium transition-all rounded-full"
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')">
:class="activeTab === 'card' ? 'bg-brand text-white' : 'text-[#64748b]'"
@click="changeTab('card')"
>
IoT卡 IoT卡
</view> </view>
<view <view class="px-3 py-1.5 text-12px font-medium transition-all rounded-full"
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')">
:class="activeTab === 'device' ? 'bg-brand text-white' : 'text-[#64748b]'"
@click="changeTab('device')"
>
设备 设备
</view> </view>
</view> </view>
<view <view class="flex items-center gap-1 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0"
class="flex items-center gap-1 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0" @click="showSearchTypePicker = true">
@click="showSearchTypePicker = true"
>
<text class="text-12px" :class="searchType !== 'iccid' ? 'text-brand font-medium' : 'text-[#64748b]'"> <text class="text-12px" :class="searchType !== 'iccid' ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ searchType === 'iccid' ? 'ICCID' : '虚拟号' }} {{ searchType === 'iccid' ? 'ICCID' : '虚拟号' }}
</text> </text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8]" /> <i class="i-mdi-chevron-down text-12px text-[#94a3b8]" />
</view> </view>
<view <view class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0 max-w-36"
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">
@click="showCarrierPicker = true" <text class="text-12px truncate"
> :class="carrierFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'">
<text class="text-12px truncate" :class="carrierFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ selectedCarrierName }} {{ selectedCarrierName }}
</text> </text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8] flex-shrink-0" /> <i class="i-mdi-chevron-down text-12px text-[#94a3b8] flex-shrink-0" />
</view> </view>
<view <view class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0"
class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0" @click="showStatusPicker = true">
@click="showStatusPicker = true"
>
<text class="text-12px" :class="statusFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'"> <text class="text-12px" :class="statusFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ selectedStatusName }} {{ selectedStatusName }}
</text> </text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8]" /> <i class="i-mdi-chevron-down text-12px text-[#94a3b8]" />
</view> </view>
<view <view v-if="carrierFilter !== undefined || statusFilter !== undefined"
v-if="carrierFilter !== undefined || statusFilter !== undefined"
class="flex items-center gap-1 px-2 py-1.5 rounded-full bg-[#ff4d4f]/10 text-[#ff4d4f] flex-shrink-0" 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; refreshList()" @click="carrierFilter = undefined; statusFilter = undefined; refreshList()">
>
<i class="i-mdi-close text-12px" /> <i class="i-mdi-close text-12px" />
<text class="text-12px font-medium">清除</text> <text class="text-12px font-medium">清除</text>
</view> </view>
</view> </view>
</view> </view>
<!-- 列表内容区域 -->
<view class="safe-area-bottom min-h-screen flex flex-col pt-28">
<!-- 加载状态 --> <!-- 加载状态 -->
<Skeleton v-if="loading && assetList.length === 0" type="list" /> <Skeleton v-if="loading && assetList.length === 0" type="list" />
<!-- 资产列表 --> <!-- 资产列表 -->
<view <view v-else class="flex-1 overflow-y-auto min-h-0" @scroll="onScroll">
v-else
class="flex-1 overflow-y-auto min-h-0"
@scroll="onScroll"
>
<view v-if="assetList.length > 0" class="px-4 pb-4"> <view v-if="assetList.length > 0" class="px-4 pb-4">
<view <view v-for="asset in assetList" :key="`${asset.type}-${asset.id}`"
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" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3 cursor-pointer"
@click="viewAssetDetail(asset)" @click="viewAssetDetail(asset)">
>
<!-- 顶部区域左侧状态+名称右侧类型标签 --> <!-- 顶部区域左侧状态+名称右侧类型标签 -->
<view class="flex items-start justify-between mb-3"> <view class="flex items-start justify-between mb-3">
<view class="flex items-center gap-2 flex-1 min-w-0"> <view class="flex items-center gap-2 flex-1 min-w-0">
<view <view class="w-2 h-2 rounded-full mt-1 flex-shrink-0" :style="{ backgroundColor: asset.statusColor }" />
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"> <text class="text-15px font-600 text-[#212121] truncate">
{{ asset.name }} {{ asset.name }}
</text> </text>
</view> </view>
<view <view class="px-2 py-1 rounded-6px text-11px font-500 flex-shrink-0 ml-2" :style="{
class="px-2 py-1 rounded-6px text-11px font-500 flex-shrink-0 ml-2"
:style="{
backgroundColor: asset.type === 'card' ? '#e6f4ff' : '#f5f5f5', backgroundColor: asset.type === 'card' ? '#e6f4ff' : '#f5f5f5',
color: asset.type === 'card' ? 'var(--brand-primary)' : '#666', color: asset.type === 'card' ? 'var(--brand-primary)' : '#666',
}" }">
>
{{ asset.type === 'card' ? 'IoT卡' : '设备' }} {{ asset.type === 'card' ? 'IoT卡' : '设备' }}
</view> </view>
</view> </view>
@@ -715,10 +678,7 @@ onMounted(async () => {
<!-- 底部区域状态标签 + 操作按钮 --> <!-- 底部区域状态标签 + 操作按钮 -->
<view class="flex items-center justify-between pt-3 border-t border-[#f5f5f5]"> <view class="flex items-center justify-between pt-3 border-t border-[#f5f5f5]">
<view class="flex items-center gap-1"> <view class="flex items-center gap-1">
<view <view class="w-1.5 h-1.5 rounded-full" :style="{ backgroundColor: asset.statusColor }" />
class="w-1.5 h-1.5 rounded-full"
:style="{ backgroundColor: asset.statusColor }"
/>
<text class="text-13px font-500" :style="{ color: asset.statusColor }"> <text class="text-13px font-500" :style="{ color: asset.statusColor }">
{{ asset.status }} {{ asset.status }}
</text> </text>
@@ -730,30 +690,26 @@ onMounted(async () => {
<template v-if="(asset.raw as CardInfo).network_status === 0"> <template v-if="(asset.raw as CardInfo).network_status === 0">
<view <view
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" 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)" @click="handleStartCard(asset, $event)">
>
复机 复机
</view> </view>
</template> </template>
<template v-else-if="(asset.raw as CardInfo).network_status === 1"> <template v-else-if="(asset.raw as CardInfo).network_status === 1">
<view <view
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" 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)" @click="handleStopCard(asset, $event)">
>
停机 停机
</view> </view>
</template> </template>
<template v-else> <template v-else>
<view <view
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" 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)" @click="handleStopCard(asset, $event)">
>
停机 停机
</view> </view>
<view <view
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" 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)" @click="handleStartCard(asset, $event)">
>
复机 复机
</view> </view>
</template> </template>
@@ -763,14 +719,12 @@ onMounted(async () => {
<template v-else> <template v-else>
<view <view
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" 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)" @click="handleStopDevice(asset, $event)">
>
停机 停机
</view> </view>
<view <view
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" 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)" @click="handleStartDevice(asset, $event)">
>
复机 复机
</view> </view>
</template> </template>
@@ -785,7 +739,8 @@ onMounted(async () => {
<i class="i-mdi-loading animate-spin text-xl text-brand mr-2" /> <i class="i-mdi-loading animate-spin text-xl text-brand mr-2" />
<text class="text-sm text-[#64748b]">加载中...</text> <text class="text-sm text-[#64748b]">加载中...</text>
</view> </view>
<view v-else-if="!((activeTab === 'card' && cardHasMore) || (activeTab === 'device' && deviceHasMore))" class="py-4 text-center"> <view v-else-if="!((activeTab === 'card' && cardHasMore) || (activeTab === 'device' && deviceHasMore))"
class="py-4 text-center">
<text class="text-sm text-[#94a3b8]">没有更多了</text> <text class="text-sm text-[#94a3b8]">没有更多了</text>
</view> </view>
</view> </view>
@@ -801,27 +756,15 @@ onMounted(async () => {
</view> </view>
<!-- 运营商选择器 --> <!-- 运营商选择器 -->
<SimplePicker <SimplePicker v-model:show="showCarrierPicker" :options="carrierOptions" title="选择运营商"
v-model:show="showCarrierPicker" @confirm="onCarrierConfirm" />
:options="carrierOptions"
title="选择运营商"
@confirm="onCarrierConfirm"
/>
<!-- 状态选择器 --> <!-- 状态选择器 -->
<SimplePicker <SimplePicker v-model:show="showStatusPicker" :options="statusOptions" title="选择状态" @confirm="onStatusConfirm" />
v-model:show="showStatusPicker"
:options="statusOptions"
title="选择状态"
@confirm="onStatusConfirm"
/>
<!-- 搜索类型选择器 --> <!-- 搜索类型选择器 -->
<SimplePicker <SimplePicker v-model:show="showSearchTypePicker" :options="searchTypeOptions" title="选择搜索类型"
v-model:show="showSearchTypePicker" @confirm="onSearchTypeConfirm" />
:options="searchTypeOptions" </view>
title="选择搜索类型"
@confirm="onSearchTypeConfirm"
/>
</view> </view>
</template> </template>