This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
import { PackageManageService } from '@/api/modules'
|
||||
import type { PackageResponse } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getStatusLabel, getShelfStatusText } from '@/config/constants'
|
||||
import { getShelfStatusText } from '@/config/constants'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'PackageDetail' })
|
||||
@@ -103,7 +103,16 @@
|
||||
{
|
||||
label: '价格配置状态',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return data.price_config_status_name || '-'
|
||||
const statusMap: Record<number, string> = {
|
||||
0: '未配置',
|
||||
1: '赠送 0 价',
|
||||
2: '已配置非 0'
|
||||
}
|
||||
return (
|
||||
data.price_config_status_name ||
|
||||
statusMap[data.price_config_status ?? -1] ||
|
||||
'-'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -179,7 +188,9 @@
|
||||
{
|
||||
label: '状态',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return getStatusLabel(data.status ?? 0)
|
||||
if (data.status === 1) return '启用'
|
||||
if (data.status === 0 || data.status === 2) return '禁用'
|
||||
return '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -224,6 +235,13 @@
|
||||
}
|
||||
return `${virtualDataMb} MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '虚流量比例',
|
||||
formatter: (_: unknown, data: PackageResponse) =>
|
||||
data.virtual_ratio === null || data.virtual_ratio === undefined
|
||||
? '-'
|
||||
: Number(data.virtual_ratio).toFixed(2)
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
<ElButton type="primary" @click="showDialog('add')" v-permission="'package:add'"
|
||||
>新增套餐</ElButton
|
||||
>
|
||||
<ElButton v-if="hasAuth('package:export')" @click="exportDialogVisible = true">
|
||||
导出
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -45,6 +48,14 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ExportTaskCreateDialog
|
||||
v-model="exportDialogVisible"
|
||||
scene="package"
|
||||
:query="exportQuery"
|
||||
confirm-permission="package:export"
|
||||
title="导出套餐"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -240,11 +251,12 @@
|
||||
>
|
||||
<div>
|
||||
缩减比例:{{ realDataGb }}×(1-{{ virtualRatioPercent }}%) =
|
||||
{{ calculatedVirtualDataGb }}GB
|
||||
{{ formatTwoDecimals(calculatedVirtualDataGb) }}GB
|
||||
</div>
|
||||
<div v-if="calculatedVirtualDataGb > 0">
|
||||
增长比例:{{ realDataGb }}GB/{{ calculatedVirtualDataGb }}GB =
|
||||
{{ calculatedGrowthRatio * 100 }}%
|
||||
增长比例:{{ realDataGb }}GB/{{
|
||||
formatTwoDecimals(calculatedVirtualDataGb)
|
||||
}}GB = {{ formatTwoDecimals(calculatedGrowthRatio * 100) }}%
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
@@ -407,10 +419,10 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
import {
|
||||
CommonStatus,
|
||||
getStatusText,
|
||||
frontendStatusToApi,
|
||||
apiStatusToFrontend,
|
||||
PACKAGE_TYPE_OPTIONS,
|
||||
getPackageTypeLabel,
|
||||
@@ -434,6 +446,7 @@
|
||||
const shouldHideVirtualTrafficColumns = computed(() => [3, 4].includes(currentUserType.value))
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const exportDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const seriesLoading = ref(false)
|
||||
@@ -589,6 +602,7 @@
|
||||
]),
|
||||
{ label: '套餐周期类型', prop: 'calendar_type' },
|
||||
{ label: '有效期', prop: 'duration_months' },
|
||||
{ label: '套餐天数', prop: 'duration_days' },
|
||||
{ label: '总流量', prop: 'real_data_mb' },
|
||||
...(!shouldHideVirtualTrafficColumns.value
|
||||
? [
|
||||
@@ -607,8 +621,8 @@
|
||||
formatter: (row: any) => {
|
||||
const map: Record<string, string> = {
|
||||
daily: '每日',
|
||||
weekly: '每周',
|
||||
monthly: '每月',
|
||||
yearly: '每年',
|
||||
none: '不重置'
|
||||
}
|
||||
return map[row.data_reset_cycle] || row.data_reset_cycle || '-'
|
||||
@@ -632,8 +646,8 @@
|
||||
formatter: (row: PackageResponse) => row.expiry_base_override_name || '跟随套餐默认'
|
||||
},
|
||||
{ label: '最终生效条件', prop: 'effective_expiry_base_name' },
|
||||
...(canUpdatePackageShelfStatus ? [{ label: '上架状态', prop: 'shelf_status' }] : []),
|
||||
...(canUpdatePackageStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||
{ label: '上架状态', prop: 'shelf_status' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '更新时间', prop: 'updated_at' }
|
||||
]
|
||||
@@ -749,6 +763,8 @@
|
||||
return Number((realDataGb.value / calculatedVirtualDataGb.value).toFixed(2))
|
||||
})
|
||||
|
||||
const formatTwoDecimals = (value: number) => value.toFixed(2)
|
||||
|
||||
// GB 转 MB 处理
|
||||
const handleRealDataChange = (value: number | null | undefined) => {
|
||||
if (value === null || value === undefined) {
|
||||
@@ -902,7 +918,17 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
: [
|
||||
{
|
||||
prop: 'shelf_status',
|
||||
label: '上架状态',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) =>
|
||||
h(ElTag, { type: row.shelf_status === 1 ? 'success' : 'info', size: 'small' }, () =>
|
||||
getShelfStatusText(row.shelf_status ?? 0)
|
||||
)
|
||||
}
|
||||
]),
|
||||
...(canUpdatePackageStatus
|
||||
? [
|
||||
{
|
||||
@@ -924,7 +950,24 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
: [
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => {
|
||||
const frontendStatus = apiStatusToFrontend(row.status ?? 0)
|
||||
return h(
|
||||
ElTag,
|
||||
{
|
||||
type: frontendStatus === CommonStatus.ENABLED ? 'success' : 'danger',
|
||||
size: 'small'
|
||||
},
|
||||
() => getStatusText(frontendStatus)
|
||||
)
|
||||
}
|
||||
}
|
||||
]),
|
||||
{
|
||||
prop: 'real_data_mb',
|
||||
label: '总流量',
|
||||
@@ -959,28 +1002,36 @@
|
||||
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%'
|
||||
}
|
||||
formatter: (row: PackageResponse) =>
|
||||
row.virtual_ratio === null || row.virtual_ratio === undefined
|
||||
? '-'
|
||||
: formatTwoDecimals(Number(row.virtual_ratio))
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
prop: 'enable_virtual_data',
|
||||
label: '启用虚流量',
|
||||
width: 110,
|
||||
formatter: (row: PackageResponse) => (row.enable_virtual_data ? '是' : '否')
|
||||
},
|
||||
{
|
||||
prop: 'duration_months',
|
||||
label: '有效期',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => `${row.duration_months}月`
|
||||
},
|
||||
{
|
||||
prop: 'duration_days',
|
||||
label: '套餐天数',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) =>
|
||||
row.calendar_type === 'by_day' &&
|
||||
row.duration_days !== null &&
|
||||
row.duration_days !== undefined
|
||||
? `${row.duration_days}天`
|
||||
: '-'
|
||||
},
|
||||
{
|
||||
prop: 'calendar_type',
|
||||
label: '套餐周期类型',
|
||||
@@ -1002,8 +1053,8 @@
|
||||
formatter: (row: PackageResponse) => {
|
||||
const map: Record<string, string> = {
|
||||
daily: '每日',
|
||||
weekly: '每周',
|
||||
monthly: '每月',
|
||||
yearly: '每年',
|
||||
none: '不重置'
|
||||
}
|
||||
const dataResetCycle = row.data_reset_cycle
|
||||
@@ -1025,11 +1076,33 @@
|
||||
return map[expiryBase] || expiryBase
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'default_expiry_base_name',
|
||||
label: '套餐默认生效条件',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'expiry_base_override_name',
|
||||
label: '分配覆盖生效条件',
|
||||
width: 150,
|
||||
formatter: (row: PackageResponse) => row.expiry_base_override_name || '跟随套餐默认'
|
||||
},
|
||||
{
|
||||
prop: 'effective_expiry_base_name',
|
||||
label: '最终生效条件',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: PackageResponse) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: PackageResponse) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1080,7 +1153,7 @@
|
||||
// 监听流量重置周期变化
|
||||
watch(
|
||||
() => form.data_reset_cycle,
|
||||
(cycle) => {
|
||||
() => {
|
||||
// 流量重置周期变化时不需要清空套餐天数,套餐天数只受calendar_type控制
|
||||
}
|
||||
)
|
||||
@@ -1097,7 +1170,7 @@
|
||||
|
||||
// 监听 is_gift 变化,自动设置 suggested_retail_price
|
||||
const handleIsGiftChange = (isGift: string | number | boolean) => {
|
||||
if (Boolean(isGift)) {
|
||||
if (isGift) {
|
||||
form.suggested_retail_price = 0
|
||||
} else {
|
||||
form.suggested_retail_price = undefined
|
||||
@@ -1195,7 +1268,7 @@
|
||||
series_id: searchForm.series_id || undefined,
|
||||
package_type: searchForm.package_type || undefined,
|
||||
shelf_status: searchForm.shelf_status || undefined,
|
||||
status: searchForm.status || undefined
|
||||
status: searchForm.status ?? undefined
|
||||
}
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
@@ -1210,6 +1283,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 导出查询参数:保留 status=0(禁用)等有效零值
|
||||
const exportQuery = computed(() => {
|
||||
const query: Record<string, unknown> = {
|
||||
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
|
||||
}
|
||||
|
||||
Object.keys(query).forEach((key) => {
|
||||
if (query[key] === undefined || query[key] === null || query[key] === '') {
|
||||
delete query[key]
|
||||
}
|
||||
})
|
||||
|
||||
return query
|
||||
})
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
@@ -1387,7 +1479,6 @@
|
||||
const costPriceInCents = Math.round(form.cost_price * 100)
|
||||
|
||||
const data: any = {
|
||||
package_code: form.package_code,
|
||||
package_name: form.package_name,
|
||||
package_type: form.package_type,
|
||||
duration_months: form.duration_months,
|
||||
@@ -1395,8 +1486,12 @@
|
||||
is_gift: form.is_gift
|
||||
}
|
||||
|
||||
if (dialogType.value === 'add') {
|
||||
data.package_code = form.package_code
|
||||
}
|
||||
|
||||
// 可选字段
|
||||
if (form.series_id !== undefined && form.series_id !== null) {
|
||||
if (form.series_id !== undefined) {
|
||||
data.series_id = form.series_id
|
||||
}
|
||||
if (form.calendar_type) {
|
||||
@@ -1458,7 +1553,7 @@
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: PackageResponse, newFrontendStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
const newApiStatus = frontendStatusToApi(newFrontendStatus)
|
||||
const newApiStatus = newFrontendStatus
|
||||
row.status = newApiStatus
|
||||
try {
|
||||
await PackageManageService.updatePackageStatus(row.id, newApiStatus)
|
||||
|
||||
@@ -398,7 +398,6 @@
|
||||
getStatusText,
|
||||
frontendStatusToApi,
|
||||
apiStatusToFrontend,
|
||||
STATUS_SELECT_OPTIONS,
|
||||
ENABLE_STATUS_OPTIONS,
|
||||
getEnableStatusText
|
||||
} from '@/config/constants'
|
||||
@@ -406,6 +405,11 @@
|
||||
|
||||
defineOptions({ name: 'PackageSeries' })
|
||||
|
||||
const PACKAGE_SERIES_STATUS_SELECT_OPTIONS = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const canUpdatePackageSeriesStatus = hasAuth('package_series:update_status')
|
||||
const router = useRouter()
|
||||
@@ -455,7 +459,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: STATUS_SELECT_OPTIONS
|
||||
options: PACKAGE_SERIES_STATUS_SELECT_OPTIONS
|
||||
}
|
||||
]
|
||||
|
||||
@@ -788,7 +792,7 @@
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
series_name: searchForm.series_name || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
status: searchForm.status ?? undefined,
|
||||
enable_one_time_commission: searchForm.enable_one_time_commission ?? undefined
|
||||
}
|
||||
const res = await PackageSeriesService.getPackageSeries(params)
|
||||
|
||||
@@ -348,7 +348,8 @@
|
||||
GrantPackageItem,
|
||||
GrantPackageInfo,
|
||||
PackageAllocationExpiryBaseOverride,
|
||||
PackageSeriesResponse
|
||||
PackageSeriesResponse,
|
||||
PackageResponse
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getEnableStatusText } from '@/config/constants'
|
||||
@@ -550,23 +551,31 @@
|
||||
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
series_id: detailData.value.series_id
|
||||
}
|
||||
const pageSize = 100
|
||||
const allPackages: PackageResponse[] = []
|
||||
let page = 1
|
||||
let total = 0
|
||||
|
||||
if (packageName) {
|
||||
params.package_name = packageName
|
||||
}
|
||||
do {
|
||||
const params: any = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
series_id: detailData.value.series_id
|
||||
}
|
||||
if (packageName) params.package_name = packageName
|
||||
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
availablePackages.value = mergeGrantPackageCandidates(
|
||||
res.data.items,
|
||||
detailData.value.packages || []
|
||||
)
|
||||
}
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code !== 0) break
|
||||
|
||||
allPackages.push(...(res.data.items || []))
|
||||
total = res.data.total || allPackages.length
|
||||
page += 1
|
||||
} while (allPackages.length < total)
|
||||
|
||||
availablePackages.value = mergeGrantPackageCandidates(
|
||||
allPackages,
|
||||
detailData.value.packages || []
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('加载套餐选项失败:', error)
|
||||
} finally {
|
||||
|
||||
@@ -594,12 +594,16 @@
|
||||
getStatusText,
|
||||
frontendStatusToApi,
|
||||
apiStatusToFrontend,
|
||||
STATUS_SELECT_OPTIONS,
|
||||
getEnableStatusText
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'SeriesGrants' })
|
||||
|
||||
const SERIES_GRANT_STATUS_SELECT_OPTIONS = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const canUpdateSeriesGrantStatus = hasAuth('series_grants:update_status')
|
||||
const router = useRouter()
|
||||
@@ -736,7 +740,7 @@
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: STATUS_SELECT_OPTIONS
|
||||
options: SERIES_GRANT_STATUS_SELECT_OPTIONS
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1074,25 +1078,26 @@
|
||||
const loadPackageOptions = async (packageName?: string) => {
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
const pageSize = 100
|
||||
const allPackages: PackageResponse[] = []
|
||||
let page = 1
|
||||
let total = 0
|
||||
|
||||
// 如果已选择套餐系列,则根据系列ID过滤套餐
|
||||
if (form.series_id) {
|
||||
params.series_id = form.series_id
|
||||
}
|
||||
do {
|
||||
const params: any = { page, page_size: pageSize }
|
||||
if (form.series_id) params.series_id = form.series_id
|
||||
if (packageName) params.package_name = packageName
|
||||
|
||||
if (packageName) {
|
||||
params.package_name = packageName
|
||||
}
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code !== 0) break
|
||||
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
// 过滤掉赠送套餐
|
||||
packageOptions.value = res.data.items.filter((pkg) => !pkg.is_gift)
|
||||
}
|
||||
allPackages.push(...(res.data.items || []))
|
||||
total = res.data.total || allPackages.length
|
||||
page += 1
|
||||
} while (allPackages.length < total)
|
||||
|
||||
// 过滤掉赠送套餐
|
||||
packageOptions.value = allPackages.filter((pkg) => !pkg.is_gift)
|
||||
} catch (error) {
|
||||
console.error('加载套餐选项失败:', error)
|
||||
} finally {
|
||||
@@ -1125,7 +1130,7 @@
|
||||
// 获取套餐名称
|
||||
const getPackageName = (packageId: number) => {
|
||||
const pkg = packageOptions.value.find((p) => p.id === packageId)
|
||||
return pkg ? pkg.package_name : `套餐ID: ${packageId}`
|
||||
return pkg ? pkg.package_name : '套餐名称不可用'
|
||||
}
|
||||
|
||||
const getPackageCostPriceMax = (pkg?: {
|
||||
@@ -1452,7 +1457,7 @@
|
||||
series_id: searchForm.series_id || undefined,
|
||||
allocator_shop_id:
|
||||
searchForm.allocator_shop_id !== undefined ? searchForm.allocator_shop_id : undefined,
|
||||
status: searchForm.status || undefined
|
||||
status: searchForm.status ?? undefined
|
||||
}
|
||||
const res = await ShopSeriesGrantService.getShopSeriesGrants(params)
|
||||
if (res.code === 0) {
|
||||
|
||||
@@ -371,18 +371,20 @@
|
||||
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
series_id: seriesId.value
|
||||
}
|
||||
if (packageName) {
|
||||
params.package_name = packageName
|
||||
}
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
availablePackages.value = mergeGrantPackageCandidates(res.data.items, packageList.value)
|
||||
}
|
||||
const pageSize = 100
|
||||
const allPackages: any[] = []
|
||||
let page = 1
|
||||
let total = 0
|
||||
do {
|
||||
const params: any = { page, page_size: pageSize, series_id: seriesId.value }
|
||||
if (packageName) params.package_name = packageName
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code !== 0) break
|
||||
allPackages.push(...(res.data.items || []))
|
||||
total = res.data.total || allPackages.length
|
||||
page += 1
|
||||
} while (allPackages.length < total)
|
||||
availablePackages.value = mergeGrantPackageCandidates(allPackages, packageList.value)
|
||||
} catch (error) {
|
||||
console.error('加载套餐选项失败:', error)
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user