Files
one-pipe-system/src/views/asset-management/asset-information/components/PackageListCard.vue
sexygoat 57ddbe5f85
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m34s
fix: order
2026-04-30 12:04:04 +08:00

487 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 package-info">
<template #header>
<div class="card-header">
<span>套餐列表</span>
<div class="card-header-right">
<ElButton type="primary" :disabled="props.boundDeviceId" @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="240">
<template #default="scope">
<ElButton
type="primary"
link
@click="handleOrderNoClick(scope.row)"
v-if="hasAuth('order:package_list_detail')"
>
{{ scope.row.order_no }}
</ElButton>
<span v-else>{{ scope.row.order_no }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="套餐名称" width="200" show-overflow-tooltip>
<template #default="scope">
<span class="package-name-text">{{ scope.row.package_name }}</span>
</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 label="流量" min-width="450">
<template #default="scope">
<div class="traffic-progress">
<template v-if="isAdminOrPlatform">
<div class="progress-text">
<span class="used"
>已使用真流量: {{ formatDataSize(scope.row.real_used_mb || 0) }}</span
>
<span class="total"
>真流量总量: {{ formatDataSize(scope.row.real_total_mb || 0) }}</span
>
<span class="remaining"
>真流量剩余:
{{
formatDataSize(
(scope.row.real_total_mb || 0) - (scope.row.real_used_mb || 0)
)
}}</span
>
</div>
<ElProgress
style="margin-top: 4px"
:percentage="
getUsageProgress(scope.row.real_used_mb || 0, scope.row.real_total_mb || 0)
"
:color="
getProgressColorByPercentage(
getUsageProgress(scope.row.real_used_mb || 0, scope.row.real_total_mb || 0)
)
"
/>
<div class="progress-text" style="margin-top: 8px">
<span class="used"
>已使用虚流量: {{ formatDataSize(scope.row.virtual_used_mb || 0) }}</span
>
<span class="total"
>停机阈值: {{ formatDataSize(scope.row.virtual_total_mb || 0) }}</span
>
</div>
<div class="progress-wrapper">
<ElProgress
style="margin-top: 4px"
:percentage="
getUsageProgress(
scope.row.virtual_used_mb || 0,
scope.row.virtual_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
getUsageProgress(
scope.row.virtual_used_mb || 0,
scope.row.virtual_total_mb || 0
)
)
"
/>
<div
class="breakpoint-marker"
:style="{ left: getBreakpointPosition(scope.row) + '%' }"
:title="`停机阈值/真流量总量: ${formatDataSize(scope.row.virtual_total_mb || 0)} / ${formatDataSize(scope.row.real_total_mb || 0)}`"
></div>
</div>
</template>
<template v-else>
<div class="progress-text">
<span class="used"
>已使用:
{{
formatDataSize(
scope.row.enable_virtual_data
? scope.row.virtual_used_mb || 0
: scope.row.real_used_mb || 0
)
}}</span
>
<span class="total"
>总量: {{ formatDataSize(scope.row.real_total_mb || 0) }}</span
>
<span class="remaining"
>剩余:
{{
formatDataSize(
(scope.row.real_total_mb || 0) - (scope.row.real_used_mb || 0)
)
}}</span
>
</div>
<ElProgress
style="margin-top: 8px"
:percentage="
getUsageProgress(
scope.row.enable_virtual_data
? scope.row.virtual_used_mb || 0
: scope.row.real_used_mb || 0,
scope.row.real_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
getUsageProgress(
scope.row.enable_virtual_data
? scope.row.virtual_used_mb || 0
: scope.row.real_used_mb || 0,
scope.row.real_total_mb || 0
)
)
"
/>
</template>
</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>
<ElTableColumn label="操作" width="180" fixed="right">
<template #default="scope">
<ElButton
v-if="hasAuth('package_usage:daily_records')"
type="primary"
link
@click="handlePackageNameClick(scope.row)"
>
流量详单
</ElButton>
<ElButton
v-if="scope.row.status !== 4 && hasAuth('package:create_refund')"
type="primary"
link
@click="handleCreateRefund(scope.row)"
>
退款申请
</ElButton>
<ElButton
v-if="
scope.row.status === 4 &&
scope.row.refund_id &&
hasAuth('refund:package_list_detail')
"
type="primary"
link
@click="handleViewRefundDetail(scope.row)"
>
退款详情
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<ElPagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.page_size"
:total="pagination.total"
:page-sizes="[5, 10, 20]"
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>
<!-- 退款申请对话框 -->
<CreateRefundDialog
v-model="createRefundDialogVisible"
:initial-order-no="currentRefundOrderNo"
@success="handleRefundSuccess"
/>
</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'
import { useUserStore } from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import CreateRefundDialog from '@/components/business/CreateRefundDialog.vue'
const { hasAuth } = useAuth()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isAdminOrPlatform = computed(
() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2
)
// 退款申请对话框
const createRefundDialogVisible = ref(false)
const currentRefundOrderNo = ref<string | undefined>(undefined)
const handleCreateRefund = (row: PackageInfo) => {
if (!hasAuth('package:create_refund')) {
ElMessage.warning('您没有创建退款申请的权限')
return
}
currentRefundOrderNo.value = row.order_no
createRefundDialogVisible.value = true
}
const handleRefundSuccess = () => {
emit('refresh')
}
// 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
(e: 'navigateToOrder', orderId: number): void
(e: 'navigateToRefund', refundId: number): void
(e: 'refresh'): void
}
const emit = defineEmits<Emits>()
// 分页状态
const pagination = ref({
page: props.page,
page_size: props.pageSize,
total: props.total
})
// 监听props变化更新分页
watch(
() => [props.page, props.pageSize, props.total],
([newPage, newPageSize, newTotal]) => {
pagination.value.page = newPage
pagination.value.page_size = newPageSize
pagination.value.total = newTotal
}
)
// 使用格式化工具
const {
formatDataSize,
formatAmount,
getPackageStatusTagType,
getUsageProgress,
getProgressColorByPercentage
} = useAssetFormatters()
const getBreakpointPosition = (pkg: PackageInfo) => {
if (!pkg.real_total_mb || pkg.real_total_mb === 0) return 0
return (pkg.virtual_total_mb / pkg.real_total_mb) * 100
}
// 套餐名称点击
const handlePackageNameClick = (packageRow: PackageInfo) => {
if (!hasAuth('package_usage:daily_records')) {
ElMessage.warning('您没有查看流量详单的权限')
return
}
emit('showDailyRecords', { packageRow })
}
// 显示流量详单
const handleShowDailyRecords = (packageRow: PackageInfo) => {
emit('showDailyRecords', { packageRow })
}
// 点击订单号
const handleOrderNoClick = (packageRow: PackageInfo) => {
if (!hasAuth('order:package_list_detail')) {
ElMessage.warning('您没有查看订单详情的权限')
return
}
emit('navigateToOrder', packageRow.order_id!)
}
// 查看退款详情
const handleViewRefundDetail = (packageRow: PackageInfo) => {
if (!hasAuth('refund:detail')) {
ElMessage.warning('您没有查看退款详情的权限')
return
}
emit('navigateToRefund', packageRow.refund_id!)
}
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-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
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);
}
}
.progress-wrapper {
position: relative;
.breakpoint-marker {
position: absolute;
top: 50%;
width: 8px;
height: 8px;
background-color: var(--el-color-danger);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 1;
cursor: pointer;
}
}
}
}
}
.pagination {
margin-top: 16px;
justify-content: flex-end;
}
}
}
</style>