fix: 套餐
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m14s

This commit is contained in:
sexygoat
2026-04-28 18:40:18 +08:00
parent 55515410ae
commit b6625748ac
5 changed files with 320 additions and 60 deletions

View File

@@ -139,11 +139,17 @@
:key="pkg.id"
:label="`${pkg.package_name} (¥${((pkg.cost_price || 0) / 100).toFixed(2)})`"
:value="pkg.id"
:disabled="pkg.is_gift"
>
<div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }}
<template v-if="pkg.is_gift">
<el-tag type="warning" size="small">赠送</el-tag>
</template>
<template v-else>
¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }}
</template>
</span>
</div>
</ElOption>
@@ -158,14 +164,24 @@
该系列暂无可用套餐
</div>
</template>
<template v-else-if="selectedPackageIsGift">
<div style="color: var(--el-color-warning); font-size: 12px; margin-top: 4px">
提示: 赠送套餐只能使用线下支付方式
</div>
</template>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElSelect
v-model="createForm.payment_method"
placeholder="请选择支付方式"
style="width: 100%"
:disabled="selectedPackageIsGift"
>
<ElOption label="钱包支付" value="wallet" />
<ElOption
v-if="!selectedPackageIsGift"
label="钱包支付"
value="wallet"
/>
<!-- 只有平台用户(user_type 1:超级管理员, 2:平台用户)才显示线下支付选项 -->
<ElOption
v-if="userStore.info.user_type === 1 || userStore.info.user_type === 2"
@@ -713,6 +729,13 @@
return false
})
// 检查是否选择了赠送套餐
const selectedPackageIsGift = computed(() => {
if (!createForm.package_id) return false
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === createForm.package_id)
return selectedPackage?.is_gift || false
})
// IoT卡选择变化时根据series_id加载套餐列表
const handleIotCardChange = (cardId: number | null) => {
if (!cardId) {
@@ -997,6 +1020,19 @@
}
)
// 当套餐切换时,如果选择的是赠送套餐,自动切换到线下支付
watch(
() => createForm.package_id,
(packageId) => {
if (packageId) {
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId)
if (selectedPackage?.is_gift) {
createForm.payment_method = 'offline'
}
}
}
)
onMounted(() => {
getTableData()
})
@@ -1170,6 +1206,18 @@
return
}
// 检查是否是赠送套餐
const selectedPackage = packageOptions.value.find(
(pkg) => pkg.id === createForm.package_id
)
const isGiftPackage = selectedPackage?.is_gift || false
// 线下支付时,支付凭证为必填
if (createForm.payment_method === 'offline' && !createForm.payment_voucher_key) {
ElMessage.error('线下支付必须上传支付凭证')
return
}
// 创建订单(后端接收数组,前端单选转为单元素数组)
const data: CreateOrderRequest = {
identifier,

View File

@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
@@ -35,11 +35,15 @@
import type { PackageResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { getStatusLabel, getShelfStatusText } from '@/config/constants'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'PackageDetail' })
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const isAgentAccount = computed(() => userStore.info.user_type === 3)
const loading = ref(false)
const detailData = ref<PackageResponse | null>(null)
@@ -62,6 +66,62 @@
return typeMap[data.package_type] || data.package_type
}
},
{
label: '成本价',
formatter: (_, data) => {
return `¥${(data.cost_price / 100).toFixed(2)}`
}
},
{
label: '建议售价',
formatter: (_, data) => {
if (data.suggested_retail_price === null || data.suggested_retail_price === undefined)
return '-'
return `¥${(data.suggested_retail_price / 100).toFixed(2)}`
}
},
...(isAgentAccount.value
? [
{
label: '代理零售价',
formatter: (_, data) => {
if (data.retail_price === null || data.retail_price === undefined) return '-'
return `¥${(data.retail_price / 100).toFixed(2)}`
}
},
{
label: '利润空间',
formatter: (_, data) => {
if (data.profit_margin === null || data.profit_margin === undefined) return '-'
return `¥${(data.profit_margin / 100).toFixed(2)}`
}
}
]
: [
{
label: '价格配置状态',
formatter: (_, data) => {
return data.price_config_status_name || '-'
}
},
{
label: '生效零售价',
formatter: (_, data) => {
if (
data.effective_retail_price === null ||
data.effective_retail_price === undefined
)
return '-'
return `¥${(data.effective_retail_price / 100).toFixed(2)}`
}
},
{
label: '是否赠送',
formatter: (_, data) => {
return data.is_gift ? '是' : '否'
}
}
]),
{
label: '套餐时长',
formatter: (_, data) => {
@@ -152,30 +212,6 @@
}
]
},
{
title: '价格信息',
fields: [
{
label: '成本价',
formatter: (_, data) => {
return `¥${(data.cost_price / 100).toFixed(2)}`
}
},
{
label: '建议售价',
formatter: (_, data) => {
return `¥${(data.suggested_retail_price / 100).toFixed(2)}`
}
},
{
label: '利润空间',
formatter: (_, data) => {
if (data.profit_margin === null || data.profit_margin === undefined) return '-'
return `¥${(data.profit_margin / 100).toFixed(2)}`
}
}
]
},
{
title: '佣金配置',
fields: [

View File

@@ -35,7 +35,8 @@
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:inlineActionsCount="2"
:actionsWidth="120"
:inlineActionsCount="1"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
@@ -282,11 +283,30 @@
:controls="false"
style="width: 100%"
placeholder="请输入建议零售价"
:disabled="form.is_gift"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="设为赠送套餐">
<div class="gift-switch-wrapper">
<ElSwitch v-model="form.is_gift" @change="handleIsGiftChange" />
<div v-if="form.is_gift" class="gift-hint">
<el-icon class="gift-hint-icon"><WarningFilled /></el-icon>
<span>赠送套餐的建议零售价将自动设为 0</span>
</div>
<div v-else class="gift-hint gift-hint--info">
<el-icon class="gift-hint-icon"><InfoFilled /></el-icon>
<span>开启后该套餐将作为赠送套餐无法用于常规销售</span>
</div>
</div>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="24">
<ElFormItem label="套餐描述" prop="description">
@@ -368,6 +388,7 @@
import { useRouter } from 'vue-router'
import { PackageManageService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElInputNumber } from 'element-plus'
import { WarningFilled, InfoFilled } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { PackageResponse, SeriesSelectOption } from '@/types/api'
import type { SearchFormItem } from '@/types'
@@ -396,6 +417,8 @@
const userStore = useUserStore()
const router = useRouter()
const isAgentAccount = computed(() => userStore.info.user_type === 3)
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
@@ -512,6 +535,18 @@
{ label: '套餐名称', prop: 'package_name' },
{ label: '所属系列', prop: 'series_name' },
{ label: '套餐类型', prop: 'package_type' },
{ label: '成本价', prop: 'cost_price' },
{ label: '建议售价', prop: 'suggested_retail_price' },
...(isAgentAccount.value
? [
{ label: '代理零售价', prop: 'retail_price' },
{ label: '利润空间', prop: 'profit_margin' }
]
: [
{ label: '价格配置状态', prop: 'price_config_status_name' },
{ label: '生效零售价', prop: 'effective_retail_price' },
{ label: '是否赠送', prop: 'is_gift' }
]),
{ label: '套餐周期类型', prop: 'calendar_type' },
{ label: '有效期', prop: 'duration_months' },
{ label: '真流量', prop: 'real_data_mb' },
@@ -546,8 +581,6 @@
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' },
@@ -571,7 +604,27 @@
{ required: true, message: '请输入有效期', trigger: 'blur' },
{ type: 'number', min: 1, max: 120, message: '有效期范围 1-120 月', trigger: 'blur' }
],
cost_price: [{ required: true, message: '请输入成本价', trigger: 'blur' }]
cost_price: [{ required: true, message: '请输入成本价', trigger: 'blur' }],
suggested_retail_price: [
{
validator: (_rule: any, value: any, callback: any) => {
if (form.is_gift) {
if (value !== 0 && value !== undefined && value !== null) {
callback(new Error('赠送套餐的建议零售价必须为0'))
} else {
callback()
}
} else {
if (value === 0) {
callback(new Error('普通套餐的建议零售价不能为0'))
} else {
callback()
}
}
},
trigger: 'blur'
}
]
}
// 如果启用虚流量,则虚流量额度为必填,并且不能超过真流量额度
@@ -619,12 +672,14 @@
virtual_data_mb: 0,
cost_price: 0,
suggested_retail_price: undefined,
is_gift: false,
description: ''
})
// GB 输入值(用于界面显示)
const realDataGb = ref<number>(0)
const virtualRatioPercent = ref<number | null>(null) // 虚流量比例(百分比)
const originalSuggestedRetailPrice = ref<number | null>(null) // 编辑时原始建议零售价
// 计算得出的虚流量GB值
const calculatedVirtualDataGb = computed(() => {
@@ -709,6 +764,68 @@
)
}
},
{
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 !== null && row.suggested_retail_price !== undefined
? `¥${((row.suggested_retail_price ?? 0) / 100).toFixed(2)}`
: '-'
},
...(isAgentAccount.value
? [
{
prop: 'retail_price',
label: '代理零售价',
width: 100,
formatter: (row: PackageResponse) =>
row.retail_price !== null && row.retail_price !== undefined
? `¥${((row.retail_price ?? 0) / 100).toFixed(2)}`
: '-'
},
{
prop: 'profit_margin',
label: '利润空间',
width: 100,
formatter: (row: PackageResponse) =>
row.profit_margin !== null && row.profit_margin !== undefined
? `¥${((row.profit_margin ?? 0) / 100).toFixed(2)}`
: '-'
}
]
: [
{
prop: 'price_config_status_name',
label: '价格配置状态',
width: 120,
formatter: (row: PackageResponse) => row.price_config_status_name || '-'
},
{
prop: 'effective_retail_price',
label: '生效零售价',
width: 100,
formatter: (row: PackageResponse) =>
row.effective_retail_price !== null && row.effective_retail_price !== undefined
? `¥${((row.effective_retail_price ?? 0) / 100).toFixed(2)}`
: '-'
},
{
prop: 'is_gift',
label: '是否赠送',
width: 100,
formatter: (row: PackageResponse) =>
h(ElTag, { type: row.is_gift ? 'warning' : 'info', size: 'small' }, () =>
row.is_gift ? '是' : '否'
)
}
]),
{
prop: 'shelf_status',
label: '上架状态',
@@ -834,21 +951,6 @@
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: 'created_at',
label: '创建时间',
@@ -919,6 +1021,15 @@
}
)
// 监听 is_gift 变化,自动设置 suggested_retail_price
const handleIsGiftChange = (isGift: boolean) => {
if (isGift) {
form.suggested_retail_price = 0
} else {
form.suggested_retail_price = undefined
}
}
onMounted(() => {
getTableData()
})
@@ -1048,7 +1159,6 @@
// 显示新增/编辑对话框
const showDialog = async (type: string, row?: PackageResponse) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
@@ -1082,7 +1192,12 @@
form.suggested_retail_price = data.suggested_retail_price
? data.suggested_retail_price / 100
: undefined
originalSuggestedRetailPrice.value = data.suggested_retail_price
form.is_gift = data.is_gift || false
form.description = data.description || ''
// 数据填充完成后再显示对话框
dialogVisible.value = true
}
} catch (error) {
console.error('获取套餐详情失败:', error)
@@ -1108,6 +1223,8 @@
form.cost_price = 0
form.suggested_retail_price = undefined
form.description = ''
dialogVisible.value = true
}
// 重置表单验证状态
@@ -1149,6 +1266,8 @@
virtualRatioPercent.value = null
form.cost_price = 0
form.suggested_retail_price = undefined
originalSuggestedRetailPrice.value = null
form.is_gift = false
form.description = ''
}
@@ -1183,16 +1302,14 @@
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
cost_price: costPriceInCents,
is_gift: form.is_gift
}
// 可选字段
@@ -1215,8 +1332,18 @@
if (form.expiry_base) {
data.expiry_base = form.expiry_base
}
if (suggestedRetailPriceInCents !== undefined && suggestedRetailPriceInCents !== null) {
data.suggested_retail_price = suggestedRetailPriceInCents
// 处理建议零售价
if (form.suggested_retail_price !== undefined && form.suggested_retail_price !== null) {
data.suggested_retail_price = Math.round(form.suggested_retail_price * 100)
}
// 编辑模式下,如果清空了建议零售价,使用 clear_suggested_retail_price
if (dialogType.value === 'edit') {
const wasNull = originalSuggestedRetailPrice.value === null
const isNowNull =
form.suggested_retail_price === undefined || form.suggested_retail_price === null
if (!wasNull && isNowNull) {
data.clear_suggested_retail_price = true
}
}
if (form.real_data_mb !== undefined && form.real_data_mb !== null) {
data.real_data_mb = form.real_data_mb
@@ -1290,8 +1417,8 @@
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
// 零售价值来自代理零售价
retailPriceForm.retail_price = row.retail_price ? row.retail_price / 100 : 0
retailPriceDialogVisible.value = true
}
@@ -1329,4 +1456,32 @@
.dialog-footer {
text-align: right;
}
.gift-switch-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.gift-hint {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
white-space: nowrap;
background-color: var(--el-color-warning-light-9);
color: var(--el-color-warning);
&--info {
background-color: var(--el-fill-color-light);
color: var(--el-text-color-secondary);
}
.gift-hint-icon {
flex-shrink: 0;
}
}
</style>

View File

@@ -1359,7 +1359,8 @@
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
packageOptions.value = res.data.items
// 过滤掉赠送套餐
packageOptions.value = res.data.items.filter((pkg) => !pkg.is_gift)
}
} catch (error) {
console.error('加载套餐选项失败:', error)
@@ -1999,6 +2000,16 @@
// 可选:套餐配置(将元转换为分)
if (form.packages.length > 0) {
// 验证是否包含赠送套餐
for (const pkg of form.packages) {
const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id)
if (pkgInfo?.is_gift) {
ElMessage.error(`套餐 ${getPackageName(pkg.package_id)} 是赠送套餐,不能参与授权`)
submitLoading.value = false
return
}
}
// 验证成本价是否在允许范围内
for (const pkg of form.packages) {
const costPrice = pkg.cost_price ?? 0
@@ -2138,7 +2149,8 @@
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
availablePackages.value = res.data.items
// 过滤掉赠送套餐
availablePackages.value = res.data.items.filter((pkg) => !pkg.is_gift)
}
} catch (error) {
console.error('加载套餐选项失败:', error)