Files
one-pipe-system/src/views/package-management/package-list/index.vue
sexygoat a055ff166a
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m41s
feat:log
2026-04-27 15:06:27 +08:00

1333 lines
42 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>
<ArtTableFullScreen>
<div class="package-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="true"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showDialog('add')" v-permission="'package:add'"
>新增套餐</ElButton
>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="packageList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:inlineActionsCount="2"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增套餐' : '编辑套餐'"
width="50%"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="套餐编码" prop="package_code">
<ElInput
v-model="form.package_code"
placeholder="请输入套餐编码或点击生成"
:disabled="dialogType === 'edit'"
clearable
>
<template #append>
<ElButton v-if="dialogType === 'add'" @click="handleGeneratePackageCode"
>生成编码</ElButton
>
</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="套餐名称" prop="package_name">
<ElInput v-model="form.package_name" placeholder="请输入套餐名称" clearable />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="所属系列" prop="series_id">
<ElSelect
v-model="form.series_id"
placeholder="请选择套餐系列"
style="width: 100%"
filterable
remote
:remote-method="searchSeries"
:loading="seriesLoading"
clearable
>
<ElOption
v-for="series in seriesOptions"
:key="series.id"
:label="series.series_name"
:value="series.id"
/>
<template #empty>
<div style="padding: 10px; color: #909399; text-align: center">
暂无套餐系列请先创建套餐系列
</div>
</template>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="套餐类型" prop="package_type">
<ElSelect
v-model="form.package_type"
placeholder="请选择套餐类型"
style="width: 100%"
>
<ElOption
v-for="option in PACKAGE_TYPE_OPTIONS"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="有效期(月)" prop="duration_months">
<ElInputNumber
v-model="form.duration_months"
:min="1"
:max="120"
:controls="false"
style="width: 100%"
placeholder="请输入有效期(月)"
/>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 流量重置周期和套餐周期类型 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="流量重置周期" prop="data_reset_cycle">
<ElSelect
v-model="form.data_reset_cycle"
placeholder="请选择流量重置周期"
style="width: 100%"
clearable
>
<ElOption label="每日" value="daily" />
<ElOption label="每月" value="monthly" />
<ElOption label="每年" value="yearly" />
<ElOption label="不重置" value="none" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="套餐周期类型" prop="calendar_type">
<ElSelect
v-model="form.calendar_type"
placeholder="请选择套餐周期类型(可选)"
style="width: 100%"
clearable
>
<ElOption label="自然月" value="natural_month" />
<ElOption label="按天" value="by_day" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 套餐天数calendar_type为by_day时显示 -->
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day'">
<ElCol :span="12">
<ElFormItem label="套餐天数" prop="duration_days">
<ElInputNumber
v-model="form.duration_days"
:min="1"
:max="3650"
:controls="false"
style="width: 100%"
placeholder="请输入套餐天数"
/>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 真流量额度 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="真流量额度(GB)" prop="real_data_gb">
<ElInputNumber
v-model="realDataGb"
:min="0"
:precision="2"
:step="0.1"
:controls="false"
style="width: 100%"
placeholder="请输入真流量额度"
@change="handleRealDataChange"
/>
<div
v-if="realDataGb"
style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)"
>
{{ realDataGb }}GB = {{ form.real_data_mb }}MB
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="启用虚流量">
<ElSwitch
v-model="form.enable_virtual_data"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 虚流量比例和到期计时基准 -->
<ElRow :gutter="20">
<ElCol :span="12" v-if="form.enable_virtual_data">
<ElFormItem label="虚流量比例(%)" prop="virtual_ratio_percent">
<ElInputNumber
v-model="virtualRatioPercent"
:min="0"
:max="100"
:precision="2"
:step="1"
:controls="false"
style="width: 100%"
placeholder="请输入虚流量比例"
@change="handleVirtualRatioChange"
/>
<div
v-if="virtualRatioPercent !== null && realDataGb > 0"
style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)"
>
{{ realDataGb }}×(1-{{ virtualRatioPercent }}%) =
{{ calculatedVirtualDataGb }}GB
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="到期计时基准" prop="expiry_base">
<ElSelect
v-model="form.expiry_base"
placeholder="请选择到期计时基准"
style="width: 100%"
clearable
>
<ElOption label="实名激活时起算" value="from_activation" />
<ElOption label="购买时起算" value="from_purchase" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 成本价和建议零售价 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="成本价(元)" prop="cost_price">
<ElInputNumber
v-model="form.cost_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="建议零售价(元)" prop="suggested_retail_price">
<ElInputNumber
v-model="form.suggested_retail_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入建议零售价"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="24">
<ElFormItem label="套餐描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入套餐描述(可选)"
maxlength="500"
show-word-limit
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
<!-- 修改零售价对话框 -->
<ElDialog
v-model="retailPriceDialogVisible"
title="修改零售价"
width="500px"
:close-on-click-modal="false"
@closed="handleCloseRetailPriceDialog"
>
<ElForm
ref="retailPriceFormRef"
:model="retailPriceForm"
:rules="retailPriceRules"
label-width="100px"
>
<ElFormItem label="套餐名称">
<span>{{ retailPriceForm.package_name }}</span>
</ElFormItem>
<ElFormItem label="零售价" prop="retail_price">
<ElInputNumber
v-model="retailPriceForm.retail_price"
:min="0"
:max="999999"
:step="0.01"
:precision="2"
placeholder="请输入零售价"
style="width: 100%"
/>
<div style="margin-top: 5px; font-size: 12px; color: #909399">
单位 | 存储值{{ Math.round(retailPriceForm.retail_price * 100) }}
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="retailPriceDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleConfirmUpdateRetailPrice"
:loading="retailPriceLoading"
>
确认修改
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h, watch } from 'vue'
import { useRouter } from 'vue-router'
import { PackageManageService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElInputNumber } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { PackageResponse, SeriesSelectOption } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend,
PACKAGE_TYPE_OPTIONS,
getPackageTypeLabel,
getPackageTypeTag,
STATUS_SELECT_OPTIONS,
SHELF_STATUS_SELECT_OPTIONS,
getShelfStatusText
} from '@/config/constants'
import { generatePackageCode } from '@/utils/codeGenerator'
defineOptions({ name: 'PackageList' })
const { hasAuth } = useAuth()
const userStore = useUserStore()
const router = useRouter()
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const seriesLoading = ref(false)
const tableRef = ref()
const formRef = ref<FormInstance>()
const seriesOptions = ref<SeriesSelectOption[]>([])
const searchSeriesOptions = ref<SeriesSelectOption[]>([])
// 零售价修改对话框
const retailPriceDialogVisible = ref(false)
const retailPriceLoading = ref(false)
const retailPriceFormRef = ref<FormInstance>()
const retailPriceForm = reactive({
id: 0,
package_name: '',
retail_price: 0
})
const retailPriceRules = reactive<FormRules>({
retail_price: [
{ required: true, message: '请输入零售价', trigger: 'blur' },
{ type: 'number', message: '零售价必须为数字', trigger: 'blur' }
]
})
// 使用表格右键菜单功能
// 搜索表单初始值
const initialSearchState = {
package_name: '',
series_id: undefined as number | undefined,
package_type: undefined as string | undefined,
shelf_status: undefined as number | undefined,
status: undefined as number | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '套餐名称',
prop: 'package_name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入套餐名称'
}
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeries,
loading: seriesLoading.value,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '套餐类型',
prop: 'package_type',
type: 'select',
config: {
clearable: true,
placeholder: '请选择套餐类型'
},
options: () =>
PACKAGE_TYPE_OPTIONS.map((o) => ({
label: o.label,
value: o.value
}))
},
{
label: '上架状态',
prop: 'shelf_status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择上架状态'
},
options: SHELF_STATUS_SELECT_OPTIONS
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
options: STATUS_SELECT_OPTIONS
}
])
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '套餐编码', prop: 'package_code' },
{ label: '套餐名称', prop: 'package_name' },
{ label: '所属系列', prop: 'series_name' },
{ label: '套餐类型', prop: 'package_type' },
{ label: '套餐周期类型', prop: 'calendar_type' },
{ label: '有效期', prop: 'duration_months' },
{ label: '真流量', prop: 'real_data_mb' },
{ label: '虚流量', prop: 'virtual_data_mb' },
{ label: '虚流量比例', prop: 'virtual_ratio' },
{
label: '启用虚流量',
prop: 'enable_virtual_data',
formatter: (row: any) => (row.enable_virtual_data ? '是' : '否')
},
{
label: '流量重置周期',
prop: 'data_reset_cycle',
formatter: (row: any) => {
const map: Record<string, string> = {
daily: '每日',
weekly: '每周',
monthly: '每月',
none: '不重置'
}
return map[row.data_reset_cycle] || row.data_reset_cycle || '-'
}
},
{
label: '到期时间基准',
prop: 'expiry_base',
formatter: (row: any) => {
const map: Record<string, string> = {
from_activation: '实名激活',
from_purchase: '购买时'
}
return map[row.expiry_base] || row.expiry_base || '-'
}
},
{ label: '成本价', prop: 'cost_price' },
{ label: '建议售价', prop: 'suggested_retail_price' },
{ label: '上架状态', prop: 'shelf_status' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' }
]
// 表单验证规则
const rules = computed<FormRules>(() => {
const baseRules: FormRules = {
package_code: [
{ required: true, message: '请输入套餐编码', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
package_name: [
{ required: true, message: '请输入套餐名称', trigger: 'blur' },
{ min: 1, max: 255, message: '长度在 1 到 255 个字符', trigger: 'blur' }
],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
package_type: [{ required: true, message: '请选择套餐类型', trigger: 'change' }],
duration_months: [
{ required: true, message: '请输入有效期', trigger: 'blur' },
{ type: 'number', min: 1, max: 120, message: '有效期范围 1-120 月', trigger: 'blur' }
],
cost_price: [{ required: true, message: '请输入成本价', trigger: 'blur' }]
}
// 如果启用虚流量,则虚流量额度为必填,并且不能超过真流量额度
if (form.enable_virtual_data) {
baseRules.virtual_data_mb = [
{ required: true, message: '请输入虚流量额度', trigger: 'blur' },
{
validator: (_rule: any, value: any, callback: any) => {
if (value && form.real_data_mb && value > form.real_data_mb) {
callback(new Error('虚流量额度不能超过真流量额度'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
// 如果流量重置周期是每日,或套餐周期类型是按天,则套餐天数为必填
if (form.data_reset_cycle === 'daily' || form.calendar_type === 'by_day') {
baseRules.duration_days = [
{ required: true, message: '请输入套餐天数', trigger: 'blur' },
{ type: 'number', min: 1, max: 3650, message: '套餐天数范围 1-3650 天', trigger: 'blur' }
]
}
return baseRules
})
// 表单数据
const form = reactive<any>({
id: 0,
package_code: '',
package_name: '',
series_id: undefined,
package_type: '',
calendar_type: undefined,
duration_days: undefined,
duration_months: 1,
data_reset_cycle: undefined,
enable_virtual_data: false,
expiry_base: 'from_activation',
real_data_mb: 0,
virtual_data_mb: 0,
cost_price: 0,
suggested_retail_price: undefined,
description: ''
})
// GB 输入值(用于界面显示)
const realDataGb = ref<number>(0)
const virtualRatioPercent = ref<number | null>(null) // 虚流量比例(百分比)
// 计算得出的虚流量GB值
const calculatedVirtualDataGb = computed(() => {
if (virtualRatioPercent.value === null || realDataGb.value === 0) {
return 0
}
const ratio = virtualRatioPercent.value / 100
const virtualGb = realDataGb.value * (1 - ratio)
return Number(virtualGb.toFixed(2))
})
// GB 转 MB 处理
const handleRealDataChange = (value: number | null) => {
if (value === null || value === undefined) {
form.real_data_mb = 0
return
}
form.real_data_mb = Math.round(value * 1024)
// 真流量变化时,重新计算虚流量
handleVirtualRatioChange(virtualRatioPercent.value)
}
// 虚流量比例变化处理
const handleVirtualRatioChange = (value: number | null) => {
if (value === null || value === undefined || realDataGb.value === 0) {
form.virtual_data_mb = 0
return
}
// 根据比例计算虚流量:真流量 × (1 - 比例)
const virtualGb = calculatedVirtualDataGb.value
form.virtual_data_mb = Math.round(virtualGb * 1024)
}
const packageList = ref<PackageResponse[]>([])
const dialogType = ref('add')
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'package_code',
label: '套餐编码',
showOverflowTooltip: true,
width: 210
},
{
prop: 'package_name',
label: '套餐名称',
width: 200,
showOverflowTooltip: true,
formatter: (row: PackageResponse) => {
const hasPermission = hasAuth('package:detail')
return h(
'span',
{
style: hasPermission
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
: '',
onClick: hasPermission
? (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
: undefined
},
row.package_name
)
}
},
{
prop: 'series_name',
label: '所属系列',
width: 160,
showOverflowTooltip: true
},
{
prop: 'package_type',
label: '套餐类型',
width: 100,
formatter: (row: PackageResponse) => {
return h(ElTag, { type: getPackageTypeTag(row.package_type), size: 'small' }, () =>
getPackageTypeLabel(row.package_type)
)
}
},
{
prop: 'real_data_mb',
label: '真流量',
width: 120,
formatter: (row: PackageResponse) => {
const mb = row.real_data_mb ?? 0
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)}GB`
}
return `${mb}MB`
}
},
{
prop: 'virtual_data_mb',
label: '虚流量',
width: 120,
formatter: (row: PackageResponse) => {
// 如果没有启用虚流量,显示 "-"
if (!row.enable_virtual_data) {
return '-'
}
const mb = row.virtual_data_mb ?? 0
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)}GB`
}
return `${mb}MB`
}
},
{
prop: 'virtual_ratio',
label: '虚流量比例',
width: 120,
formatter: (row: PackageResponse) => {
// 如果启用虚流量且真流量大于0计算虚量百分比
const virtualData = row.virtual_data_mb ?? 0
const realData = row.real_data_mb ?? 0
if (row.enable_virtual_data && realData > 0 && virtualData > 0) {
// 虚量百分比 = (1 - 虚流量/真流量) * 100%
// 例如真流量100G虚流量70G则虚量百分比 = (1 - 70/100) * 100% = 30%
const ratio = (1 - virtualData / realData) * 100
return `${ratio.toFixed(2)}%`
}
// 否则返回 0%
return '0%'
}
},
{
prop: 'duration_months',
label: '有效期',
width: 100,
formatter: (row: PackageResponse) => `${row.duration_months}`
},
{
prop: 'calendar_type',
label: '套餐周期类型',
width: 120,
formatter: (row: PackageResponse) => {
const map: Record<string, string> = {
natural_month: '自然月',
by_day: '按天'
}
return map[row.calendar_type] || row.calendar_type || '-'
}
},
{
prop: 'data_reset_cycle',
label: '流量重置周期',
width: 120,
formatter: (row: PackageResponse) => {
const map: Record<string, string> = {
daily: '每日',
weekly: '每周',
monthly: '每月',
none: '不重置'
}
return map[row.data_reset_cycle] || row.data_reset_cycle || '-'
}
},
{
prop: 'expiry_base',
label: '到期时间基准',
width: 120,
formatter: (row: PackageResponse) => {
const map: Record<string, string> = {
from_activation: '实名激活',
from_purchase: '购买时'
}
return map[row.expiry_base] || row.expiry_base || '-'
}
},
{
prop: 'cost_price',
label: '成本价',
width: 100,
formatter: (row: PackageResponse) => `¥${((row.cost_price ?? 0) / 100).toFixed(2)}`
},
{
prop: 'suggested_retail_price',
label: '建议售价',
width: 100,
formatter: (row: PackageResponse) =>
row.suggested_retail_price
? `¥${((row.suggested_retail_price ?? 0) / 100).toFixed(2)}`
: '-'
},
{
prop: 'shelf_status',
label: '上架状态',
width: 100,
formatter: (row: PackageResponse) => {
return h(ElSwitch, {
modelValue: row.shelf_status === 1,
activeText: getShelfStatusText(1),
inactiveText: getShelfStatusText(2),
inlinePrompt: true,
disabled: !hasAuth('package:update_away'),
'onUpdate:modelValue': (val: string | number | boolean) =>
handleShelfStatusChange(row, val ? 1 : 2)
})
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: PackageResponse) => {
const frontendStatus = apiStatusToFrontend(row.status ?? 0)
return h(ElSwitch, {
modelValue: frontendStatus,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
disabled: !hasAuth('package:update_status'),
'onUpdate:modelValue': (val: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: PackageResponse) => formatDateTime(row.created_at)
}
])
// 操作列配置
const getActions = (row: PackageResponse) => {
const actions: any[] = []
if (hasAuth('package:edit')) {
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
}
// 只有代理账号(user_type=3)才显示修改零售价
if (hasAuth('package:update_retail_price') && userStore.getUserInfo.user_type === 3) {
actions.push({
label: '修改零售价',
handler: () => showRetailPriceDialog(row),
type: 'primary'
})
}
if (hasAuth('package:delete')) {
actions.push({ label: '删除', handler: () => deletePackage(row), type: 'danger' })
}
return actions
}
// 监听虚流量开关变化,关闭时重置虚流量额度
watch(
() => form.enable_virtual_data,
(enabled) => {
if (!enabled) {
form.virtual_data_mb = 0
}
}
)
// 监听真流量额度变化,如果虚流量超过真流量则触发验证
watch(
() => form.real_data_mb,
() => {
if (form.enable_virtual_data && formRef.value) {
formRef.value.validateField('virtual_data_mb')
}
}
)
// 监听流量重置周期变化
watch(
() => form.data_reset_cycle,
(cycle) => {
// 流量重置周期变化时不需要清空套餐天数套餐天数只受calendar_type控制
}
)
// 监听套餐周期类型变化,不是按天时清空套餐天数
watch(
() => form.calendar_type,
(type) => {
if (type !== 'by_day') {
form.duration_days = undefined
}
}
)
onMounted(() => {
getTableData()
})
// 监听对话框打开,加载系列选项
watch(dialogVisible, (visible) => {
if (visible) {
loadSeriesOptions()
}
})
// 加载系列选项(用于新增/编辑对话框,默认加载10条)
const loadSeriesOptions = async (seriesName?: string) => {
seriesLoading.value = true
try {
const params: any = {
page: 1,
page_size: 10,
status: 1
}
if (seriesName) {
params.series_name = seriesName
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
seriesOptions.value = res.data.items
}
} catch (error) {
console.error('加载系列选项失败:', error)
} finally {
seriesLoading.value = false
}
}
// 加载搜索栏系列选项(默认加载10条)
const loadSearchSeriesOptions = async (seriesName?: string) => {
try {
const params: any = {
page: 1,
page_size: 10,
status: 1
}
if (seriesName) {
params.series_name = seriesName
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
searchSeriesOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏系列选项失败:', error)
}
}
// 搜索系列(用于新增/编辑对话框)
const searchSeries = (query: string) => {
if (query) {
loadSeriesOptions(query)
} else {
loadSeriesOptions()
}
}
// 搜索系列(用于搜索栏)
const handleSearchSeries = (query: string) => {
if (query) {
loadSearchSeriesOptions(query)
} else {
loadSearchSeriesOptions()
}
}
// 获取套餐列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.page_size,
package_name: searchForm.package_name || undefined,
series_id: searchForm.series_id || undefined,
package_type: searchForm.package_type || undefined,
shelf_status: searchForm.shelf_status || undefined,
status: searchForm.status || undefined
}
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
packageList.value = res.data.items
pagination.total = res.data.total
}
} catch (error) {
console.error(error)
console.log('获取套餐列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示新增/编辑对话框
const showDialog = async (type: string, row?: PackageResponse) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
try {
const res = await PackageManageService.getPackageDetail(row.id)
if (res.code === 0 && res.data) {
const data = res.data
form.id = data.id
form.package_code = data.package_code
form.package_name = data.package_name
form.series_id = data.series_id
form.package_type = data.package_type
form.calendar_type = data.calendar_type || undefined
form.duration_days = data.duration_days
form.duration_months = data.duration_months
form.data_reset_cycle = data.data_reset_cycle || undefined
form.enable_virtual_data = data.enable_virtual_data || false
form.expiry_base = data.expiry_base || 'from_activation'
form.real_data_mb = data.real_data_mb || 0
form.virtual_data_mb = data.virtual_data_mb || 0
// MB 转换为 GB 显示
realDataGb.value = form.real_data_mb ? Number((form.real_data_mb / 1024).toFixed(2)) : 0
// 从虚流量额度反推比例:比例 = (1 - 虚流量/真流量) × 100%
if (form.real_data_mb > 0 && form.virtual_data_mb > 0) {
const ratio = (1 - form.virtual_data_mb / form.real_data_mb) * 100
virtualRatioPercent.value = Number(ratio.toFixed(2))
} else {
virtualRatioPercent.value = null
}
form.cost_price = (data.cost_price ?? 0) / 100 // 分转换为元显示
form.suggested_retail_price = data.suggested_retail_price
? data.suggested_retail_price / 100
: undefined
form.description = data.description || ''
}
} catch (error) {
console.error('获取套餐详情失败:', error)
ElMessage.error('获取套餐详情失败')
}
} else {
form.id = 0
form.package_code = ''
form.package_name = ''
form.series_id = undefined
form.package_type = ''
form.calendar_type = undefined
form.duration_days = undefined
form.duration_months = 1
form.data_reset_cycle = undefined
form.enable_virtual_data = false
form.expiry_base = 'from_activation'
form.real_data_mb = 0
form.virtual_data_mb = 0
// 重置 GB 值和比例
realDataGb.value = 0
virtualRatioPercent.value = null
form.cost_price = 0
form.suggested_retail_price = undefined
form.description = ''
}
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 生成套餐编码
const handleGeneratePackageCode = () => {
form.package_code = generatePackageCode()
// 生成编码后清除该字段的验证错误
nextTick(() => {
formRef.value?.clearValidate('package_code')
})
ElMessage.success('编码生成成功')
}
// 处理弹窗关闭事件
const handleDialogClosed = () => {
// 清除表单验证状态
formRef.value?.clearValidate()
// 重置表单数据
form.id = 0
form.package_code = ''
form.package_name = ''
form.series_id = undefined
form.package_type = ''
form.calendar_type = undefined
form.duration_days = undefined
form.duration_months = 1
form.data_reset_cycle = undefined
form.enable_virtual_data = false
form.expiry_base = 'from_activation'
form.real_data_mb = 0
form.virtual_data_mb = 0
// 重置 GB 值和比例
realDataGb.value = 0
virtualRatioPercent.value = null
form.cost_price = 0
form.suggested_retail_price = undefined
form.description = ''
}
// 删除套餐
const deletePackage = (row: PackageResponse) => {
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await PackageManageService.deletePackage(row.id)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
// 将元转换为分提交给后端
const costPriceInCents = Math.round(form.cost_price * 100)
const suggestedRetailPriceInCents = form.suggested_retail_price
? Math.round(form.suggested_retail_price * 100)
: undefined
const data: any = {
package_code: form.package_code,
package_name: form.package_name,
package_type: form.package_type,
duration_months: form.duration_months,
cost_price: costPriceInCents
}
// 可选字段
if (form.series_id !== undefined && form.series_id !== null) {
data.series_id = form.series_id
}
if (form.calendar_type) {
data.calendar_type = form.calendar_type
}
if (form.data_reset_cycle) {
data.data_reset_cycle = form.data_reset_cycle
}
// 只有 calendar_type 为 by_day 时才传 duration_days
if (form.calendar_type === 'by_day' && form.duration_days !== undefined) {
data.duration_days = form.duration_days
}
if (form.enable_virtual_data !== undefined) {
data.enable_virtual_data = form.enable_virtual_data
}
if (form.expiry_base) {
data.expiry_base = form.expiry_base
}
if (suggestedRetailPriceInCents !== undefined && suggestedRetailPriceInCents !== null) {
data.suggested_retail_price = suggestedRetailPriceInCents
}
if (form.real_data_mb !== undefined && form.real_data_mb !== null) {
data.real_data_mb = form.real_data_mb
}
if (form.virtual_data_mb !== undefined && form.virtual_data_mb !== null) {
data.virtual_data_mb = form.virtual_data_mb
}
if (dialogType.value === 'add') {
await PackageManageService.createPackage(data)
ElMessage.success('新增成功')
} else {
await PackageManageService.updatePackage(form.id, data)
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 状态切换
const handleStatusChange = async (row: PackageResponse, newFrontendStatus: number) => {
const oldStatus = row.status
const newApiStatus = frontendStatusToApi(newFrontendStatus)
row.status = newApiStatus
try {
await PackageManageService.updatePackageStatus(row.id, newApiStatus)
ElMessage.success('状态切换成功')
} catch (error) {
row.status = oldStatus
console.error(error)
}
}
// 上架状态切换
const handleShelfStatusChange = async (row: PackageResponse, newShelfStatus: number) => {
const oldShelfStatus = row.shelf_status
row.shelf_status = newShelfStatus
try {
await PackageManageService.updatePackageShelfStatus(row.id, newShelfStatus)
ElMessage.success('上架状态切换成功')
} catch (error) {
row.shelf_status = oldShelfStatus
console.error(error)
}
}
// 查看详情
const handleViewDetail = (row: PackageResponse) => {
router.push(`${RoutesAlias.PackageDetail}/${row.id}`)
}
// 处理名称点击
const handleNameClick = (row: PackageResponse) => {
if (hasAuth('package:detail')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 显示零售价修改对话框
const showRetailPriceDialog = (row: PackageResponse) => {
retailPriceForm.id = row.id
retailPriceForm.package_name = row.package_name
// 将分转换为元显示
retailPriceForm.retail_price = row.suggested_retail_price ? row.suggested_retail_price / 100 : 0
retailPriceDialogVisible.value = true
}
// 确认修改零售价
const handleConfirmUpdateRetailPrice = () => {
retailPriceFormRef.value?.validate(async (valid) => {
if (!valid) return
retailPriceLoading.value = true
try {
// 将元转换为分
const priceInCents = Math.round(retailPriceForm.retail_price * 100)
const res = await PackageManageService.updateRetailPrice(retailPriceForm.id, priceInCents)
if (res.code === 0) {
ElMessage.success('零售价修改成功')
retailPriceDialogVisible.value = false
await getTableData()
}
} catch (error: any) {
console.error('修改零售价失败:', error)
} finally {
retailPriceLoading.value = false
}
})
}
// 关闭零售价对话框
const handleCloseRetailPriceDialog = () => {
retailPriceDialogVisible.value = false
retailPriceFormRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.dialog-footer {
text-align: right;
}
</style>