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

This commit is contained in:
sexygoat
2026-04-25 15:19:44 +08:00
parent 749860f194
commit 63fe81b3da

View File

@@ -7,99 +7,77 @@ import { getCarriers } from '@/api/carrier'
import type { CarrierInfo } from '@/api/carrier'
import SimplePicker from '@/components/SimplePicker.vue'
// 企业ID (用于调用企业接口)
const enterpriseId = ref<number>(0)
// 卡片列表
const cardList = ref<CardInfo[]>([])
// 加载状态
const loading = ref(false)
const loadingMore = ref(false)
// 搜索关键词
const searchKeyword = ref('')
// 状态筛选 (undefined 表示全部)
const statusFilter = ref<number | undefined>(undefined)
// 运营商筛选
const carrierFilter = ref<number | undefined>(undefined)
const showCarrierPicker = ref(false)
// 运营商列表
const showSearchTypePicker = ref(false)
const carrierList = ref<CarrierInfo[]>([])
const page = ref(1)
const pageSize = 20
const hasMore = ref(true)
const searchType = ref<'iccid' | 'virtual'>('iccid')
// 网络状态选项 (network_status: 0=停机, 1=正常)
const statusOptions = [
{ label: '全部', value: undefined },
{ label: '正常', value: 1 },
{ label: '停机', value: 0 },
]
// 运营商选项
const searchTypeOptions = [
{ label: 'ICCID', value: 'iccid' },
{ label: '虚拟号', value: 'virtual' }
]
const carrierOptions = computed(() => [
{ label: '全部运营商', value: undefined },
...carrierList.value.map(c => ({ label: c.carrier_name, value: c.id }))
])
// 获取当前选中的运营商名称
const selectedCarrierName = computed(() => {
if (carrierFilter.value === undefined) return '运营商'
const option = carrierOptions.value.find(o => o.value === carrierFilter.value)
return option?.label || '运营商'
})
// 分页参数
const page = ref(1)
const pageSize = 20
const hasMore = ref(true)
// 直接显示卡片列表 (搜索由后端处理)
const filteredCards = computed(() => {
return cardList.value
})
// 统计数据
const stats = computed(() => {
const total = cardList.value.length
// network_status: 0=停机, 1=正常
const active = cardList.value.filter(c => c.network_status === 1).length
const inactive = cardList.value.filter(c => c.network_status === 0).length
// status: 1=可用, 2=已分销, 3=已停用等
const distributed = cardList.value.filter(c => c.status === 2).length
return { total, active, inactive, distributed }
})
// 加载运营商列表
async function loadCarriers() {
try {
const response = await getCarriers({ page: 1, size: 20 })
const response = await getCarriers({ page: 1, size: 50 })
carrierList.value = response?.items || []
} catch (error) {
console.error('加载运营商列表失败:', error)
}
}
// 页面加载
onMounted(() => {
loadCarriers()
loadEnterpriseCards()
})
async function loadEnterpriseCards(isRefresh = false) {
if (loading.value) return
if (isRefresh) {
page.value = 1
hasMore.value = true
cardList.value = []
}
// 加载企业卡列表
async function loadEnterpriseCards() {
loading.value = true
try {
// 1. 获取企业ID (优先从本地存储获取)
if (!enterpriseId.value) {
const userInfo = uni.getStorageSync('user_info')
if (userInfo?.enterprise_id) {
enterpriseId.value = userInfo.enterprise_id
} else {
// 如果本地存储没有,则调用接口获取
const apiUserInfo = await getUserInfo()
if (!apiUserInfo.enterprise_id) {
throw new Error('未找到企业ID')
@@ -108,226 +86,214 @@ async function loadEnterpriseCards() {
}
}
// 2. 获取企业卡片列表
const params: any = {
page: page.value,
page_size: pageSize,
}
// 只有选择了具体状态才传 status 参数
if (statusFilter.value !== undefined) {
params.status = statusFilter.value
}
// 运营商筛选
if (carrierFilter.value !== undefined) {
params.carrier_id = carrierFilter.value
}
// 添加搜索关键词支持 (根据接口文档)
if (searchKeyword.value) {
// 如果关键词看起来像ICCID (纯数字)
if (/^\d+$/.test(searchKeyword.value)) {
if (searchType.value === 'iccid') {
params.iccid = searchKeyword.value
} else {
params.virtual_no = searchKeyword.value
}
}
const cardsData = await getEnterpriseCards(enterpriseId.value, params)
const items = cardsData?.items || []
if (page.value === 1) {
cardList.value = cardsData?.items || []
}
else {
cardList.value.push(...(cardsData?.items || []))
cardList.value = items
} else {
cardList.value.push(...items)
}
// 判断是否还有更多数据
const items = cardsData?.items || []
hasMore.value = items.length >= pageSize
}
catch (error: any) {
page.value++
} catch (error: any) {
console.error('加载企业卡列表失败:', error)
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
if (error instanceof Error && error.message && !error.statusCode) {
uni.$u.toast(error.message)
}
// API 请求错误已由拦截器统一处理
}
finally {
} finally {
loading.value = false
}
}
// 查看卡片详情
function viewCardDetail(card: any) {
// 跳转到资产详情页
uni.navigateTo({
url: `/pages/agent-system/asset-search/index?iccid=${card.iccid}`,
})
}
// 获取网络状态文本 (使用后端返回的 network_status_name)
function getNetworkStatusText(networkStatus: number, networkStatusName?: string) {
// 优先使用后端返回的状态名称
if (networkStatusName) {
return networkStatusName
}
// 降级处理
const map: Record<number, string> = {
0: '停机',
1: '正常',
}
return map[networkStatus] ?? '未知'
if (networkStatusName) return networkStatusName
return networkStatus === 1 ? '正常' : networkStatus === 0 ? '停机' : '未知'
}
// 获取网络状态颜色
function getNetworkStatusColor(networkStatus: number) {
const map: Record<number, any> = {
1: { bg: '#e8f5e9', text: '#2e7d32' }, // 正常
0: { bg: '#ffebee', text: '#c62828' }, // 停机
}
return map[networkStatus] ?? { bg: '#f5f5f5', text: '#999' }
return networkStatus === 1
? { bg: '#e8f5e9', text: '#2e7d32' }
: networkStatus === 0
? { bg: '#ffebee', text: '#c62828' }
: { bg: '#f5f5f5', text: '#999' }
}
// 获取卡状态文本 (使用后端返回的 status_name)
function getCardStatusText(status: number, statusName?: string) {
// 优先使用后端返回的状态名称
if (statusName) {
return statusName
}
// 降级处理
const map: Record<number, string> = {
1: '可用',
2: '已分销',
3: '已停用',
}
if (statusName) return statusName
const map: Record<number, string> = { 1: '可用', 2: '已分销', 3: '已停用' }
return map[status] ?? '未知'
}
// 清空搜索
function clearSearch() {
searchKeyword.value = ''
page.value = 1
hasMore.value = true
cardList.value = []
loadEnterpriseCards()
}
// 执行搜索
function handleSearch() {
page.value = 1
hasMore.value = true
cardList.value = []
loadEnterpriseCards()
loadEnterpriseCards(true)
}
// 刷新列表
function refreshList() {
page.value = 1
hasMore.value = true
cardList.value = []
loadEnterpriseCards()
}
// 状态筛选变化
function handleStatusChange(status: number | undefined) {
statusFilter.value = status
page.value = 1
hasMore.value = true
cardList.value = []
loadEnterpriseCards()
}
// 运营商筛选变化 (SimplePicker确认时触发)
function onCarrierConfirm(option: { label: string, value: any }) {
carrierFilter.value = option.value
page.value = 1
hasMore.value = true
cardList.value = []
loadEnterpriseCards()
loadEnterpriseCards(true)
}
function onSearchTypeConfirm(option: { label: string, value: any }) {
searchType.value = option.value
}
function handleStatusChange(status: number | undefined) {
statusFilter.value = status
loadEnterpriseCards(true)
}
function loadMore() {
if (hasMore.value && !loading.value) {
loadEnterpriseCards()
}
}
function onScroll(e: any) {
const { scrollTop, scrollHeight, clientHeight } = e.target
if (scrollHeight - scrollTop - clientHeight < 100) {
loadMore()
}
}
onMounted(async () => {
loadCarriers()
loadEnterpriseCards()
})
</script>
<template>
<view class="bg-[var(--bg-page)] min-h-screen pb-safe">
<view v-if="loading" class="pt-20 center">
<text class="text-14px text-[var(--text-secondary)]">加载中...</text>
</view>
<view v-else>
<view class="bg-brand px-4 py-3 mx-4 mt-4 rounded-12px">
<text class="text-11px text-white/60">企业授权IoT卡</text>
<text class="text-24px font-700 text-white block mt-1">{{ stats.total }}</text>
</view>
<view class="px-4 mt-4">
<view class="flex items-center gap-2 mb-3">
<view class="bg-page min-h-screen flex flex-col">
<!-- 搜索区域 -->
<view class="bg-white px-4 pt-4 pb-3">
<!-- 搜索框 -->
<view class="relative flex items-center">
<input
v-model="searchKeyword"
class="flex-1 h-40px bg-[var(--bg-container)] rounded-full px-4 text-14px text-[var(--text-primary)]"
placeholder="搜索 ICCID / 虚拟号 / 手机号"
placeholder-class="text-[var(--text-quaternary)]"
class="flex-1 h-10 bg-[#f1f5f9] rounded-full pl-4 pr-12 text-sm text-[#1e293b] placeholder-[#94a3b8]"
:placeholder="searchType === 'iccid' ? '搜索ICCID' : '搜索虚拟号'"
@confirm="handleSearch"
>
<view
class="px-4 h-40px bg-brand text-white text-14px rounded-full flex items-center justify-center"
@click="handleSearch"
>
搜索
/>
<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>
<view class="flex items-center gap-2 mb-3">
<view class="flex-1 bg-[var(--bg-container)] rounded-8px px-3 h-40px flex items-center" @click="showCarrierPicker = true">
<text class="flex-1 text-14px text-[var(--text-primary)]">{{ selectedCarrierName }}</text>
<text class="text-12px text-[var(--text-tertiary)]"></text>
<view class="w-8 h-8 flex items-center justify-center text-brand" @click="handleSearch">
<i class="i-mdi-magnify text-xl" />
</view>
</view>
</view>
<view class="px-4">
<view class="bg-[var(--bg-container)] rounded-12px p-4 mb-3">
<view class="grid grid-cols-3 gap-3">
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">正常</text>
<text class="text-18px font-700 text-[#52c41a] mt-1">{{ stats.active }}</text>
<!-- 统计信息 -->
<view class="flex items-center justify-around gap-3 mt-3">
<view class="flex items-center gap-1">
<text class="text-12px text-[#999]">卡总数</text>
<text class="text-14px font-700 text-brand">{{ stats.total }}</text>
</view>
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">停机</text>
<text class="text-18px font-700 text-[#ff4d4f] mt-1">{{ stats.inactive }}</text>
<view class="flex items-center gap-1">
<view class="w-2 h-2 rounded-full bg-[#52c41a]" />
<text class="text-12px text-[#666]">正常</text>
<text class="text-12px font-600 text-[#212121]">{{ stats.active }}</text>
</view>
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">已分销</text>
<text class="text-18px font-700 text-brand mt-1">{{ stats.distributed }}</text>
<view class="flex items-center gap-1">
<view class="w-2 h-2 rounded-full bg-[#ff4d4f]" />
<text class="text-12px text-[#666]">停机</text>
<text class="text-12px font-600 text-[#212121]">{{ stats.inactive }}</text>
</view>
<view class="flex items-center gap-1">
<view class="w-2 h-2 rounded-full bg-brand" />
<text class="text-12px text-[#666]">已分销</text>
<text class="text-12px font-600 text-[#212121]">{{ stats.distributed }}</text>
</view>
</view>
<view class="flex items-center gap-2 mt-4">
<!-- 筛选器 -->
<view class="flex items-center gap-2 mt-3 flex-wrap">
<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" :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 bg-[#f1f5f9] rounded-full flex-shrink-0">
<view
v-for="option in statusOptions"
:key="option.value"
:class="[
'flex-1 text-center py-2 rounded-full text-13px transition-all',
statusFilter === option.value
? 'bg-brand text-white font-600'
: 'bg-[var(--bg-muted)] text-[var(--text-secondary)]',
]"
class="px-3 py-1.5 text-12px font-medium transition-all rounded-full"
:class="statusFilter === option.value ? 'bg-brand text-white' : 'text-[#64748b]'"
@click="handleStatusChange(option.value)"
>
{{ option.label }}
</view>
</view>
<view
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"
@click="carrierFilter = undefined; statusFilter = undefined; loadEnterpriseCards(true)"
>
<i class="i-mdi-close text-12px" />
<text class="text-12px font-medium">清除</text>
</view>
</view>
</view>
<view v-if="filteredCards.length > 0" class="space-y-3 pb-4">
<!-- 列表内容 -->
<view class="flex-1 overflow-y-auto min-h-0" @scroll="onScroll">
<!-- 卡片列表 -->
<view v-if="loading && cardList.length === 0" class="pt-20 flex justify-center">
<text class="text-14px text-[#64748b]">加载中...</text>
</view>
<view v-else-if="cardList.length > 0" class="px-4 pb-4">
<view
v-for="card in filteredCards"
v-for="card in cardList"
:key="card.id"
class="bg-[var(--bg-container)] rounded-12px p-4"
class="bg-white rounded-12px p-4 mt-3"
@click="viewCardDetail(card)"
>
<view class="flex items-center justify-between">
<view>
<text class="text-15px font-600 text-[var(--text-primary)]">{{ card.carrier_name || '-' }}</text>
<text class="text-12px text-[var(--text-tertiary)] ml-2">{{ getCardStatusText(card.status, card.status_name) }}</text>
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-2">
<text class="text-15px font-600 text-[#212121]">{{ card.carrier_name || '-' }}</text>
<text class="text-12px text-[#999]">{{ getCardStatusText(card.status, card.status_name) }}</text>
</view>
<view
:style="{
@@ -340,54 +306,56 @@ function onCarrierConfirm(option: { label: string, value: any }) {
</view>
</view>
<view class="mt-3 text-13px text-[var(--text-secondary)]">
<view class="text-13px text-[#666] mb-2">
{{ card.package_name || '暂无套餐' }}
</view>
<view class="mt-3 flex items-center justify-between text-12px text-[var(--text-tertiary)]">
<text>{{ card.iccid || '-' }}</text>
<text>{{ card.msisdn || '-' }}</text>
<view class="flex items-center justify-between text-12px text-[#999]">
<text class="truncate flex-1">{{ card.iccid || '-' }}</text>
<text class="ml-2">{{ card.msisdn || '-' }}</text>
</view>
<view v-if="card.virtual_no" class="mt-1 text-12px text-[var(--text-tertiary)]">
<view v-if="card.virtual_no" class="text-12px text-[#999] mt-1">
虚拟号: {{ card.virtual_no }}
</view>
<view class="mt-3 bg-[var(--bg-muted)] rounded-8px p-3">
<view v-if="card.data_total && parseFloat(card.data_total) > 0" class="mt-3 bg-[#f5f5f5] rounded-8px p-3">
<view class="flex items-center justify-between mb-2">
<text class="text-12px text-[var(--text-secondary)]">流量</text>
<text class="text-12px font-600 text-[var(--text-primary)]">
<text class="text-12px text-[#666]">流量</text>
<text class="text-12px font-600 text-[#212121]">
{{ card.data_usage || '0' }} / {{ card.data_total || '0' }}
</text>
</view>
<view class="h-6px bg-[var(--border-primary)] rounded-full overflow-hidden">
<view class="h-2 bg-[#e0e0e0] rounded-full overflow-hidden">
<view
:style="{
width: `${(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) * 100}%`,
}"
:style="{ width: `${Math.min(100, (parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) * 100)}%` }"
:class="[
'h-full rounded-full transition-all duration-300',
(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) > 0.9
? 'bg-[#ff4d4f]'
: 'bg-[#52c41a]',
'h-full rounded-full transition-all',
(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) > 0.9 ? 'bg-[#ff4d4f]' : 'bg-[#52c41a]',
]"
/>
</view>
</view>
<view class="mt-2 flex items-center justify-between text-11px text-[var(--text-tertiary)]">
<view class="mt-2 flex items-center justify-between text-11px text-[#999]">
<text>激活: {{ card.activate_time || '-' }}</text>
<text>到期: {{ card.expire_date || '-' }}</text>
</view>
</view>
</view>
<view v-else class="pt-16 center">
<text class="text-14px text-[var(--text-secondary)]">{{ searchKeyword ? '未找到匹配的卡' : '暂无卡数据' }}</text>
<view v-if="loading" class="py-4 text-center">
<text class="text-sm text-[#64748b]">加载中...</text>
</view>
<view v-else-if="!hasMore" class="py-4 text-center">
<text class="text-sm text-[#94a3b8]">没有更多了</text>
</view>
</view>
<view class="h-16" />
<view v-else class="pt-16 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>
<SimplePicker
@@ -396,38 +364,15 @@ function onCarrierConfirm(option: { label: string, value: any }) {
title="选择运营商"
@confirm="onCarrierConfirm"
/>
<SimplePicker
v-model:show="showSearchTypePicker"
:options="searchTypeOptions"
title="选择搜索类型"
@confirm="onSearchTypeConfirm"
/>
</view>
</template>
<style scoped>
.center {
display: flex;
align-items: center;
justify-content: center;
}
.space-y-3 > view:not(:last-child) {
margin-bottom: 12px;
}
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
.break-all {
word-break: break-all;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>