fix: 优化体验
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 3h10m20s

This commit is contained in:
sexygoat
2026-05-09 16:13:39 +08:00
parent b54d8f5b32
commit 28246f5345
17 changed files with 1684 additions and 255 deletions

View File

@@ -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>