This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
import { stopAsset, startAsset } from '@/api/assets'
|
||||
import Skeleton from '@/components/Skeleton.vue'
|
||||
import SimplePicker from '@/components/SimplePicker.vue'
|
||||
import { formatTime, formatDataSize } from '@/utils/format'
|
||||
import ShopCascadePicker from '@/components/ShopCascadePicker.vue'
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
// 卡片信息接口
|
||||
@@ -18,6 +18,7 @@
|
||||
imsi ?: string
|
||||
msisdn ?: string
|
||||
virtual_no ?: string
|
||||
device_virtual_no ?: string | null
|
||||
virtual_number ?: string // 企业端字段
|
||||
status ?: number
|
||||
status_name ?: string
|
||||
@@ -95,9 +96,15 @@
|
||||
status : string
|
||||
statusColor : string
|
||||
detail : string
|
||||
bindingStatus ?: string
|
||||
raw : CardInfo | DeviceInfo
|
||||
}
|
||||
|
||||
interface SelectedShopFilter {
|
||||
id : number
|
||||
shop_name : string
|
||||
}
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref<'card' | 'device'>('card')
|
||||
// 资产列表
|
||||
@@ -112,15 +119,21 @@
|
||||
const deviceHasMore = ref(true)
|
||||
const pageSize = 20
|
||||
// 搜索和筛选
|
||||
const searchType = ref<'iccid' | 'virtual'>('iccid')
|
||||
const searchKeyword = ref('')
|
||||
const carrierFilter = ref<number | undefined>(undefined)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
const selectedShopFilter = ref<SelectedShopFilter | null>(null)
|
||||
const showCarrierPicker = ref(false)
|
||||
const showStatusPicker = ref(false)
|
||||
const showSearchTypePicker = ref(false)
|
||||
const showShopPicker = ref(false)
|
||||
// 运营商列表
|
||||
const carrierList = ref<CarrierInfo[]>([])
|
||||
const isAgent = computed(() => userInfo.value?.user_type === 3)
|
||||
const searchPlaceholder = computed(() => activeTab.value === 'card' ? '搜索ICCID' : '搜索设备号')
|
||||
|
||||
function hasStatusCode(error : unknown) {
|
||||
return typeof error === 'object' && error !== null && 'statusCode' in error
|
||||
}
|
||||
// 运营商选择器选项
|
||||
const carrierOptions = computed(() => [
|
||||
{ label: '全部运营商', value: undefined },
|
||||
@@ -134,11 +147,16 @@
|
||||
{ label: '已激活', value: 3 },
|
||||
{ label: '已停用', value: 4 }
|
||||
]
|
||||
// 搜索类型选项
|
||||
const searchTypeOptions = [
|
||||
{ label: 'ICCID', value: 'iccid' },
|
||||
{ label: '虚拟号', value: 'virtual' }
|
||||
]
|
||||
|
||||
function getCardBindingStatus(card : CardInfo) {
|
||||
return card.device_virtual_no?.trim() ? '已绑定设备' : '未绑定设备'
|
||||
}
|
||||
|
||||
function getCardBindingTagClass(bindingStatus ?: string) {
|
||||
return bindingStatus === '已绑定设备'
|
||||
? 'bg-[#e8f5e9] text-[#2e7d32]'
|
||||
: 'bg-[#fff7e6] text-[#d46b08]'
|
||||
}
|
||||
|
||||
// 合并后的资产列表
|
||||
const assetList = computed<AssetItem[]>(() => {
|
||||
@@ -150,7 +168,11 @@
|
||||
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 || '-'}`,
|
||||
detail: `${[
|
||||
`虚拟号: ${card.virtual_no || card.virtual_number || '-'}`,
|
||||
`运营商: ${card.carrier_name || card.operator || '-'}`,
|
||||
].filter(Boolean).join(' | ')}`,
|
||||
bindingStatus: isAgent.value ? getCardBindingStatus(card) : undefined,
|
||||
raw: card,
|
||||
}))
|
||||
|
||||
@@ -215,12 +237,11 @@
|
||||
if (statusFilter.value !== undefined) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
if (searchType.value === 'iccid') {
|
||||
params.iccid = searchKeyword.value
|
||||
} else {
|
||||
params.virtual_no = searchKeyword.value
|
||||
}
|
||||
if (searchKeyword.value.trim()) {
|
||||
params.iccid = searchKeyword.value.trim()
|
||||
}
|
||||
if (userInfo.value?.user_type === 3 && selectedShopFilter.value) {
|
||||
params.shop_id = selectedShopFilter.value.id
|
||||
}
|
||||
|
||||
let cardsData : any
|
||||
@@ -249,7 +270,7 @@
|
||||
}
|
||||
catch (error : any) {
|
||||
console.error('加载IoT卡列表失败:', error)
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
if (error instanceof Error && error.message && !hasStatusCode(error)) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
}
|
||||
@@ -276,12 +297,15 @@
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
if (searchKeyword.value) {
|
||||
params.virtual_no = searchKeyword.value
|
||||
if (searchKeyword.value.trim()) {
|
||||
params.virtual_no = 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
|
||||
|
||||
@@ -309,7 +333,7 @@
|
||||
}
|
||||
catch (error : any) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
if (error instanceof Error && error.message && !hasStatusCode(error)) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
}
|
||||
@@ -339,7 +363,7 @@
|
||||
// 加载运营商列表
|
||||
async function loadCarriers() {
|
||||
try {
|
||||
const response = await getCarriers({ page: 1, page_size: 100 })
|
||||
const response = await getCarriers({ page: 1, size: 100 })
|
||||
carrierList.value = response?.items || []
|
||||
} catch (error) {
|
||||
console.error('加载运营商列表失败:', error)
|
||||
@@ -358,11 +382,6 @@
|
||||
refreshList()
|
||||
}
|
||||
|
||||
// 选择搜索类型
|
||||
function onSearchTypeConfirm(option : { label : string, value : any }) {
|
||||
searchType.value = option.value
|
||||
}
|
||||
|
||||
// 获取当前选中的运营商名称
|
||||
const selectedCarrierName = computed(() => {
|
||||
if (carrierFilter.value === undefined) return '运营商'
|
||||
@@ -377,6 +396,15 @@
|
||||
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') {
|
||||
@@ -397,13 +425,7 @@
|
||||
if (activeTab.value === tab) return
|
||||
|
||||
activeTab.value = tab
|
||||
|
||||
// 切换后加载对应数据
|
||||
if (tab === 'card' && cardList.value.length === 0) {
|
||||
loadCards(true)
|
||||
} else if (tab === 'device' && deviceList.value.length === 0) {
|
||||
loadDevices(true)
|
||||
}
|
||||
refreshList()
|
||||
}
|
||||
|
||||
// 设备停机
|
||||
@@ -411,8 +433,9 @@
|
||||
event.stopPropagation()
|
||||
|
||||
const device = asset.raw as DeviceInfo
|
||||
const identifier = device.virtual_no
|
||||
|
||||
if (!device.virtual_no) {
|
||||
if (!identifier) {
|
||||
uni.$u.toast('设备虚拟号不存在')
|
||||
return
|
||||
}
|
||||
@@ -424,7 +447,7 @@
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '停机中...' })
|
||||
try {
|
||||
const result = await stopAsset(device.virtual_no)
|
||||
await stopAsset(identifier)
|
||||
uni.hideLoading()
|
||||
|
||||
// 显示停机结果 (统一接口不返回详情,仅提示成功)
|
||||
@@ -452,8 +475,9 @@
|
||||
event.stopPropagation()
|
||||
|
||||
const device = asset.raw as DeviceInfo
|
||||
const identifier = device.virtual_no
|
||||
|
||||
if (!device.virtual_no) {
|
||||
if (!identifier) {
|
||||
uni.$u.toast('设备虚拟号不存在')
|
||||
return
|
||||
}
|
||||
@@ -465,7 +489,7 @@
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '复机中...' })
|
||||
try {
|
||||
await startAsset(device.virtual_no)
|
||||
await startAsset(identifier)
|
||||
uni.hideLoading()
|
||||
uni.$u.toast('复机成功')
|
||||
// 刷新列表
|
||||
@@ -546,15 +570,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动事件处理
|
||||
function onScroll(e : any) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target
|
||||
// 当滚动到底部附近时加载更多
|
||||
if (scrollHeight - scrollTop - clientHeight < 100) {
|
||||
loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
const statusBarHeight = ref(0)
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -578,14 +593,14 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-page min-h-screen flex flex-col">
|
||||
<view class="bg-page h-screen flex flex-col overflow-hidden">
|
||||
<!-- 搜索区域 -->
|
||||
<view class="bg-white px-4 pt-4 pb-3 mb-3">
|
||||
<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="searchType === 'iccid' ? '搜索ICCID' : '搜索虚拟号'" @confirm="refreshList" />
|
||||
: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 = ''">
|
||||
@@ -611,14 +626,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center gap-1 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0"
|
||||
@click="showSearchTypePicker = true">
|
||||
<text class="text-12px" :class="searchType !== 'iccid' ? 'text-brand font-medium' : 'text-[#64748b]'">
|
||||
{{ searchType === 'iccid' ? 'ICCID' : '虚拟号' }}
|
||||
</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"
|
||||
@@ -636,9 +643,19 @@
|
||||
<i class="i-mdi-chevron-down text-12px text-[#94a3b8]" />
|
||||
</view>
|
||||
|
||||
<view v-if="carrierFilter !== undefined || statusFilter !== undefined"
|
||||
<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; refreshList()">
|
||||
@click="carrierFilter = undefined; statusFilter = undefined; selectedShopFilter = null; refreshList()">
|
||||
<i class="i-mdi-close text-12px" />
|
||||
<text class="text-12px font-medium">清除</text>
|
||||
</view>
|
||||
@@ -646,13 +663,13 @@
|
||||
</view>
|
||||
|
||||
<!-- 列表内容区域 -->
|
||||
<view class="safe-area-bottom min-h-screen overflow-y-auto">
|
||||
<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="flex-1 overflow-y-auto min-h-0" @scroll="onScroll">
|
||||
<view v-if="assetList.length > 0" class="px-4 pb-4">
|
||||
<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)">
|
||||
@@ -677,6 +694,14 @@
|
||||
<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>
|
||||
|
||||
<!-- 底部区域:状态标签 + 操作按钮 -->
|
||||
@@ -738,19 +763,30 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view v-if="loading" class="py-4 text-center">
|
||||
<i class="i-mdi-loading animate-spin text-xl text-brand mr-2" />
|
||||
<text class="text-sm text-[#64748b]">加载中...</text>
|
||||
<!-- 加载更多 -->
|
||||
<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"
|
||||
: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-if="!((activeTab === 'card' && cardHasMore) || (activeTab === 'device' && deviceHasMore))"
|
||||
class="py-4 text-center">
|
||||
<view v-else class="py-4 text-center">
|
||||
<text class="text-sm text-[#94a3b8]">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="px-4">
|
||||
<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>
|
||||
@@ -758,17 +794,16 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 运营商选择器 -->
|
||||
<SimplePicker v-model:show="showCarrierPicker" :options="carrierOptions" title="选择运营商"
|
||||
@confirm="onCarrierConfirm" />
|
||||
|
||||
<!-- 状态选择器 -->
|
||||
<SimplePicker v-model:show="showStatusPicker" :options="statusOptions" title="选择状态" @confirm="onStatusConfirm" />
|
||||
|
||||
<!-- 搜索类型选择器 -->
|
||||
<SimplePicker v-model:show="showSearchTypePicker" :options="searchTypeOptions" title="选择搜索类型"
|
||||
@confirm="onSearchTypeConfirm" />
|
||||
</view>
|
||||
|
||||
<!-- 运营商选择器 -->
|
||||
<SimplePicker v-model:show="showCarrierPicker" :options="carrierOptions" title="选择运营商"
|
||||
@confirm="onCarrierConfirm" />
|
||||
|
||||
<!-- 状态选择器 -->
|
||||
<SimplePicker v-model:show="showStatusPicker" :options="statusOptions" title="选择状态" @confirm="onStatusConfirm" />
|
||||
|
||||
<ShopCascadePicker v-model:show="showShopPicker" :selected-shop="selectedShopFilter" title="选择店铺"
|
||||
@confirm="onShopConfirm" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user