Files
one-pipe-system/src/views/asset-management/asset-information/components/CurrentPackageCard.vue
luo f52970ae6c
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m22s
fix: 资产信息流量显示
2026-07-21 14:51:29 +08:00

526 lines
16 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>
<ElCard shadow="never" class="info-card current-package-card">
<template #header>
<div class="card-header">
<span class="header-title">当前生效套餐</span>
<div class="header-actions">
<slot name="header-actions" />
</div>
</div>
</template>
<!-- 绑定设备提示优先于一切内容 -->
<template v-if="props.boundDeviceId">
<ElEmpty :image-size="100">
<template #description>
<span>该卡已绑定设备请点击 </span>
<ElButton
type="primary"
link
style="font-size: 15px; font-weight: 600"
@click="emit('navigateToDevice', props.boundDeviceNo!)"
>{{ props.boundDeviceNo }}</ElButton
>
<span> 进行查看</span>
</template>
</ElEmpty>
</template>
<!-- 显示错误信息 -->
<ElAlert v-else-if="errorMsg" :title="errorMsg" type="warning" :closable="false" />
<template v-else-if="currentPackage">
<!-- 流量使用情况 - 横向进度条卡片 -->
<div v-if="shouldShowTrafficCard" class="flow-progress-card">
<!-- 管理员视角始终显示真流量和虚流量 -->
<template v-if="isAdminOrPlatform">
<div class="flow-stats-container">
<!-- 真流量 -->
<div v-if="canViewCurrentPackageRealUsage" class="flow-stat-section">
<div class="flow-stat-label">真流量使用</div>
<div class="flow-stat-row">
<span class="stat-item">
<span class="stat-label">已使用</span>
<span class="stat-value">{{
formatDataSize(currentPackage.real_used_mb || 0)
}}</span>
</span>
<span class="stat-item">
<span class="stat-label">总量</span>
<span class="stat-value">{{
formatDataSize(currentPackage.real_total_mb || 0)
}}</span>
</span>
<span class="stat-item">
<span class="stat-label">剩余</span>
<span class="stat-value">{{
formatDataSize(
(currentPackage.real_total_mb || 0) - (currentPackage.real_used_mb || 0)
)
}}</span>
</span>
<span v-if="canShowCurrentPackageVirtualUsage" class="stat-item">
<span class="stat-label">停机阈值</span>
<span class="stat-value">{{
formatDataSize(currentPackage.virtual_total_mb || 0)
}}</span>
</span>
</div>
<div class="progress-wrapper">
<div class="progress-track">
<ElProgress
:percentage="
getUsageProgress(
currentPackage.real_used_mb || 0,
currentPackage.real_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
getUsageProgress(
currentPackage.real_used_mb || 0,
currentPackage.real_total_mb || 0
)
)
"
:stroke-width="10"
/>
<div
v-if="canShowCurrentPackageVirtualUsage"
class="breakpoint-marker"
:style="
getBreakpointMarkerStyle(
currentPackage.virtual_total_mb || 0,
currentPackage.real_total_mb || 0
)
"
:title="`停机阈值/真流量总量: ${formatDataSize(currentPackage.virtual_total_mb || 0)} / ${formatDataSize(currentPackage.real_total_mb || 0)}`"
></div>
</div>
</div>
</div>
<!-- 虚流量 -->
<div v-if="canShowCurrentPackageVirtualUsage" class="flow-stat-section">
<div class="flow-stat-label">虚流量使用</div>
<div class="flow-stat-row">
<span class="stat-item">
<span class="stat-label">已使用</span>
<span class="stat-value">{{
formatDataSize(currentPackage.virtual_used_mb || 0)
}}</span>
</span>
<span class="stat-item">
<span class="stat-label">总量</span>
<span class="stat-value">{{
formatDataSize(currentPackage.real_total_mb || 0)
}}</span>
</span>
<span class="stat-item">
<span class="stat-label">剩余</span>
<span class="stat-value">{{
formatDataSize(
(currentPackage.real_total_mb || 0) - (currentPackage.virtual_used_mb || 0)
)
}}</span>
</span>
</div>
<div class="progress-wrapper">
<div class="progress-track">
<ElProgress
:show-text="false"
:percentage="
getUsageProgress(
currentPackage.virtual_used_mb || 0,
currentPackage.real_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
getUsageProgress(
currentPackage.virtual_used_mb || 0,
currentPackage.real_total_mb || 0
)
)
"
:stroke-width="10"
/>
</div>
<span class="progress-percentage">
{{
formatProgressPercentage(
getUsageProgress(
currentPackage.virtual_used_mb || 0,
currentPackage.real_total_mb || 0
)
)
}}
</span>
</div>
</div>
</div>
</template>
<!-- 非管理员 未启用虚流量 -->
<template v-else>
<div class="flow-stats-row">
<div class="flow-stat-item">
<span class="flow-stat-label">已使用</span>
<span class="flow-stat-value">
{{ formatDataSize(getDisplayedUsedMb(currentPackage)) }}
</span>
</div>
<div class="flow-stat-item">
<span class="flow-stat-label">总量</span>
<span class="flow-stat-value">
{{ formatDataSize(getDisplayedTotalMb(currentPackage)) }}
</span>
</div>
<div class="flow-stat-item">
<span class="flow-stat-label">剩余</span>
<span class="flow-stat-value">
{{ formatDataSize(getDisplayedRemainingMb(currentPackage)) }}
</span>
</div>
</div>
<div class="flow-progress-bar" style="margin-top: 16px">
<ElProgress
:percentage="
getUsageProgress(
getDisplayedUsedMb(currentPackage),
getDisplayedTotalMb(currentPackage)
)
"
:color="
getProgressColorByPercentage(
getUsageProgress(
getDisplayedUsedMb(currentPackage),
getDisplayedTotalMb(currentPackage)
)
)
"
:stroke-width="14"
/>
</div>
</template>
</div>
<!-- 套餐详情表格 -->
<ElDescriptions :column="descriptionsColumn" border class="package-info-table">
<ElDescriptionsItem label="订单号">
{{ currentPackage.order_no || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐名称">
{{ currentPackage.package_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐成本价格" v-if="isAdminOrPlatform">
{{ formatAmount(currentPackage.paid_amount || 0) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐零售价">
{{ formatAmount(currentPackage.retail_amount || 0) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐类型">
{{ getPackageTypeName(currentPackage.package_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">
{{ formatDateTime(currentPackage.start_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="到期时间">
{{ formatDateTime(currentPackage.expire_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="下单时间">
{{ formatDateTime(currentPackage.created_at) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentPackage.duration_days" label="套餐时长">
{{ currentPackage.duration_days }}
</ElDescriptionsItem>
<ElDescriptionsItem label="到期计时基准">
{{ getExpiryBaseLabel(currentPackage.expiry_base) }}
</ElDescriptionsItem>
</ElDescriptions>
</template>
<!-- 空状态 -->
<ElEmpty v-else-if="!loading" :description="errorMsg || '暂无当前生效套餐'" :image-size="100" />
</ElCard>
</template>
<script setup lang="ts">
import {
ElCard,
ElButton,
ElDescriptions,
ElDescriptionsItem,
ElProgress,
ElEmpty,
ElAlert
} from 'element-plus'
import { useWindowSize } from '@vueuse/core'
import { useAssetFormatters } from '../composables/useAssetFormatters'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import type { AssetPackageUsageRecord } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
const {
formatAmount,
formatDataSize,
getExpiryBaseLabel,
getUsageProgress,
getProgressColorByPercentage,
formatProgressPercentage,
getBreakpointMarkerStyle
} = useAssetFormatters()
const { hasAuth } = useAuth()
const { width } = useWindowSize()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isAdminOrPlatform = computed(
() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2
)
interface Props {
currentPackage: AssetPackageUsageRecord | null
loading?: boolean
errorMsg?: string
boundDeviceId?: number | null
boundDeviceNo?: string
boundDeviceName?: string
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
errorMsg: '',
boundDeviceId: undefined,
boundDeviceNo: '',
boundDeviceName: ''
})
const canViewCurrentPackageRealUsage = computed(() =>
hasAuth('asset_info:view_current_package_real_usage')
)
const canViewCurrentPackageVirtualUsage = computed(() =>
hasAuth('asset_info:view_current_package_virtual_usage')
)
const canShowCurrentPackageVirtualUsage = computed(
() =>
canViewCurrentPackageVirtualUsage.value && props.currentPackage?.enable_virtual_data !== false
)
const shouldShowTrafficCard = computed(() => {
if (!isAdminOrPlatform.value) return true
return canViewCurrentPackageRealUsage.value || canShowCurrentPackageVirtualUsage.value
})
const descriptionsColumn = computed(() => {
if (width.value <= 640) return 1
if (width.value <= 1024) return 2
return 4
})
const emit = defineEmits<{
showRecharge: []
navigateToDevice: [deviceNo: string]
}>()
const getDisplayedTotalMb = (pkg: AssetPackageUsageRecord) => pkg.real_total_mb || 0
const getDisplayedUsedMb = (pkg: AssetPackageUsageRecord) =>
pkg.enable_virtual_data ? pkg.virtual_used_mb || 0 : pkg.real_used_mb || 0
const getDisplayedRemainingMb = (pkg: AssetPackageUsageRecord) =>
getDisplayedTotalMb(pkg) - getDisplayedUsedMb(pkg)
const getPackageTypeName = (type?: string) => {
const typeMap: Record<string, string> = {
formal: '正式套餐',
addon: '加油包'
}
return typeMap[type || ''] || '-'
}
</script>
<style scoped lang="scss">
.current-package-card {
.card-header {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
.header-title {
font-size: 16px;
font-weight: 500;
}
}
.flow-progress-card {
padding: 20px;
margin-bottom: 20px;
background: var(--el-fill-color-light);
border-radius: 8px;
.flow-stats-container {
display: flex;
gap: 40px;
min-width: 0;
.flow-stat-section {
flex: 1;
min-width: 0;
.flow-stat-label {
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.flow-stat-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 8px;
.stat-item {
display: flex;
gap: 4px;
align-items: baseline;
.stat-label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
}
}
.flow-stats-row {
display: flex;
flex-wrap: wrap;
gap: 32px;
margin-bottom: 16px;
.flow-stat-item {
display: flex;
gap: 8px;
align-items: baseline;
.flow-stat-label {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.flow-stat-value {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.flow-progress-bar {
padding: 0 4px;
.progress-label {
margin-bottom: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.progress-wrapper {
display: flex;
gap: 1px;
align-items: center;
.progress-track {
position: relative;
flex: 1;
min-width: 0;
.breakpoint-marker {
position: absolute;
top: 50%;
z-index: 1;
width: 8px;
height: 8px;
cursor: pointer;
background-color: var(--el-color-danger);
border-radius: 50%;
transform: translate(-50%, -50%);
}
}
.progress-percentage {
min-width: 44px;
font-size: 14px;
color: var(--el-text-color-secondary);
text-align: right;
white-space: nowrap;
}
}
}
.package-info-table {
margin-top: 0;
}
:deep(.el-descriptions__body) {
overflow-x: auto;
}
:deep(.el-descriptions__cell) {
word-break: break-word;
}
@media (width <= 992px) {
.flow-progress-card {
.flow-stats-container {
flex-direction: column;
gap: 20px;
}
}
}
@media (width <= 768px) {
.card-header {
flex-direction: column;
align-items: flex-start;
.header-actions {
width: 100%;
}
}
.flow-progress-card {
padding: 14px;
.flow-stats-row,
.flow-stat-row {
flex-direction: column;
gap: 8px;
}
.progress-wrapper {
flex-direction: column;
gap: 6px;
align-items: stretch;
.progress-percentage {
min-width: 0;
text-align: left;
}
}
}
}
}
</style>