Files
one-pipe-system/src/views/asset-management/asset-information/index.vue
sexygoat 17c299e4ce
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m11s
fix: bug
2026-04-24 18:39:39 +08:00

1376 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>