947 lines
32 KiB
Vue
947 lines
32 KiB
Vue
<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>
|