1376 lines
34 KiB
Vue
1376 lines
34 KiB
Vue
<template>
|
||
<div class="single-card-page">
|
||
<!-- 资产查询区域 -->
|
||
<AssetSearchCard
|
||
ref="assetSearchCardRef"
|
||
:has-card-info="!!cardInfo"
|
||
@search="handleSearch"
|
||
@refresh="handleRefresh"
|
||
/>
|
||
|
||
<!-- 卡片内容区域 -->
|
||
<div v-if="cardInfo" class="card-content-area slide-in">
|
||
<div class="main-content-layout">
|
||
<!-- 第一行:基本信息 -->
|
||
<div class="row full-width">
|
||
<BasicInfoCard
|
||
:card-info="cardInfo"
|
||
:device-realtime="deviceRealtime"
|
||
v-model:polling-enabled="pollingEnabled"
|
||
:realtime-loading="realtimeLoading"
|
||
@enable-card="handleEnableCard"
|
||
@disable-card="handleDisableCard"
|
||
@manual-deactivate="handleManualDeactivate"
|
||
@reboot-device="handleRebootDevice"
|
||
@reset-device="handleResetDevice"
|
||
@show-switch-card="showSwitchCardDialog"
|
||
@show-switch-mode="showSwitchModeDialog"
|
||
@show-set-wifi="showSetWiFiDialog"
|
||
@show-realname-policy="showRealnamePolicyDialog"
|
||
@show-update-realname-status="showUpdateRealnameStatusDialog"
|
||
@enable-binding-card="handleEnableBindingCard"
|
||
@disable-binding-card="handleDisableBindingCard"
|
||
@update-binding-card-realname-status="handleUpdateBindingCardRealnameStatus"
|
||
@navigate-to-card="handleNavigateToCard"
|
||
@navigate-to-device="handleNavigateToDevice"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 第二行:当前生效套餐 -->
|
||
<div class="row full-width">
|
||
<CurrentPackageCard
|
||
:current-package="currentPackage"
|
||
:loading="currentPackageLoading"
|
||
:error-msg="currentPackageErrorMsg"
|
||
:bound-device-id="cardInfo.bound_device_id"
|
||
:bound-device-no="cardInfo.bound_device_no"
|
||
:bound-device-name="cardInfo.bound_device_name"
|
||
@show-recharge="showRechargeDialog"
|
||
@navigate-to-device="handleNavigateToDevice"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 第三行:套餐列表 -->
|
||
<div class="row full-width">
|
||
<PackageListCard
|
||
:package-list="packageList"
|
||
:total="packagePagination.total"
|
||
:page="packagePagination.page"
|
||
:page-size="packagePagination.pageSize"
|
||
:bound-device-id="cardInfo.bound_device_id"
|
||
:bound-device-no="cardInfo.bound_device_no"
|
||
:loading="packageLoading"
|
||
@show-daily-records="handleShowDailyRecords"
|
||
@show-recharge="showRechargeDialog"
|
||
@page-change="handlePackagePageChange"
|
||
@size-change="handlePackageSizeChange"
|
||
@navigate-to-device="handleNavigateToDevice"
|
||
@navigate-to-order="handleNavigateToOrder"
|
||
@navigate-to-refund="handleNavigateToRefund"
|
||
@refresh="handlePackageRefresh"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 第四行:钱包(概况+流水) -->
|
||
<div class="row full-width">
|
||
<WalletTransactionCard
|
||
:asset-identifier="cardInfo.identifier"
|
||
:wallet-info="walletInfo"
|
||
:wallet-loading="walletLoading"
|
||
:bound-device-id="cardInfo.bound_device_id"
|
||
:bound-device-no="cardInfo.bound_device_no"
|
||
@navigate-to-device="handleNavigateToDevice"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<ElCard v-else shadow="never" class="empty-state-card">
|
||
<div class="empty-state">
|
||
<ElEmpty description="请输入资产标识符进行查询" />
|
||
</div>
|
||
</ElCard>
|
||
|
||
<!-- 对话框组件 -->
|
||
<SwitchCardDialog
|
||
v-model="switchCardDialogVisible"
|
||
:device-info="cardInfo?.virtual_no || ''"
|
||
:cards="cardInfo?.cards || []"
|
||
@confirm="handleConfirmSwitchCard"
|
||
/>
|
||
<SwitchModeDialog
|
||
v-model="switchModeDialogVisible"
|
||
:device-identifier="cardInfo?.virtual_no || ''"
|
||
:current-mode="cardInfo?.switch_mode as number"
|
||
@confirm="handleConfirmSwitchMode"
|
||
/>
|
||
<WiFiConfigDialog
|
||
v-model="setWiFiDialogVisible"
|
||
:card-no="deviceRealtime?.current_iccid"
|
||
:wifi-enabled="deviceRealtime?.wifi_enabled ? 1 : 0"
|
||
:wifi-ssid="deviceRealtime?.ssid || ''"
|
||
:wifi-password="deviceRealtime?.wifi_password || ''"
|
||
@confirm="handleConfirmSetWiFi"
|
||
/>
|
||
<PackageRechargeDialog
|
||
v-model="rechargeDialogVisible"
|
||
:asset-identifier="cardInfo?.identifier"
|
||
:asset-type="cardInfo?.asset_type"
|
||
:series-id="cardInfo?.series_id"
|
||
@success="handleRechargeSuccess"
|
||
/>
|
||
<DailyRecordsDialog
|
||
v-model="dailyRecordsDialogVisible"
|
||
:package-usage-id="selectedPackageUsageId"
|
||
/>
|
||
<OrderHistoryDialog
|
||
v-model="orderHistoryDialogVisible"
|
||
:asset-identifier="cardInfo?.identifier"
|
||
:asset-type="cardInfo?.asset_type"
|
||
/>
|
||
<RealnamePolicyDialog
|
||
v-model="realnamePolicyDialogVisible"
|
||
:asset-identifier="cardInfo?.identifier || ''"
|
||
:current-policy="cardInfo?.realname_policy"
|
||
@confirm="handleConfirmRealnamePolicy"
|
||
/>
|
||
<UpdateRealnameStatusDialog
|
||
v-model="updateRealnameStatusDialogVisible"
|
||
:asset-identifier="cardInfo?.identifier || ''"
|
||
:current-realname-status="cardInfo?.real_name_status"
|
||
@success="handleUpdateRealnameStatusSuccess"
|
||
/>
|
||
<UpdateRealnameStatusDialog
|
||
v-model="bindingCardRealnameStatusDialogVisible"
|
||
:asset-identifier="bindingCardRealnameStatusIccid"
|
||
:current-realname-status="bindingCardRealnameStatusValue"
|
||
@success="handleBindingCardRealnameStatusSuccess"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, ElCard, ElEmpty } from 'element-plus'
|
||
import { RoutesAlias } from '@/router/routesAlias'
|
||
|
||
// 引入子组件
|
||
import AssetSearchCard from './components/AssetSearchCard.vue'
|
||
import BasicInfoCard from './components/BasicInfoCard.vue'
|
||
import CurrentPackageCard from './components/CurrentPackageCard.vue'
|
||
import PackageListCard from './components/PackageListCard.vue'
|
||
import WalletTransactionCard from './components/WalletTransactionCard.vue'
|
||
|
||
// 引入对话框组件
|
||
import {
|
||
SwitchModeDialog,
|
||
WiFiConfigDialog,
|
||
PackageRechargeDialog,
|
||
DailyRecordsDialog,
|
||
OrderHistoryDialog
|
||
} from './components/dialogs'
|
||
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
|
||
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
||
import UpdateRealnameStatusDialog from '@/components/business/UpdateRealnameStatusDialog.vue'
|
||
|
||
// 引入 composables
|
||
import { useAssetInfo } from './composables/useAssetInfo'
|
||
import { useAssetOperations } from './composables/useAssetOperations'
|
||
|
||
// 引入类型
|
||
import type { SwitchCardForm, WiFiForm } from './types'
|
||
|
||
defineOptions({ name: 'SingleCard' })
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
// ========== 组件引用 ==========
|
||
const assetSearchCardRef = ref<InstanceType<typeof AssetSearchCard>>()
|
||
|
||
// ========== 核心数据状态(使用 composable) ==========
|
||
const {
|
||
cardInfo,
|
||
deviceRealtime,
|
||
currentPackage,
|
||
currentPackageLoading,
|
||
currentPackageErrorMsg,
|
||
packageList,
|
||
packageLoading,
|
||
walletInfo,
|
||
walletLoading,
|
||
realtimeLoading,
|
||
fetchAssetDetail,
|
||
loadPackageList,
|
||
loadCurrentPackage,
|
||
loadRealtimeStatus,
|
||
loadAssetWallet,
|
||
refreshAsset
|
||
} = useAssetInfo()
|
||
|
||
// ========== 资产操作方法(使用 composable) ==========
|
||
const {
|
||
enableCard: enableCardOp,
|
||
disableCard: disableCardOp,
|
||
manualDeactivateCard: manualDeactivateCardOp,
|
||
rebootDevice: rebootDeviceOp,
|
||
resetDevice: resetDeviceOp,
|
||
switchCard: switchCardOp,
|
||
setWiFi: setWiFiOp,
|
||
updatePollingStatus
|
||
} = useAssetOperations(cardInfo, async () => {
|
||
if (cardInfo.value) {
|
||
await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||
}
|
||
})
|
||
|
||
// ========== 对话框状态 ==========
|
||
const switchCardDialogVisible = ref(false)
|
||
const switchModeDialogVisible = ref(false)
|
||
const setWiFiDialogVisible = ref(false)
|
||
const rechargeDialogVisible = ref(false)
|
||
const dailyRecordsDialogVisible = ref(false)
|
||
const orderHistoryDialogVisible = ref(false)
|
||
const realnamePolicyDialogVisible = ref(false)
|
||
const updateRealnameStatusDialogVisible = ref(false)
|
||
const selectedPackageUsageId = ref<number | null>(null)
|
||
|
||
// 绑定卡实名状态更新
|
||
const bindingCardRealnameStatusDialogVisible = ref(false)
|
||
const bindingCardRealnameStatusIccid = ref('')
|
||
const bindingCardRealnameStatusValue = ref<number>(0)
|
||
|
||
// ========== 套餐列表分页状态 ==========
|
||
const packagePagination = ref({
|
||
page: 1,
|
||
pageSize: 5,
|
||
total: 0
|
||
})
|
||
|
||
// ========== 轮询状态 ==========
|
||
const pollingEnabled = ref(false)
|
||
let pollingInitializing = false
|
||
watch(pollingEnabled, async (val) => {
|
||
if (pollingInitializing) return
|
||
await updatePollingStatus(val)
|
||
})
|
||
|
||
// ========== 事件处理 ==========
|
||
|
||
/**
|
||
* 处理资产搜索
|
||
*/
|
||
const handleSearch = async ({ identifier }: { identifier: string }) => {
|
||
// 更新搜索框的值
|
||
assetSearchCardRef.value?.updateSearchValue(identifier)
|
||
|
||
// fetchAssetDetail 内部已经会并行加载所有相关数据,不需要重复调用
|
||
await fetchAssetDetail(identifier)
|
||
|
||
// 如果是设备类型,需要再次调用搜索接口获取完整数据
|
||
if (cardInfo.value?.asset_type === 'device') {
|
||
const deviceIdentifier = cardInfo.value.virtual_no || cardInfo.value.iccid || identifier
|
||
await fetchAssetDetail(deviceIdentifier)
|
||
}
|
||
|
||
// 从接口数据初始化轮询开关,不触发更新接口
|
||
pollingInitializing = true
|
||
pollingEnabled.value = cardInfo.value?.enable_polling ?? false
|
||
await nextTick()
|
||
pollingInitializing = false
|
||
|
||
// 重置分页并加载套餐列表
|
||
packagePagination.value.page = 1
|
||
const result = await loadPackageList(identifier, 1, packagePagination.value.pageSize)
|
||
packagePagination.value.total = result.total
|
||
}
|
||
|
||
/**
|
||
* 处理刷新
|
||
*/
|
||
const handleRefresh = async () => {
|
||
if (!cardInfo.value) return
|
||
await refreshAsset(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||
}
|
||
|
||
/**
|
||
* IoT卡操作 - 启用卡
|
||
*/
|
||
const handleEnableCard = async () => {
|
||
await enableCardOp()
|
||
}
|
||
|
||
/**
|
||
* IoT卡操作 - 停用卡
|
||
*/
|
||
const handleDisableCard = async () => {
|
||
await disableCardOp()
|
||
}
|
||
|
||
/**
|
||
* IoT卡操作 - 手动停用
|
||
*/
|
||
const handleManualDeactivate = async () => {
|
||
await manualDeactivateCardOp()
|
||
}
|
||
|
||
/**
|
||
* 设备操作 - 重启设备
|
||
*/
|
||
const handleRebootDevice = async () => {
|
||
await rebootDeviceOp()
|
||
}
|
||
|
||
/**
|
||
* 设备操作 - 恢复出厂
|
||
*/
|
||
const handleResetDevice = async () => {
|
||
await resetDeviceOp()
|
||
}
|
||
|
||
/**
|
||
* 显示切卡对话框
|
||
*/
|
||
const showSwitchCardDialog = () => {
|
||
switchCardDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 确认切卡
|
||
*/
|
||
const handleConfirmSwitchCard = async (form: SwitchCardForm) => {
|
||
await switchCardOp(form)
|
||
switchCardDialogVisible.value = false
|
||
}
|
||
|
||
/**
|
||
* 显示切换模式对话框
|
||
*/
|
||
const showSwitchModeDialog = () => {
|
||
switchModeDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 确认切换模式
|
||
*/
|
||
const handleConfirmSwitchMode = async (form: { switch_mode: number }) => {
|
||
try {
|
||
const { DeviceService } = await import('@/api/modules')
|
||
const identifier = cardInfo.value?.virtual_no || ''
|
||
const res = await DeviceService.setSwitchMode(identifier, form.switch_mode)
|
||
if (res.code === 0) {
|
||
ElMessage.success('设置切卡模式成功')
|
||
switchModeDialogVisible.value = false
|
||
await refreshAsset(cardInfo.value!.identifier, cardInfo.value!.asset_type)
|
||
} else {
|
||
ElMessage.error(res.msg || '设置切卡模式失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('设置切卡模式失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示WiFi配置对话框
|
||
*/
|
||
const showSetWiFiDialog = () => {
|
||
setWiFiDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 确认WiFi配置
|
||
*/
|
||
const handleConfirmSetWiFi = async (form: WiFiForm) => {
|
||
await setWiFiOp(form)
|
||
setWiFiDialogVisible.value = false
|
||
}
|
||
|
||
/**
|
||
* 显示实名认证策略对话框
|
||
*/
|
||
const showRealnamePolicyDialog = () => {
|
||
realnamePolicyDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 显示更新实名状态对话框
|
||
*/
|
||
const showUpdateRealnameStatusDialog = () => {
|
||
updateRealnameStatusDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 更新实名状态成功
|
||
*/
|
||
const handleUpdateRealnameStatusSuccess = async () => {
|
||
await refreshAsset(cardInfo.value!.identifier, cardInfo.value!.asset_type)
|
||
}
|
||
|
||
/**
|
||
* 显示绑定卡更新实名状态对话框
|
||
*/
|
||
const handleUpdateBindingCardRealnameStatus = (payload: { card: any }) => {
|
||
bindingCardRealnameStatusIccid.value = payload.card.iccid
|
||
bindingCardRealnameStatusValue.value = payload.card.real_name_status ?? 0
|
||
bindingCardRealnameStatusDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 绑定卡更新实名状态成功
|
||
*/
|
||
const handleBindingCardRealnameStatusSuccess = async () => {
|
||
await refreshAsset(cardInfo.value!.identifier, cardInfo.value!.asset_type)
|
||
}
|
||
|
||
/**
|
||
* 确认实名认证策略
|
||
*/
|
||
const handleConfirmRealnamePolicy = async (form: { realname_policy: string }) => {
|
||
try {
|
||
const { AssetService } = await import('@/api/modules')
|
||
const identifier = cardInfo.value?.identifier || ''
|
||
const res = await AssetService.updateRealnamePolicy(identifier, form.realname_policy)
|
||
if (res.code === 0) {
|
||
ElMessage.success('设置实名认证策略成功')
|
||
realnamePolicyDialogVisible.value = false
|
||
await refreshAsset(cardInfo.value!.identifier, cardInfo.value!.asset_type)
|
||
} else {
|
||
ElMessage.error(res.msg || '设置实名认证策略失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('设置实名认证策略失败:', error)
|
||
ElMessage.error('设置实名认证策略失败')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 绑定卡操作 - 启用绑定卡
|
||
*/
|
||
const handleEnableBindingCard = async (payload: { card: any }) => {
|
||
const { card } = payload
|
||
try {
|
||
const { AssetService } = await import('@/api/modules')
|
||
await AssetService.startAsset(card.iccid)
|
||
ElMessage.success('启用成功')
|
||
if (cardInfo.value)
|
||
await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||
} catch (error: any) {
|
||
console.error('启用绑定卡失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 绑定卡操作 - 停用绑定卡
|
||
*/
|
||
const handleDisableBindingCard = async (payload: { card: any }) => {
|
||
const { card } = payload
|
||
try {
|
||
const { AssetService } = await import('@/api/modules')
|
||
await AssetService.stopAsset(card.iccid)
|
||
ElMessage.success('停用成功')
|
||
if (cardInfo.value)
|
||
await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||
} catch (error: any) {
|
||
console.error('停用绑定卡失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示套餐充值对话框
|
||
*/
|
||
const showRechargeDialog = () => {
|
||
rechargeDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 充值成功回调
|
||
*/
|
||
const handleRechargeSuccess = async () => {
|
||
rechargeDialogVisible.value = false
|
||
if (cardInfo.value) {
|
||
// 重置分页并重新加载
|
||
packagePagination.value.page = 1
|
||
const result = await loadPackageList(
|
||
cardInfo.value.identifier,
|
||
packagePagination.value.page,
|
||
packagePagination.value.pageSize
|
||
)
|
||
packagePagination.value.total = result.total
|
||
await Promise.all([
|
||
loadCurrentPackage(cardInfo.value.identifier),
|
||
loadAssetWallet(cardInfo.value.identifier)
|
||
])
|
||
}
|
||
ElMessage.success('充值成功')
|
||
}
|
||
|
||
/**
|
||
* 套餐列表页码变化
|
||
*/
|
||
const handlePackagePageChange = async (page: number) => {
|
||
if (!cardInfo.value) return
|
||
packagePagination.value.page = page
|
||
const result = await loadPackageList(
|
||
cardInfo.value.identifier,
|
||
page,
|
||
packagePagination.value.pageSize
|
||
)
|
||
packagePagination.value.total = result.total
|
||
}
|
||
|
||
/**
|
||
* 套餐列表每页条数变化
|
||
*/
|
||
const handlePackageSizeChange = async (size: number) => {
|
||
if (!cardInfo.value) return
|
||
packagePagination.value.pageSize = size
|
||
packagePagination.value.page = 1 // 重置到第一页
|
||
const result = await loadPackageList(cardInfo.value.identifier, 1, size)
|
||
packagePagination.value.total = result.total
|
||
}
|
||
|
||
/**
|
||
* 套餐列表刷新
|
||
*/
|
||
const handlePackageRefresh = async () => {
|
||
if (!cardInfo.value) return
|
||
const result = await loadPackageList(
|
||
cardInfo.value.identifier,
|
||
packagePagination.value.page,
|
||
packagePagination.value.pageSize
|
||
)
|
||
packagePagination.value.total = result.total
|
||
}
|
||
|
||
/**
|
||
* 显示流量详单
|
||
*/
|
||
const handleShowDailyRecords = (payload: { packageRow: any }) => {
|
||
const { packageRow } = payload
|
||
selectedPackageUsageId.value = packageRow.package_usage_id
|
||
dailyRecordsDialogVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 跳转到绑定卡信息
|
||
*/
|
||
const handleNavigateToCard = async (iccid: string) => {
|
||
await handleSearch({ identifier: iccid })
|
||
}
|
||
|
||
/**
|
||
* 跳转到绑定设备信息
|
||
*/
|
||
const handleNavigateToDevice = async (deviceNo: string) => {
|
||
await handleSearch({ identifier: deviceNo })
|
||
}
|
||
|
||
/**
|
||
* 跳转到订单详情
|
||
*/
|
||
const handleNavigateToOrder = (orderId: number) => {
|
||
router.push(`${RoutesAlias.OrderDetail}/${orderId}`)
|
||
}
|
||
|
||
/**
|
||
* 跳转到退款详情
|
||
*/
|
||
const handleNavigateToRefund = (refundId: number) => {
|
||
router.push(`${RoutesAlias.RefundDetail}/${refundId}`)
|
||
}
|
||
|
||
/**
|
||
* URL参数自动加载
|
||
*/
|
||
onMounted(() => {
|
||
autoLoadFromQuery()
|
||
})
|
||
|
||
/**
|
||
* 监听路由查询参数变化
|
||
*/
|
||
watch(
|
||
() => route.query,
|
||
() => {
|
||
autoLoadFromQuery()
|
||
}
|
||
)
|
||
|
||
/**
|
||
* 从URL参数自动加载资产
|
||
*/
|
||
const autoLoadFromQuery = () => {
|
||
const iccid = route.query.iccid as string
|
||
const virtualNo = route.query.virtual_no as string
|
||
const deviceNo = route.query.device_no as string
|
||
|
||
const identifier = iccid || virtualNo || deviceNo
|
||
if (identifier) {
|
||
handleSearch({ identifier })
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// 套餐名称链接样式
|
||
.package-name-link {
|
||
text-decoration: underline;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.single-card-page {
|
||
padding: 20px;
|
||
|
||
// 资产查询卡片
|
||
.search-card {
|
||
margin-bottom: 20px;
|
||
overflow: visible;
|
||
background: var(--el-bg-color, #fff);
|
||
|
||
:deep(.el-card__header) {
|
||
padding: 16px 20px;
|
||
background: var(--el-fill-color-light);
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
:deep(.el-card__body) {
|
||
padding: 16px 20px;
|
||
overflow: visible;
|
||
}
|
||
|
||
.iccid-search {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
overflow: visible;
|
||
|
||
.iccid-input-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
|
||
// 放大显示区域
|
||
.iccid-magnifier {
|
||
position: absolute;
|
||
top: calc(100% + 12px);
|
||
left: 0;
|
||
z-index: 1000;
|
||
white-space: nowrap;
|
||
|
||
// 三角箭头 - 上边框样式
|
||
.magnifier-arrow {
|
||
position: absolute;
|
||
top: -9px;
|
||
left: 80px;
|
||
width: 16px;
|
||
height: 16px;
|
||
background-color: var(--el-bg-color, #fff);
|
||
border-top: 2px solid var(--el-color-primary);
|
||
border-left: 2px solid var(--el-color-primary);
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
// 内容区域
|
||
.magnifier-content {
|
||
display: inline-block;
|
||
padding: 16px 24px;
|
||
font-family: 'Courier New', Courier, monospace;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
color: var(--el-color-primary);
|
||
text-align: center;
|
||
letter-spacing: 2px;
|
||
white-space: nowrap;
|
||
background-color: var(--el-bg-color, #fff);
|
||
border: 2px solid var(--el-color-primary);
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 操作按钮组
|
||
.operation-button-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-left: auto;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 内容区域
|
||
.card-content-area,
|
||
.empty-state {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
// 移动端
|
||
@media (width <= 768px) {
|
||
padding: 16px;
|
||
|
||
.search-card {
|
||
top: 16px;
|
||
margin-bottom: 16px;
|
||
|
||
:deep(.el-card__header) {
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
:deep(.el-card__body) {
|
||
padding: 12px 16px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 放大效果的过渡动画
|
||
.zoom-fade-enter-active,
|
||
.zoom-fade-leave-active {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.zoom-fade-enter-from {
|
||
opacity: 0;
|
||
transform: scale(0.8) translateY(-10px);
|
||
}
|
||
|
||
.zoom-fade-leave-to {
|
||
opacity: 0;
|
||
transform: scale(0.8) translateY(-10px);
|
||
}
|
||
|
||
.zoom-fade-enter-to,
|
||
.zoom-fade-leave-from {
|
||
opacity: 1;
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
|
||
// 卡片内容区域
|
||
.card-content-area {
|
||
&.slide-in {
|
||
animation: slideInUp 0.6s ease-out;
|
||
}
|
||
}
|
||
|
||
// 主内容布局
|
||
.main-content-layout {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
|
||
// 行布局
|
||
.row {
|
||
display: flex;
|
||
gap: 24px;
|
||
|
||
&.full-width {
|
||
.info-card {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
&.two-columns {
|
||
.col {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-width: 0; // 防止被内容撑开
|
||
|
||
.info-card {
|
||
height: 100%;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
&:first-child {
|
||
flex: 2;
|
||
}
|
||
|
||
&:last-child {
|
||
flex: 8;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 响应式调整
|
||
@media (width <= 1200px) {
|
||
.row.two-columns {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
|
||
.col {
|
||
.info-card {
|
||
height: auto;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 钱包和套餐两栏在小屏幕也改为垂直布局
|
||
.wallet-package-row {
|
||
flex-direction: column;
|
||
|
||
.wallet-col,
|
||
.package-col {
|
||
flex: 1;
|
||
min-width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
@media (width <= 768px) {
|
||
gap: 16px;
|
||
|
||
.row {
|
||
gap: 16px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 信息卡片通用样式
|
||
.info-card {
|
||
width: 100%;
|
||
|
||
:deep(.el-card__header) {
|
||
padding: 20px 24px 16px;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary, #1f2937);
|
||
|
||
.card-header-left {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.card-header-right {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
|
||
.polling-switch-wrapper {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
|
||
.polling-label {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
:deep(.el-switch) {
|
||
height: 26px;
|
||
|
||
.el-switch__core {
|
||
height: 26px;
|
||
min-width: 50px;
|
||
|
||
.el-switch__inner {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.el-switch__action {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.header-title {
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.package-series-value {
|
||
font-weight: 500;
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
i {
|
||
font-size: 18px;
|
||
color: #667eea;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.el-card__body) {
|
||
padding: 24px;
|
||
}
|
||
}
|
||
|
||
// 当前套餐卡片
|
||
.current-package-card {
|
||
.package-actions {
|
||
margin-top: 16px;
|
||
text-align: right;
|
||
}
|
||
|
||
.traffic-progress-wrapper {
|
||
padding: 20px;
|
||
margin-top: 24px;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 8px;
|
||
|
||
.progress-info {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
|
||
.progress-label {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
.progress-percentage {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
|
||
.flow-stats {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 12px;
|
||
text-align: center;
|
||
background: var(--el-bg-color, #fff);
|
||
border-radius: 6px;
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
|
||
&.used {
|
||
color: var(--el-color-warning);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.el-progress) {
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 流量信息卡片
|
||
.traffic-info {
|
||
// 流量概览
|
||
.traffic-overview {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
|
||
// 流量进度条区域
|
||
.traffic-progress-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.progress-bar-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
.progress-info {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.progress-label {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
.progress-percentage {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
|
||
:deep(.el-progress) {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.progress-stats {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: space-between;
|
||
margin-top: 8px;
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 12px;
|
||
text-align: center;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 6px;
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
|
||
&.used {
|
||
color: var(--el-color-warning);
|
||
}
|
||
|
||
&.remaining {
|
||
color: var(--el-color-success);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 额外流量信息
|
||
.extra-traffic-info {
|
||
display: flex;
|
||
gap: 16px;
|
||
|
||
.info-item {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 12px;
|
||
text-align: center;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 8px;
|
||
|
||
.label {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 钱包概况和套餐列表两栏布局
|
||
.wallet-package-row {
|
||
align-items: stretch;
|
||
|
||
.wallet-col {
|
||
flex: 0 0 380px;
|
||
min-width: 320px;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.package-col {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.info-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
|
||
:deep(.el-card__body) {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 套餐信息卡片
|
||
.package-info {
|
||
.package-table-wrapper {
|
||
flex: 1;
|
||
overflow: auto;
|
||
|
||
.package-table {
|
||
:deep(.el-table__header) {
|
||
th {
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary, #374151);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 钱包概况卡片
|
||
.wallet-overview-card {
|
||
.wallet-balance-cards {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.balance-card {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
padding: 10px 14px;
|
||
background: var(--el-fill-color-light);
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 4px;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
border-color: var(--el-color-primary);
|
||
box-shadow: 0 2px 6px rgb(0 0 0 / 6%);
|
||
transform: translateX(2px);
|
||
}
|
||
|
||
.balance-icon {
|
||
flex-shrink: 0;
|
||
font-size: 20px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.balance-info {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
|
||
.balance-label {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.balance-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
|
||
&.total {
|
||
.balance-value {
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
&.available {
|
||
.balance-value {
|
||
color: var(--el-color-success);
|
||
}
|
||
}
|
||
|
||
&.frozen {
|
||
.balance-value {
|
||
color: var(--el-color-warning);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 钱包流水卡片
|
||
.wallet-info {
|
||
.wallet-header {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-section {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.wallet-table-wrapper {
|
||
.wallet-table {
|
||
:deep(.el-table__header) {
|
||
th {
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary, #374151);
|
||
}
|
||
}
|
||
|
||
.reference-no-link {
|
||
color: var(--el-color-primary);
|
||
text-decoration: underline;
|
||
text-decoration-style: dashed;
|
||
text-underline-offset: 2px;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
color: var(--el-color-primary-light-3);
|
||
text-decoration-style: solid;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载和空状态
|
||
.loading-state,
|
||
.empty-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 24px auto;
|
||
background: var(--el-bg-color, #fff);
|
||
border-radius: 16px;
|
||
}
|
||
|
||
// 响应式设计
|
||
@media (width <= 768px) {
|
||
padding: 16px;
|
||
|
||
.main-content-layout {
|
||
gap: 16px;
|
||
|
||
.row {
|
||
gap: 16px;
|
||
|
||
&.two-columns {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-card {
|
||
:deep(.el-card__body) {
|
||
padding: 16px;
|
||
}
|
||
|
||
:deep(.el-card__header) {
|
||
padding: 16px 16px 12px;
|
||
}
|
||
}
|
||
|
||
.traffic-info .traffic-overview {
|
||
.progress-stats {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.extra-traffic-info {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
//.operation-card .operations-grid .operation-group .operation-buttons {
|
||
// grid-template-columns: 1fr;
|
||
//}
|
||
}
|
||
|
||
@media (width <= 480px) {
|
||
.traffic-info .traffic-overview {
|
||
.progress-percentage {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.progress-stats .stat-item {
|
||
padding: 10px;
|
||
|
||
.stat-value {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 动画效果
|
||
@keyframes slideInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(30px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
// 加载动画
|
||
.loading-state {
|
||
:deep(.el-skeleton) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
// 深色模式特定样式
|
||
html.dark & {
|
||
background: var(--el-bg-color-page);
|
||
|
||
.traffic-details .info-item {
|
||
background: var(--el-fill-color);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 往期订单样式
|
||
.generation-section {
|
||
padding: 16px;
|
||
background: var(--el-fill-color-lighter);
|
||
border-radius: 8px;
|
||
|
||
h3 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
</style>
|