878 lines
28 KiB
Vue
878 lines
28 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"
|
||
:disabled="Boolean(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 v-if="isAdminOrPlatform" label="套餐成本价格" width="120">
|
||
<template #default="scope">
|
||
{{ formatAmount(scope.row.paid_amount || 0) }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="套餐零售价" width="120">
|
||
<template #default="scope">
|
||
{{ formatAmount(scope.row.retail_amount || 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 && shouldShowAdminTrafficBreakdown(scope.row)">
|
||
<div v-if="canViewCurrentPackageRealUsage" 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
|
||
v-if="canViewCurrentPackageRealUsage"
|
||
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
|
||
v-if="canShowPackageVirtualUsage(scope.row)"
|
||
class="progress-text"
|
||
:style="{ marginTop: canViewCurrentPackageRealUsage ? '8px' : '0' }"
|
||
>
|
||
<span class="used"
|
||
>已使用虚流量: {{ formatDataSize(scope.row.virtual_used_mb || 0) }}</span
|
||
>
|
||
<span class="total"
|
||
>停机阈值: {{ formatDataSize(scope.row.virtual_total_mb || 0) }}</span
|
||
>
|
||
</div>
|
||
<div v-if="canShowPackageVirtualUsage(scope.row)" class="progress-wrapper">
|
||
<div class="progress-track">
|
||
<ElProgress
|
||
style="margin-top: 4px"
|
||
:show-text="false"
|
||
:percentage="
|
||
getUsageProgress(
|
||
scope.row.virtual_used_mb || 0,
|
||
scope.row.real_total_mb || 0
|
||
)
|
||
"
|
||
:color="
|
||
getProgressColorByPercentage(
|
||
getUsageProgress(
|
||
scope.row.virtual_used_mb || 0,
|
||
scope.row.real_total_mb || 0
|
||
)
|
||
)
|
||
"
|
||
/>
|
||
<div
|
||
class="breakpoint-marker"
|
||
:style="
|
||
getBreakpointMarkerStyle(
|
||
scope.row.virtual_total_mb || 0,
|
||
scope.row.real_total_mb || 0
|
||
)
|
||
"
|
||
:title="`停机阈值/真流量总量: ${formatDataSize(scope.row.virtual_total_mb || 0)} / ${formatDataSize(scope.row.real_total_mb || 0)}`"
|
||
></div>
|
||
</div>
|
||
<span class="progress-percentage">
|
||
{{
|
||
formatProgressPercentage(
|
||
getUsageProgress(
|
||
scope.row.virtual_used_mb || 0,
|
||
scope.row.real_total_mb || 0
|
||
)
|
||
)
|
||
}}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="!isAdminOrPlatform">
|
||
<div class="progress-text">
|
||
<span class="used"
|
||
>已使用: {{ formatDataSize(getDisplayedUsedMb(scope.row)) }}</span
|
||
>
|
||
<span class="total"
|
||
>总量: {{ formatDataSize(scope.row.real_total_mb || 0) }}</span
|
||
>
|
||
<span class="remaining"
|
||
>剩余: {{ formatDataSize(getDisplayedRemainingMb(scope.row)) }}</span
|
||
>
|
||
</div>
|
||
<ElProgress
|
||
style="margin-top: 8px"
|
||
:percentage="
|
||
getUsageProgress(
|
||
getDisplayedUsedMb(scope.row),
|
||
getDisplayedTotalMb(scope.row)
|
||
)
|
||
"
|
||
:color="
|
||
getProgressColorByPercentage(
|
||
getUsageProgress(
|
||
getDisplayedUsedMb(scope.row),
|
||
getDisplayedTotalMb(scope.row)
|
||
)
|
||
)
|
||
"
|
||
/>
|
||
</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="240" fixed="right">
|
||
<template #default="scope">
|
||
<div class="package-actions">
|
||
<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>
|
||
<ElButton
|
||
v-if="canUpdateUsedData"
|
||
type="primary"
|
||
link
|
||
@click="handleShowUsedDataDialog(scope.row)"
|
||
>
|
||
修改已用量
|
||
</ElButton>
|
||
<ElButton
|
||
v-if="canUpdateExpiresAt"
|
||
type="primary"
|
||
link
|
||
@click="handleShowExpiresAtDialog(scope.row)"
|
||
>
|
||
修改过期时间
|
||
</ElButton>
|
||
</div>
|
||
</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-id="currentRefundOrderId"
|
||
:initial-order-no="currentRefundOrderNo"
|
||
@success="handleRefundSuccess"
|
||
/>
|
||
|
||
<ElDialog
|
||
v-model="usedDataDialogVisible"
|
||
title="修改已用量"
|
||
width="30%"
|
||
@closed="resetUsedDataForm"
|
||
>
|
||
<ElForm
|
||
ref="usedDataFormRef"
|
||
:model="usedDataForm"
|
||
:rules="usedDataRules"
|
||
label-width="90px"
|
||
>
|
||
<ElFormItem label="套餐名称">
|
||
<span>{{ selectedPackage?.package_name || '-' }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="当前已用量">
|
||
<span>{{ formatDataSize(selectedPackage?.real_used_mb || 0) }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="新的已用量" prop="data_usage">
|
||
<div class="used-data-input">
|
||
<ElInputNumber
|
||
v-model="usedDataForm.data_usage"
|
||
:min="0"
|
||
:max="usedDataMaxValue"
|
||
:precision="usedDataForm.unit === 'MB' ? 0 : 3"
|
||
:step="usedDataForm.unit === 'MB' ? 1 : 0.1"
|
||
class="used-data-input__number"
|
||
placeholder="请输入新的套餐真实已用量"
|
||
/>
|
||
<ElSelect v-model="usedDataForm.unit" class="used-data-input__unit">
|
||
<ElOption label="MB" value="MB" />
|
||
<ElOption label="GB" value="GB" />
|
||
</ElSelect>
|
||
</div>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="usedDataDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="usedDataSubmitting" @click="handleSubmitUsedData">
|
||
确认修改
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<ElDialog
|
||
v-model="expiresAtDialogVisible"
|
||
title="修改过期时间"
|
||
width="420px"
|
||
@closed="resetExpiresAtForm"
|
||
>
|
||
<ElForm
|
||
ref="expiresAtFormRef"
|
||
:model="expiresAtForm"
|
||
:rules="expiresAtRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="套餐名称">
|
||
<span>{{ selectedPackage?.package_name || '-' }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="当前过期时间">
|
||
<span>{{ formatDateTime(selectedPackage?.expires_at) }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="新的过期时间" prop="expires_at">
|
||
<ElDatePicker
|
||
v-model="expiresAtForm.expires_at"
|
||
type="datetime"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
placeholder="请选择新的套餐过期时间"
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="expiresAtDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="expiresAtSubmitting" @click="handleSubmitExpiresAt">
|
||
确认修改
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</ElCard>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, reactive, ref, watch } from 'vue'
|
||
import {
|
||
ElCard,
|
||
ElTable,
|
||
ElTableColumn,
|
||
ElTag,
|
||
ElButton,
|
||
ElProgress,
|
||
ElEmpty,
|
||
ElPagination,
|
||
ElDialog,
|
||
ElForm,
|
||
ElFormItem,
|
||
ElInputNumber,
|
||
ElDatePicker,
|
||
ElSelect,
|
||
ElOption
|
||
} from 'element-plus'
|
||
import { ElMessage } from 'element-plus'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
|
||
import { useAssetFormatters } from '../composables/useAssetFormatters'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import type { AssetPackageUsageRecord } from '@/types/api'
|
||
import { AssetService } from '@/api/modules'
|
||
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 canViewCurrentPackageRealUsage = computed(() =>
|
||
hasAuth('asset_info:view_current_package_real_usage')
|
||
)
|
||
const canViewCurrentPackageVirtualUsage = computed(() =>
|
||
hasAuth('asset_info:view_current_package_virtual_usage')
|
||
)
|
||
const isAdminOrPlatform = computed(
|
||
() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2
|
||
)
|
||
const isProductionMode = import.meta.env.MODE === 'production'
|
||
const canUseAdjustmentInCurrentEnv = (permission: string) => {
|
||
if (isProductionMode) {
|
||
return userInfo.value.user_type === 2 && hasAuth(permission)
|
||
}
|
||
|
||
return userInfo.value.user_type === 1
|
||
}
|
||
const canUpdateUsedData = computed(() => canUseAdjustmentInCurrentEnv('package:update_used_data'))
|
||
const canUpdateExpiresAt = computed(() =>
|
||
canUseAdjustmentInCurrentEnv('package:update_expires_at')
|
||
)
|
||
|
||
// 退款申请对话框
|
||
const createRefundDialogVisible = ref(false)
|
||
const currentRefundOrderId = ref<number | undefined>(undefined)
|
||
const currentRefundOrderNo = ref<string | undefined>(undefined)
|
||
const selectedPackage = ref<PackageInfo>()
|
||
const usedDataDialogVisible = ref(false)
|
||
const expiresAtDialogVisible = ref(false)
|
||
const usedDataSubmitting = ref(false)
|
||
const expiresAtSubmitting = ref(false)
|
||
const usedDataFormRef = ref<FormInstance>()
|
||
const expiresAtFormRef = ref<FormInstance>()
|
||
const usedDataForm = reactive<{ data_usage: number | undefined; unit: 'MB' | 'GB' }>({
|
||
data_usage: undefined,
|
||
unit: 'MB'
|
||
})
|
||
const expiresAtForm = reactive<{ expires_at: string }>({
|
||
expires_at: ''
|
||
})
|
||
|
||
const usedDataMaxValue = computed(() => {
|
||
const realTotalMb = selectedPackage.value?.real_total_mb ?? 0
|
||
return usedDataForm.unit === 'GB' ? Number((realTotalMb / 1024).toFixed(3)) : realTotalMb
|
||
})
|
||
|
||
const validateUsedData = (
|
||
_rule: unknown,
|
||
_value: number | undefined,
|
||
callback: (error?: Error) => void
|
||
) => {
|
||
const dataUsageMb = getUsedDataMb()
|
||
if (usedDataForm.data_usage === undefined || usedDataForm.data_usage === null) {
|
||
callback(new Error('请输入新的套餐真实已用量'))
|
||
return
|
||
}
|
||
|
||
if (dataUsageMb === undefined || dataUsageMb < 0 || !Number.isInteger(dataUsageMb)) {
|
||
callback(new Error('请输入可换算为非负整数 MB 的已用量'))
|
||
return
|
||
}
|
||
|
||
if (dataUsageMb > (selectedPackage.value?.real_total_mb ?? 0)) {
|
||
callback(new Error('新的已用量不能大于真流量总量'))
|
||
return
|
||
}
|
||
|
||
callback()
|
||
}
|
||
|
||
const usedDataRules = reactive<FormRules>({
|
||
data_usage: [{ validator: validateUsedData, trigger: 'blur' }]
|
||
})
|
||
const expiresAtRules = reactive<FormRules>({
|
||
expires_at: [{ required: true, message: '请选择新的套餐过期时间', trigger: 'change' }]
|
||
})
|
||
|
||
const handleCreateRefund = (row: PackageInfo) => {
|
||
if (!hasAuth('package:create_refund')) {
|
||
ElMessage.warning('您没有创建退款申请的权限')
|
||
return
|
||
}
|
||
currentRefundOrderId.value = row.order_id
|
||
currentRefundOrderNo.value = row.order_no
|
||
createRefundDialogVisible.value = true
|
||
}
|
||
|
||
const handleRefundSuccess = () => {
|
||
emit('refresh')
|
||
}
|
||
|
||
const getPackageUsageId = (row: PackageInfo) => row.package_usage_id ?? row.id
|
||
|
||
const getUsedDataMb = () => {
|
||
if (usedDataForm.data_usage === undefined || usedDataForm.data_usage === null) return undefined
|
||
return usedDataForm.unit === 'GB' ? usedDataForm.data_usage * 1024 : usedDataForm.data_usage
|
||
}
|
||
|
||
const handleShowUsedDataDialog = (row: PackageInfo) => {
|
||
selectedPackage.value = row
|
||
usedDataForm.data_usage = row.real_used_mb ?? 0
|
||
usedDataForm.unit = 'MB'
|
||
usedDataDialogVisible.value = true
|
||
}
|
||
|
||
const handleShowExpiresAtDialog = (row: PackageInfo) => {
|
||
selectedPackage.value = row
|
||
expiresAtForm.expires_at = row.expires_at ? formatDateTime(row.expires_at) : ''
|
||
expiresAtDialogVisible.value = true
|
||
}
|
||
|
||
const resetUsedDataForm = () => {
|
||
usedDataFormRef.value?.resetFields()
|
||
usedDataForm.data_usage = undefined
|
||
usedDataForm.unit = 'MB'
|
||
selectedPackage.value = undefined
|
||
}
|
||
|
||
const resetExpiresAtForm = () => {
|
||
expiresAtFormRef.value?.resetFields()
|
||
expiresAtForm.expires_at = ''
|
||
selectedPackage.value = undefined
|
||
}
|
||
|
||
watch(
|
||
() => usedDataForm.unit,
|
||
() => {
|
||
if (usedDataForm.data_usage === undefined) return
|
||
usedDataForm.data_usage = Math.min(usedDataForm.data_usage, usedDataMaxValue.value)
|
||
}
|
||
)
|
||
|
||
const handleSubmitUsedData = async () => {
|
||
if (!usedDataFormRef.value || !selectedPackage.value) return
|
||
const packageUsageId = getPackageUsageId(selectedPackage.value)
|
||
if (!props.assetIdentifier || packageUsageId === undefined) {
|
||
ElMessage.error('缺少资产标识或套餐使用记录ID')
|
||
return
|
||
}
|
||
|
||
await usedDataFormRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
usedDataSubmitting.value = true
|
||
try {
|
||
const dataUsageMb = getUsedDataMb()
|
||
await AssetService.updateAssetPackageUsedData(props.assetIdentifier, packageUsageId, {
|
||
data_usage_mb: dataUsageMb ?? null
|
||
})
|
||
ElMessage.success('已用量修改成功')
|
||
usedDataDialogVisible.value = false
|
||
emit('refresh')
|
||
} finally {
|
||
usedDataSubmitting.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleSubmitExpiresAt = async () => {
|
||
if (!expiresAtFormRef.value || !selectedPackage.value) return
|
||
const packageUsageId = getPackageUsageId(selectedPackage.value)
|
||
if (!props.assetIdentifier || packageUsageId === undefined) {
|
||
ElMessage.error('缺少资产标识或套餐使用记录ID')
|
||
return
|
||
}
|
||
|
||
await expiresAtFormRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
expiresAtSubmitting.value = true
|
||
try {
|
||
await AssetService.updateAssetPackageExpiresAt(props.assetIdentifier, packageUsageId, {
|
||
expires_at: expiresAtForm.expires_at
|
||
})
|
||
ElMessage.success('过期时间修改成功')
|
||
expiresAtDialogVisible.value = false
|
||
emit('refresh')
|
||
} finally {
|
||
expiresAtSubmitting.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// Props 使用 API 类型
|
||
type PackageInfo = AssetPackageUsageRecord
|
||
|
||
const canShowPackageVirtualUsage = (pkg: PackageInfo) =>
|
||
canViewCurrentPackageVirtualUsage.value && pkg.enable_virtual_data !== false
|
||
|
||
const shouldShowAdminTrafficBreakdown = (pkg: PackageInfo) => {
|
||
if (!isAdminOrPlatform.value) return true
|
||
return canViewCurrentPackageRealUsage.value || canShowPackageVirtualUsage(pkg)
|
||
}
|
||
|
||
interface Props {
|
||
packageList: PackageInfo[]
|
||
total?: number
|
||
page?: number
|
||
pageSize?: number
|
||
boundDeviceId?: number
|
||
boundDeviceNo?: string
|
||
loading?: boolean
|
||
assetIdentifier?: string
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
total: 0,
|
||
page: 1,
|
||
pageSize: 10,
|
||
boundDeviceId: undefined,
|
||
boundDeviceNo: '',
|
||
loading: false,
|
||
assetIdentifier: ''
|
||
})
|
||
|
||
// 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,
|
||
formatProgressPercentage,
|
||
getBreakpointMarkerStyle
|
||
} = useAssetFormatters()
|
||
|
||
// 套餐名称点击
|
||
const handlePackageNameClick = (packageRow: PackageInfo) => {
|
||
if (!hasAuth('package_usage:daily_records')) {
|
||
ElMessage.warning('您没有查看流量详单的权限')
|
||
return
|
||
}
|
||
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 getDisplayedTotalMb = (pkg: PackageInfo) => pkg.real_total_mb || 0
|
||
|
||
const getDisplayedUsedMb = (pkg: PackageInfo) =>
|
||
pkg.enable_virtual_data ? pkg.virtual_used_mb || 0 : pkg.real_used_mb || 0
|
||
|
||
const getDisplayedRemainingMb = (pkg: PackageInfo) =>
|
||
getDisplayedTotalMb(pkg) - getDisplayedUsedMb(pkg)
|
||
|
||
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 {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.package-table {
|
||
:deep(.el-table__header) {
|
||
th {
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary, #374151);
|
||
}
|
||
}
|
||
|
||
.package-name-text {
|
||
display: block;
|
||
overflow: hidden;
|
||
font-weight: 500;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.package-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 6px 8px;
|
||
align-items: center;
|
||
|
||
.el-button {
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
.traffic-progress {
|
||
.progress-text {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
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 {
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.pagination {
|
||
justify-content: flex-end;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
@media (width <= 768px) {
|
||
.card-header {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
align-items: stretch;
|
||
|
||
.card-header-right,
|
||
.card-header-right .el-button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.package-table-wrapper {
|
||
.package-table {
|
||
min-width: 900px;
|
||
}
|
||
|
||
.traffic-progress {
|
||
.progress-text {
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.progress-wrapper {
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
align-items: stretch;
|
||
|
||
.progress-percentage {
|
||
min-width: 0;
|
||
text-align: left;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pagination {
|
||
justify-content: flex-start;
|
||
overflow-x: auto;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.used-data-input {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
gap: 8px;
|
||
width: 100%;
|
||
|
||
&__number {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__unit {
|
||
width: 112px;
|
||
|
||
:deep(.el-select__placeholder) {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
:deep(.el-select__selected-item) {
|
||
justify-content: flex-start;
|
||
}
|
||
}
|
||
}
|
||
</style>
|