304 lines
9.3 KiB
Vue
304 lines
9.3 KiB
Vue
<template>
|
||
<ElCard shadow="never" class="info-card package-info">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>套餐列表</span>
|
||
<div class="card-header-right">
|
||
<ElButton type="primary" @click="handleShowRecharge">套餐充值</ElButton>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div v-loading="loading" class="package-table-wrapper">
|
||
<!-- 绑定设备提示 -->
|
||
<ElEmpty v-if="props.boundDeviceId" :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>
|
||
<div v-else-if="packageList && packageList.length > 0" class="table-scroll-container">
|
||
<ElTable :data="packageList" class="package-table" border>
|
||
<ElTableColumn label="套餐名称" width="150">
|
||
<template #default="scope">
|
||
<ElButton
|
||
type="primary"
|
||
link
|
||
class="package-name-link"
|
||
@click="handlePackageNameClick(scope.row)"
|
||
>
|
||
{{ scope.row.package_name }}
|
||
</ElButton>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="套餐价格" width="120">
|
||
<template #default="scope">
|
||
{{ formatAmount(scope.row.paid_amount || scope.row.package_price || 0) }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="status_name" label="套餐状态" width="100">
|
||
<template #default="scope">
|
||
<ElTag :type="getPackageStatusTagType(scope.row.status)" size="small">
|
||
{{ scope.row.status_name }}
|
||
</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="data_limit_mb" label="总流量" width="120">
|
||
<template #default="scope">
|
||
{{ formatDataSize(scope.row.data_limit_mb) }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="流量" min-width="220">
|
||
<template #default="scope">
|
||
<div class="traffic-progress">
|
||
<div class="progress-text">
|
||
<template v-if="scope.row.enable_virtual_data">
|
||
<span class="used"
|
||
>已用: {{ formatDataSize(scope.row.virtual_used_mb || 0) }}</span
|
||
>
|
||
<span class="remaining"
|
||
>剩余: {{ formatDataSize(scope.row.virtual_remain_mb || 0) }}</span
|
||
>
|
||
<span class="total"
|
||
>总量: {{ formatDataSize(scope.row.virtual_limit_mb || 0) }}</span
|
||
>
|
||
</template>
|
||
<template v-else>
|
||
<span class="used">已用: {{ formatDataSize(scope.row.data_usage_mb) }}</span>
|
||
<span class="remaining"
|
||
>剩余:
|
||
{{ formatDataSize(scope.row.data_limit_mb - scope.row.data_usage_mb) }}</span
|
||
>
|
||
<span class="total">总量: {{ formatDataSize(scope.row.data_limit_mb) }}</span>
|
||
</template>
|
||
</div>
|
||
<ElProgress
|
||
:percentage="
|
||
scope.row.enable_virtual_data
|
||
? getUsageProgress(scope.row.virtual_used_mb, scope.row.virtual_limit_mb)
|
||
: getUsageProgress(scope.row.data_usage_mb, scope.row.data_limit_mb)
|
||
"
|
||
:color="
|
||
scope.row.enable_virtual_data
|
||
? getProgressColorByPercentage(
|
||
getUsageProgress(scope.row.virtual_used_mb, scope.row.virtual_limit_mb)
|
||
)
|
||
: getProgressColorByPercentage(
|
||
getUsageProgress(scope.row.data_usage_mb, scope.row.data_limit_mb)
|
||
)
|
||
"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="activated_at" label="开始时间" width="170" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
{{ formatDateTime(scope.row.activated_at) }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="expires_at" label="到期时间" width="170" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
{{ formatDateTime(scope.row.expires_at) }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="created_at" label="创建时间" width="170" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
{{ formatDateTime(scope.row.created_at) }}
|
||
</template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
<ElPagination
|
||
v-model:current-page="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
class="pagination"
|
||
@current-change="handlePageChange"
|
||
@size-change="handleSizeChange"
|
||
/>
|
||
</div>
|
||
<ElEmpty v-else-if="!props.boundDeviceId" description="暂无套餐数据" :image-size="100" />
|
||
</div>
|
||
</ElCard>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch } from 'vue'
|
||
import {
|
||
ElCard,
|
||
ElTable,
|
||
ElTableColumn,
|
||
ElTag,
|
||
ElButton,
|
||
ElProgress,
|
||
ElEmpty,
|
||
ElPagination
|
||
} from 'element-plus'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
import { useAssetFormatters } from '../composables/useAssetFormatters'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import type { AssetPackageUsageRecord } from '@/types/api'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
|
||
const { hasAuth } = useAuth()
|
||
|
||
// Props 使用 API 类型
|
||
type PackageInfo = AssetPackageUsageRecord
|
||
|
||
interface Props {
|
||
packageList: PackageInfo[]
|
||
total?: number
|
||
page?: number
|
||
pageSize?: number
|
||
boundDeviceId?: number
|
||
boundDeviceNo?: string
|
||
loading?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
total: 0,
|
||
page: 1,
|
||
pageSize: 10,
|
||
boundDeviceId: undefined,
|
||
boundDeviceNo: '',
|
||
loading: false
|
||
})
|
||
|
||
// Emits
|
||
interface Emits {
|
||
(e: 'showDailyRecords', payload: { packageRow: PackageInfo }): void
|
||
(e: 'showRecharge'): void
|
||
(e: 'page-change', page: number): void
|
||
(e: 'size-change', size: number): void
|
||
(e: 'navigateToDevice', deviceNo: string): void
|
||
}
|
||
|
||
const emit = defineEmits<Emits>()
|
||
|
||
// 分页状态
|
||
const pagination = ref({
|
||
page: props.page,
|
||
pageSize: props.pageSize,
|
||
total: props.total
|
||
})
|
||
|
||
// 监听props变化更新分页
|
||
watch(
|
||
() => [props.page, props.pageSize, props.total],
|
||
([newPage, newPageSize, newTotal]) => {
|
||
pagination.value.page = newPage
|
||
pagination.value.pageSize = newPageSize
|
||
pagination.value.total = newTotal
|
||
}
|
||
)
|
||
|
||
// 使用格式化工具
|
||
const {
|
||
formatDataSize,
|
||
formatAmount,
|
||
getPackageStatusTagType,
|
||
getUsageProgress,
|
||
getProgressColorByPercentage
|
||
} = useAssetFormatters()
|
||
|
||
// 套餐名称点击
|
||
const handlePackageNameClick = (packageRow: PackageInfo) => {
|
||
if (!hasAuth('package_usage:daily_records')) {
|
||
ElMessage.warning('您没有查看流量详单的权限')
|
||
return
|
||
}
|
||
emit('showDailyRecords', { packageRow })
|
||
}
|
||
|
||
// 显示流量详单
|
||
const handleShowDailyRecords = (packageRow: PackageInfo) => {
|
||
emit('showDailyRecords', { packageRow })
|
||
}
|
||
|
||
const handleShowRecharge = () => {
|
||
emit('showRecharge')
|
||
}
|
||
|
||
// 页码变化
|
||
const handlePageChange = (page: number) => {
|
||
emit('page-change', page)
|
||
}
|
||
|
||
// 每页条数变化
|
||
const handleSizeChange = (size: number) => {
|
||
emit('size-change', size)
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.info-card {
|
||
&.package-info {
|
||
width: 100%;
|
||
overflow: hidden; // 防止内容溢出
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
}
|
||
|
||
.package-table-wrapper {
|
||
.table-scroll-container {
|
||
overflow-x: auto;
|
||
width: 100%;
|
||
}
|
||
|
||
.package-table {
|
||
:deep(.el-table__header) {
|
||
th {
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary, #374151);
|
||
}
|
||
}
|
||
|
||
.package-name-link {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.traffic-progress {
|
||
.progress-text {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
font-size: 12px;
|
||
|
||
.used {
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.remaining {
|
||
color: var(--el-color-success);
|
||
}
|
||
|
||
.total {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.pagination {
|
||
margin-top: 16px;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
}
|
||
</style>
|