This commit is contained in:
@@ -47,7 +47,8 @@ export interface AssetResolveResponse {
|
||||
cards?: Array<{
|
||||
card_id: number
|
||||
iccid: string
|
||||
msisdn: string
|
||||
msisdn?: string | null
|
||||
carrier_name?: string | null
|
||||
network_status: number
|
||||
real_name_status: number
|
||||
slot_position: number
|
||||
@@ -74,6 +75,7 @@ export interface CardInfo {
|
||||
msisdn?: string // 电话号码
|
||||
virtual_no?: string // 虚拟号
|
||||
virtual_number?: string // 虚拟号 (别名兼容)
|
||||
device_virtual_no?: string | null
|
||||
imei?: string
|
||||
carrier_id?: number // 运营商ID
|
||||
carrier_name?: string // 运营商名称
|
||||
@@ -246,7 +248,8 @@ export interface AssetRealtimeStatus {
|
||||
card_id: number
|
||||
iccid: string
|
||||
is_current: boolean
|
||||
msisdn: string
|
||||
msisdn?: string | null
|
||||
carrier_name?: string | null
|
||||
network_status: number
|
||||
real_name_status: number
|
||||
slot_position: number
|
||||
|
||||
16
src/api/shop.ts
Normal file
16
src/api/shop.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
export interface ShopCascadeParams {
|
||||
parent_id?: number
|
||||
shop_name?: string
|
||||
}
|
||||
|
||||
export interface ShopCascadeItem {
|
||||
id: number
|
||||
shop_name: string
|
||||
has_children: boolean
|
||||
}
|
||||
|
||||
export function getShopCascade(params?: ShopCascadeParams) {
|
||||
return get<ShopCascadeItem[]>('/api/admin/shops/cascade', { params })
|
||||
}
|
||||
345
src/components/ShopCascadePicker.vue
Normal file
345
src/components/ShopCascadePicker.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { getShopCascade } from '@/api/shop'
|
||||
import type { ShopCascadeItem } from '@/api/shop'
|
||||
|
||||
interface SelectedShop {
|
||||
id: number
|
||||
shop_name: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
selectedShop?: SelectedShop | null
|
||||
title?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:show', value: boolean): void
|
||||
(e: 'confirm', shop: SelectedShop | null): void
|
||||
(e: 'cancel'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '选择店铺',
|
||||
selectedShop: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const loading = ref(false)
|
||||
const shopOptions = ref<ShopCascadeItem[]>([])
|
||||
const breadcrumbs = ref<ShopCascadeItem[]>([])
|
||||
const selectedShop = ref<SelectedShop | null>(null)
|
||||
|
||||
async function loadShopOptions(parentId?: number) {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getShopCascade(parentId !== undefined ? { parent_id: parentId } : undefined)
|
||||
shopOptions.value = response || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载店铺级联数据失败:', error)
|
||||
shopOptions.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openPicker() {
|
||||
selectedShop.value = props.selectedShop ? { ...props.selectedShop } : null
|
||||
breadcrumbs.value = []
|
||||
await loadShopOptions()
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel')
|
||||
close()
|
||||
}
|
||||
|
||||
function selectShop(option: ShopCascadeItem) {
|
||||
selectedShop.value = {
|
||||
id: option.id,
|
||||
shop_name: option.shop_name,
|
||||
}
|
||||
}
|
||||
|
||||
async function enterChildren(option: ShopCascadeItem) {
|
||||
const nextPath = [...breadcrumbs.value, option]
|
||||
breadcrumbs.value = nextPath
|
||||
await loadShopOptions(option.id)
|
||||
}
|
||||
|
||||
async function goToBreadcrumb(index: number) {
|
||||
if (index < 0) {
|
||||
breadcrumbs.value = []
|
||||
await loadShopOptions()
|
||||
return
|
||||
}
|
||||
|
||||
const nextPath = breadcrumbs.value.slice(0, index + 1)
|
||||
breadcrumbs.value = nextPath
|
||||
await loadShopOptions(nextPath[index]?.id)
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
emit('confirm', selectedShop.value)
|
||||
close()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
async (show) => {
|
||||
if (show) {
|
||||
await openPicker()
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
v-if="show"
|
||||
class="shop-picker-overlay"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<view
|
||||
class="shop-picker-content"
|
||||
@click.stop
|
||||
>
|
||||
<view class="shop-picker-header">
|
||||
<view class="shop-picker-action" @click="handleCancel">
|
||||
<text class="text-sm text-[#666]">取消</text>
|
||||
</view>
|
||||
<view class="shop-picker-title">
|
||||
<text class="text-base font-600 text-[#1e293b]">{{ title }}</text>
|
||||
</view>
|
||||
<view class="shop-picker-action text-right" @click="handleConfirm">
|
||||
<text class="text-sm text-brand font-600">确定</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="shop-picker-breadcrumb"
|
||||
scroll-x
|
||||
>
|
||||
<view class="shop-picker-breadcrumb-list">
|
||||
<text
|
||||
class="shop-picker-breadcrumb-item"
|
||||
:class="{ 'shop-picker-breadcrumb-item-active': breadcrumbs.length === 0 }"
|
||||
@click="goToBreadcrumb(-1)"
|
||||
>
|
||||
顶级店铺
|
||||
</text>
|
||||
<template
|
||||
v-for="(item, index) in breadcrumbs"
|
||||
:key="item.id"
|
||||
>
|
||||
<text class="shop-picker-breadcrumb-separator">/</text>
|
||||
<text
|
||||
class="shop-picker-breadcrumb-item"
|
||||
:class="{ 'shop-picker-breadcrumb-item-active': index === breadcrumbs.length - 1 }"
|
||||
@click="goToBreadcrumb(index)"
|
||||
>
|
||||
{{ item.shop_name }}
|
||||
</text>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="selectedShop" class="shop-picker-selection">
|
||||
<text class="text-12px text-[#666]">
|
||||
已选店铺
|
||||
</text>
|
||||
<text class="text-13px font-600 text-[#212121]">
|
||||
{{ selectedShop.shop_name }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="shop-picker-list"
|
||||
scroll-y
|
||||
>
|
||||
<view v-if="loading" class="shop-picker-empty">
|
||||
<i class="i-mdi-loading animate-spin text-18px text-brand" />
|
||||
<text class="text-13px text-[#64748b]">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="shopOptions.length === 0" class="shop-picker-empty">
|
||||
<text class="text-13px text-[#94a3b8]">暂无下级店铺</text>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="option in shopOptions"
|
||||
:key="option.id"
|
||||
class="shop-picker-item"
|
||||
:class="{ 'shop-picker-item-active': selectedShop?.id === option.id }"
|
||||
>
|
||||
<view
|
||||
class="shop-picker-item-main"
|
||||
@click="selectShop(option)"
|
||||
>
|
||||
<text class="shop-picker-item-text">{{ option.shop_name }}</text>
|
||||
<i
|
||||
v-if="selectedShop?.id === option.id"
|
||||
class="i-mdi-check text-16px text-brand"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
v-if="option.has_children"
|
||||
class="shop-picker-next"
|
||||
@click="enterChildren(option)"
|
||||
>
|
||||
<text class="text-12px text-[#999]">下级</text>
|
||||
<i class="i-mdi-chevron-right text-14px text-[#999]" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shop-picker-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.shop-picker-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 72vh;
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.shop-picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.shop-picker-action {
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.shop-picker-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shop-picker-breadcrumb {
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #f8fafc;
|
||||
}
|
||||
|
||||
.shop-picker-breadcrumb-list {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.shop-picker-breadcrumb-item {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.shop-picker-breadcrumb-item-active {
|
||||
color: var(--brand-primary, #1677ff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.shop-picker-breadcrumb-separator {
|
||||
margin: 0 8px;
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.shop-picker-selection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.shop-picker-list {
|
||||
flex: 1;
|
||||
max-height: calc(72vh - 114px);
|
||||
}
|
||||
|
||||
.shop-picker-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.shop-picker-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-bottom: 1px solid #f8fafc;
|
||||
}
|
||||
|
||||
.shop-picker-item-active {
|
||||
background: #fef3f2;
|
||||
}
|
||||
|
||||
.shop-picker-item-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.shop-picker-item-text {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.shop-picker-item-active .shop-picker-item-text {
|
||||
color: var(--brand-primary, #1677ff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.shop-picker-next {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 16px 0 8px;
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -76,6 +76,14 @@ function getPackageTypeText(type?: AssetPackage['package_type']) {
|
||||
return type ? map[type] || type : '-'
|
||||
}
|
||||
|
||||
function formatPackageEffectivePeriod(activatedAt?: string | null, expiresAt?: string | null) {
|
||||
return `生效期:${formatTime(activatedAt || undefined, 'date')} 至 ${formatTime(expiresAt || undefined, 'date')}`
|
||||
}
|
||||
|
||||
function formatCurrentPackagePeriodRange(activatedAt?: string | null, expiresAt?: string | null) {
|
||||
return `生效期:${formatTime(activatedAt || undefined, 'date')} - ${formatTime(expiresAt || undefined, 'date')}`
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
// 从路由参数获取 assetType 和 assetIdentifier
|
||||
@@ -506,8 +514,8 @@ function viewWallet() {
|
||||
<text class="text-14px font-600 text-[#212121] block break-all">
|
||||
{{ currentPackage.package_name || '-' }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999] block mt-1 break-all">
|
||||
订单号:{{ currentPackage.order_no || '-' }}
|
||||
<text class="text-11px text-[#64748b] block mt-1 break-all">
|
||||
{{ formatCurrentPackagePeriodRange(currentPackage.activated_at, currentPackage.expires_at) }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
@@ -564,14 +572,6 @@ function viewWallet() {
|
||||
<text class="text-[#666]">套餐类型</text>
|
||||
<text class="text-[#212121]">{{ getPackageTypeText(currentPackage.package_type) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-12px">
|
||||
<text class="text-[#666]">开始时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.activated_at) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-12px">
|
||||
<text class="text-[#666]">到期时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.expires_at) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-12px">
|
||||
<text class="text-[#666]">下单时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.created_at) }}</text>
|
||||
@@ -642,9 +642,11 @@ function viewWallet() {
|
||||
<text class="text-[#666]">金额</text>
|
||||
<text class="text-[#212121]">{{ formatPackagePrice(pkg.paid_amount) }}</text>
|
||||
</view>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ pkg.activated_at || '-' }} ~ {{ pkg.expires_at || '-' }}
|
||||
</text>
|
||||
<view v-if="pkg.status === 1" class="mt-2 bg-[#f8fafc] rounded-8px px-2.5 py-2">
|
||||
<text class="text-11px font-600 text-[#475569]">
|
||||
{{ formatPackageEffectivePeriod(pkg.activated_at, pkg.expires_at) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { resolveAsset, getAssetRealtimeStatus, getCurrentPackage, getAssetPackages } from '@/api/assets'
|
||||
import type { CardInfo, DeviceInfo, AssetRealtimeStatus, AssetPackage } from '@/api/assets'
|
||||
import type { AssetRealtimeStatus, AssetPackage } from '@/api/assets'
|
||||
import { formatTime, formatDataSize } from '@/utils/format'
|
||||
import { formatPackageDataSize, formatPackagePrice, getPackageRemainMb, getPackageTotalMb, getPackageUsagePercent, getPackageUsedMb } from '../package-display'
|
||||
|
||||
@@ -122,26 +122,23 @@
|
||||
return status !== undefined ? map[status] || '未知' : '未知'
|
||||
}
|
||||
|
||||
function getRealNameStatusTagClass(status ?: number | null) {
|
||||
return status === 1
|
||||
? 'bg-[#e8f5e9] text-[#2e7d32]'
|
||||
: 'bg-[#fff1f0] text-[#cf1322]'
|
||||
}
|
||||
|
||||
// 获取网络状态文本 (卡片)
|
||||
function getNetworkStatusText(status ?: number) {
|
||||
const map : Record<number, string> = {
|
||||
0: '停机',
|
||||
1: '开机',
|
||||
1: '正常',
|
||||
}
|
||||
return status !== undefined ? map[status] || '未知' : '未知'
|
||||
}
|
||||
|
||||
// 获取切卡模式文本 (设备)
|
||||
function getSwitchModeText(mode ?: string) {
|
||||
const map : Record<string, string> = {
|
||||
'0': '自动',
|
||||
'1': '手动',
|
||||
}
|
||||
return mode ? map[mode] || mode : '自动'
|
||||
}
|
||||
|
||||
// 格式化字节数
|
||||
function formatBytes(bytes ?: string | number) {
|
||||
function formatBytes(bytes ?: string | number | null) {
|
||||
if (!bytes) return '0B'
|
||||
const num = typeof bytes === 'string' ? parseInt(bytes) : bytes
|
||||
if (num < 1024) return `${num}B`
|
||||
@@ -174,16 +171,6 @@
|
||||
return `${secs}秒`
|
||||
}
|
||||
|
||||
// 获取设备保护状态文本
|
||||
function getDeviceProtectText(status ?: string | null) {
|
||||
const map : Record<string, string> = {
|
||||
start: '已启动',
|
||||
stop: '已停止',
|
||||
none: '未启用',
|
||||
}
|
||||
return status ? map[status] || status : '未知'
|
||||
}
|
||||
|
||||
// 获取在线状态文本
|
||||
function getOnlineStatusText(status ?: number) {
|
||||
return status === 1 ? '在线' : '离线'
|
||||
@@ -209,6 +196,19 @@
|
||||
return type ? map[type] || type : '-'
|
||||
}
|
||||
|
||||
function formatPackageEffectivePeriod(activatedAt ?: string | null, expiresAt ?: string | null) {
|
||||
return `生效期:${formatTime(activatedAt || undefined, 'date')} 至 ${formatTime(expiresAt || undefined, 'date')}`
|
||||
}
|
||||
|
||||
function formatCurrentPackagePeriodRange(activatedAt ?: string | null, expiresAt ?: string | null) {
|
||||
return `生效期:${formatTime(activatedAt || undefined, 'date')} - ${formatTime(expiresAt || undefined, 'date')}`
|
||||
}
|
||||
|
||||
function getDeviceLastOnlineTime() {
|
||||
const lastOnlineTime = realtimeStatus.value?.device_realtime?.last_online_time || deviceInfo.value?.last_online_time
|
||||
return lastOnlineTime ? formatTime(lastOnlineTime) : '-'
|
||||
}
|
||||
|
||||
// 获取信号质量等级 (基于 RSRP)
|
||||
function getSignalQuality(rsrp ?: number) {
|
||||
if (rsrp === undefined) return { level: '未知', text: '无信号', color: '#999' }
|
||||
@@ -270,19 +270,9 @@
|
||||
return status !== undefined ? map[status] : { bg: '#f5f5f5', text: '#999' }
|
||||
}
|
||||
|
||||
// 获取设备保护状态文本
|
||||
function getDeviceProtectStatusText(status ?: string) {
|
||||
const map : Record<string, string> = {
|
||||
none: '无保护',
|
||||
basic: '基础保护',
|
||||
advanced: '高级保护',
|
||||
}
|
||||
return status ? map[status] || status : '无保护'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
const options = currentPage.options || {}
|
||||
|
||||
// 如果有 iccid 或 virtual_no 参数,自动搜索
|
||||
@@ -366,14 +356,6 @@
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 批次号 -->
|
||||
<view v-if="cardInfo.batch_no" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">批次号</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right">
|
||||
{{ cardInfo.batch_no }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 网络状态 -->
|
||||
<view v-if="cardInfo.network_status !== undefined" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">网络状态</text>
|
||||
@@ -512,8 +494,8 @@
|
||||
<text class="text-14px font-600 text-[#212121] block break-all">
|
||||
{{ currentPackage.package_name || '-' }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999] block mt-1 break-all">
|
||||
订单号:{{ currentPackage.order_no || '-' }}
|
||||
<text class="text-11px text-[#64748b] block mt-1 break-all">
|
||||
{{ formatCurrentPackagePeriodRange(currentPackage.activated_at, currentPackage.expires_at) }}
|
||||
</text>
|
||||
</view>
|
||||
<view :style="{
|
||||
@@ -564,14 +546,6 @@
|
||||
<text class="text-[#666]">套餐类型</text>
|
||||
<text class="text-[#212121]">{{ getPackageTypeText(currentPackage.package_type) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">开始时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.activated_at) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">到期时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.expires_at) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">下单时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.created_at) }}</text>
|
||||
@@ -912,14 +886,6 @@
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 制造商 -->
|
||||
<view v-if="deviceInfo.manufacturer" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-80px">制造商</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right">
|
||||
{{ deviceInfo.manufacturer }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 最大卡槽数 -->
|
||||
<view v-if="deviceInfo.max_sim_slots" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-80px">最大卡槽</text>
|
||||
@@ -936,14 +902,6 @@
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 批次号 -->
|
||||
<view v-if="deviceInfo.batch_no" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-80px">批次号</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right">
|
||||
{{ deviceInfo.batch_no }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 店铺信息 -->
|
||||
<view v-if="deviceInfo.shop_name" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-80px">所属店铺</text>
|
||||
@@ -960,22 +918,6 @@
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 保护状态 -->
|
||||
<view v-if="deviceInfo.device_protect_status" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-80px">保护状态</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right">
|
||||
{{ getDeviceProtectStatusText(deviceInfo.device_protect_status) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 切卡模式 -->
|
||||
<view v-if="deviceInfo.switch_mode !== undefined" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-80px">切卡模式</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right">
|
||||
{{ getSwitchModeText(deviceInfo.switch_mode) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 实名状态 -->
|
||||
<view v-if="deviceInfo.real_name_status !== undefined" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-80px">实名状态</text>
|
||||
@@ -1033,21 +975,27 @@
|
||||
<view class="space-y-2">
|
||||
<view v-for="card in deviceInfo.cards" :key="card.card_id" class="bg-[#f5f5f5] rounded-12px p-3">
|
||||
<view class="flex items-center justify-between mb-1">
|
||||
<text class="text-12px text-[#212121]">卡槽 {{ card.slot_position }}</text>
|
||||
<view class="flex items-center gap-2 min-w-0">
|
||||
<text class="text-12px text-[#212121]">卡槽 {{ card.slot_position }}</text>
|
||||
<text :class="[
|
||||
'px-2 py-0.5 rounded-full text-11px font-600',
|
||||
getRealNameStatusTagClass(card.real_name_status),
|
||||
]">
|
||||
{{ getRealNameStatusText(card.real_name_status) }}
|
||||
</text>
|
||||
</view>
|
||||
<text v-if="card.is_current" class="text-11px text-white bg-[#3ed268] px-2 py-0.5 rounded">
|
||||
当前
|
||||
</text>
|
||||
</view>
|
||||
<view class="space-y-1">
|
||||
<text class="text-11px text-[#666] block">ICCID: {{ card.iccid }}</text>
|
||||
<text class="text-11px text-[#666] block">电话: {{ card.msisdn }}</text>
|
||||
<text class="text-11px text-[#666] block">MSISDN: {{ card.msisdn || '-' }}</text>
|
||||
<text class="text-11px text-[#666] block">运营商: {{ card.carrier_name || '-' }}</text>
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-11px text-[#666]">
|
||||
网络: {{ getNetworkStatusText(card.network_status) }}
|
||||
</text>
|
||||
<text class="text-11px text-[#666]">
|
||||
实名: {{ getRealNameStatusText(card.real_name_status) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1101,6 +1049,11 @@
|
||||
<text class="text-[#212121]">{{ formatDuration(realtimeStatus.device_realtime.run_time) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">最后上线时间</text>
|
||||
<text class="text-[#212121]">{{ getDeviceLastOnlineTime() }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 信号 -->
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">信号</text>
|
||||
@@ -1125,18 +1078,6 @@
|
||||
<text class="text-[var(--brand-primary)] font-600">{{ formatBytesDisplay(realtimeStatus.device_realtime.daily_usage) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 下载流量 -->
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">下载流量</text>
|
||||
<text class="text-[#212121]">{{ formatBytesDisplay(realtimeStatus.device_realtime.dl_stats) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 上传流量 -->
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">上传流量</text>
|
||||
<text class="text-[#212121]">{{ formatBytesDisplay(realtimeStatus.device_realtime.ul_stats) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 限速 -->
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">限速</text>
|
||||
@@ -1265,8 +1206,8 @@
|
||||
<text class="text-14px font-600 text-[#212121] block break-all">
|
||||
{{ currentPackage.package_name || '-' }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999] block mt-1 break-all">
|
||||
订单号:{{ currentPackage.order_no || '-' }}
|
||||
<text class="text-11px text-[#64748b] block mt-1 break-all">
|
||||
{{ formatCurrentPackagePeriodRange(currentPackage.activated_at, currentPackage.expires_at) }}
|
||||
</text>
|
||||
</view>
|
||||
<view :style="{
|
||||
@@ -1317,14 +1258,6 @@
|
||||
<text class="text-[#666]">套餐类型</text>
|
||||
<text class="text-[#212121]">{{ getPackageTypeText(currentPackage.package_type) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">开始时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.activated_at) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">到期时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.expires_at) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#666]">下单时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(currentPackage.created_at) }}</text>
|
||||
@@ -1434,35 +1367,14 @@
|
||||
<text class="text-[#666]">金额</text>
|
||||
<text class="text-[#212121]">{{ formatPackagePrice(pkg.paid_amount) }}</text>
|
||||
</view>
|
||||
<view v-if="pkg.activated_at" class="flex items-center justify-between text-10px">
|
||||
<text class="text-[#999]">{{ formatTime(pkg.activated_at, 'date') }}</text>
|
||||
<text v-if="pkg.expires_at" class="text-[#999]">至 {{ formatTime(pkg.expires_at, 'date') }}</text>
|
||||
<view v-if="pkg.status === 1" class="mt-2 bg-white rounded-8px px-2.5 py-2">
|
||||
<text class="text-11px font-600 text-[#475569]">{{ formatPackageEffectivePeriod(pkg.activated_at, pkg.expires_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-4">
|
||||
<view class="bg-[#f5f5f5] rounded-12px p-3">
|
||||
<view class="space-y-1">
|
||||
<view v-if="deviceInfo.created_at" class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#999]">创建时间</text>
|
||||
<text class="text-[#212121]">{{ formatTime(deviceInfo.created_at) }}</text>
|
||||
</view>
|
||||
<view v-if="deviceInfo.last_online_time" class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#999]">最后在线</text>
|
||||
<text class="text-[#212121]">{{ formatTime(deviceInfo.last_online_time) }}</text>
|
||||
</view>
|
||||
<view v-if="deviceInfo.last_gateway_sync_at" class="flex items-center justify-between text-11px">
|
||||
<text class="text-[#999]">网关同步</text>
|
||||
<text class="text-[#212121]">{{ formatTime(deviceInfo.last_gateway_sync_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { UserInfo } from '@/api/auth'
|
||||
import { getFundSummary, getCommissionDailyStats } from '@/api/commission'
|
||||
import type { FundSummary, DailyCommissionStats } from '@/api/commission'
|
||||
|
||||
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
@@ -36,6 +36,10 @@
|
||||
return `¥${parts.join('.')}`
|
||||
}
|
||||
|
||||
function formatAmountDisplay(amount?: number | null) {
|
||||
return typeof amount === 'number' ? formatAmount(amount) : '--'
|
||||
}
|
||||
|
||||
// 格式化日期 (MM-DD)
|
||||
// 根据用户类型动态生成菜单
|
||||
function formatDate(dateStr : string) {
|
||||
@@ -104,7 +108,7 @@
|
||||
|
||||
// 加载佣金数据
|
||||
async function loadCommissionData(shopName?: string) {
|
||||
if (!isAgent.value || !shopId.value) return
|
||||
if (!isAgent.value) return
|
||||
|
||||
loadingCommission.value = true
|
||||
|
||||
@@ -112,6 +116,12 @@
|
||||
// 1. 加载佣金概览 (新接口 /shops/fund-summary)
|
||||
const summary = await getFundSummary(shopName ? { shop_name: shopName } : undefined)
|
||||
commissionSummary.value = summary
|
||||
|
||||
if (!shopId.value) {
|
||||
dailyStats.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const today = getTodayDate()
|
||||
const todayStats = await getCommissionDailyStats(shopId.value, {
|
||||
days: 1,
|
||||
@@ -143,11 +153,8 @@
|
||||
userInfo.value = user
|
||||
|
||||
// 设置店铺ID
|
||||
if (user.shop_id) {
|
||||
shopId.value = user.shop_id
|
||||
}
|
||||
|
||||
const loginUserInfo = getLoginUserInfo()
|
||||
shopId.value = user.shop_id || loginUserInfo?.shop_id || 0
|
||||
const fundSummaryShopName = loginUserInfo?.username || user.username
|
||||
|
||||
console.log('首页数据加载成功:', { user })
|
||||
@@ -244,33 +251,47 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 代理端: 佣金概览 -->
|
||||
<view v-if="isAgent && commissionSummary" class="px-4 pt-4">
|
||||
<text class="text-16px font-700 text-[#212121] block mb-3">可提现佣金</text>
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="grid grid-cols-4 gap-3">
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-[#999] block mb-1">累计</text>
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ formatAmount(commissionSummary.total_commission) }}
|
||||
<!-- 代理端: 佣金展示 -->
|
||||
<view v-if="isAgent" class="px-4 pt-4">
|
||||
<view
|
||||
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-5 mb-3"
|
||||
@click="navigateTo('/pages/agent-system/commission-center/index')"
|
||||
>
|
||||
<view class="flex items-center justify-between gap-3">
|
||||
<view class="min-w-0 flex-1">
|
||||
<text class="text-12px text-[#999]">可提现佣金</text>
|
||||
<text class="text-32px font-700 text-[#212121] block mt-1 break-all">
|
||||
{{ formatAmountDisplay(commissionSummary?.available_commission) }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999] block mt-2">
|
||||
当前可直接发起提现的佣金金额
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-[#999] block mb-1">冻结</text>
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ formatAmount(commissionSummary.frozen_commission) }}
|
||||
<view class="flex flex-col items-end gap-2">
|
||||
<image src="@/static/icons/佣金中心.png" class="w-12 h-12 rounded-12px" mode="aspectFit" />
|
||||
<view class="flex items-center gap-1">
|
||||
<text class="text-12px text-[#999]">佣金中心</text>
|
||||
<i class="i-mdi-chevron-right text-16px text-[#cbd5e1]" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex gap-3 mt-4 pt-4 border-t border-[#f5f5f5]">
|
||||
<view class="flex-1 text-center">
|
||||
<text class="text-11px text-[#999]">累计佣金</text>
|
||||
<text class="text-14px font-600 text-[#212121] block mt-1">
|
||||
{{ formatAmountDisplay(commissionSummary?.total_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-[#999] block mb-1">已提现</text>
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ formatAmount(commissionSummary.withdrawn_commission) }}
|
||||
<view class="flex-1 text-center border-l border-[#f5f5f5]">
|
||||
<text class="text-11px text-[#999]">已提现</text>
|
||||
<text class="text-14px font-600 text-[#52c41a] block mt-1">
|
||||
{{ formatAmountDisplay(commissionSummary?.withdrawn_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-[#999] block mb-1">可提现</text>
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ formatAmount(commissionSummary.available_commission) }}
|
||||
<view class="flex-1 text-center border-l border-[#f5f5f5]">
|
||||
<text class="text-11px text-[#999]">冻结</text>
|
||||
<text class="text-14px font-600 text-[#ff4d4f] block mt-1">
|
||||
{{ formatAmountDisplay(commissionSummary?.frozen_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -280,7 +301,7 @@
|
||||
<view>
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<text class="text-16px font-700 text-[#212121]">今日佣金</text>
|
||||
<text class="text-13px text-[#999]" @click="navigateTo('/pages/agent-system/commission-center/index')">更多</text>
|
||||
<text class="text-13px text-[#999]" @click="navigateTo('/pages/agent-system/commission-center/index')">明细</text>
|
||||
</view>
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4">
|
||||
<view class="grid grid-cols-3 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user