fix: 代理系列授权店铺
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 2m0s

This commit is contained in:
sexygoat
2026-05-09 16:13:11 +08:00
parent 3c71ae0004
commit 73ce9af11a
26 changed files with 833 additions and 807 deletions

View File

@@ -43,7 +43,9 @@
const router = useRouter()
const userStore = useUserStore()
const isAgentAccount = computed(() => userStore.info.user_type === 3)
const currentUserType = computed(() => Number(userStore.info.user_type ?? 0))
const isAgentAccount = computed(() => currentUserType.value === 3)
const shouldHideVirtualTrafficFields = computed(() => [3, 4].includes(currentUserType.value))
const loading = ref(false)
const detailData = ref<PackageResponse | null>(null)
@@ -58,7 +60,7 @@
{ label: '套餐系列', prop: 'series_name' },
{
label: '套餐类型',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
const typeMap: Record<string, string> = {
formal: '正式套餐',
addon: '附加套餐'
@@ -68,13 +70,13 @@
},
{
label: '成本价',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
return `¥${(data.cost_price / 100).toFixed(2)}`
}
},
{
label: '建议售价',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (data.suggested_retail_price === null || data.suggested_retail_price === undefined)
return '-'
return `¥${(data.suggested_retail_price / 100).toFixed(2)}`
@@ -84,14 +86,14 @@
? [
{
label: '代理零售价',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (data.retail_price === null || data.retail_price === undefined) return '-'
return `¥${(data.retail_price / 100).toFixed(2)}`
}
},
{
label: '利润空间',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (data.profit_margin === null || data.profit_margin === undefined) return '-'
return `¥${(data.profit_margin / 100).toFixed(2)}`
}
@@ -100,13 +102,13 @@
: [
{
label: '价格配置状态',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
return data.price_config_status_name || '-'
}
},
{
label: '生效零售价',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (
data.effective_retail_price === null ||
data.effective_retail_price === undefined
@@ -117,20 +119,20 @@
},
{
label: '是否赠送',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
return data.is_gift ? '是' : '否'
}
}
]),
{
label: '套餐时长',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
return `${data.duration_months} 个月`
}
},
{
label: '套餐周期类型',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (!data.calendar_type) return '-'
const typeMap: Record<string, string> = {
natural_month: '自然月',
@@ -141,14 +143,14 @@
},
{
label: '套餐天数',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (data.calendar_type !== 'by_day' || !data.duration_days) return '-'
return `${data.duration_days}`
}
},
{
label: '流量重置周期',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (!data.data_reset_cycle) return '-'
const cycleMap: Record<string, string> = {
daily: '每日',
@@ -161,20 +163,20 @@
},
{
label: '到期计时基准',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (!data.expiry_base) return '--'
return data.expiry_base === 'from_activation' ? '实名激活时起算' : '购买时起算'
}
},
{
label: '状态',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
return getStatusLabel(data.status ?? 0)
}
},
{
label: '上架状态',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
return getShelfStatusText(data.shelf_status ?? 0)
}
},
@@ -186,30 +188,34 @@
title: '流量配置',
fields: [
{
label: '流量额度',
formatter: (_, data) => {
label: '流量',
formatter: (_: unknown, data: PackageResponse) => {
if (data.real_data_mb >= 1024) {
return `${(data.real_data_mb / 1024).toFixed(2)} GB`
}
return `${data.real_data_mb} MB`
}
},
{
label: '启用虚流量',
formatter: (_, data) => {
return data.enable_virtual_data ? '是' : '否'
}
},
{
label: '虚流量额度',
formatter: (_, data) => {
if (!data.enable_virtual_data) return '-'
if (data.virtual_data_mb >= 1024) {
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB`
}
return `${data.virtual_data_mb} MB`
}
}
...(shouldHideVirtualTrafficFields.value
? []
: [
{
label: '启用虚流量',
formatter: (_: unknown, data: PackageResponse) => {
return data.enable_virtual_data ? '是' : '否'
}
},
{
label: '虚流量额度',
formatter: (_: unknown, data: PackageResponse) => {
if (!data.enable_virtual_data) return '-'
if (data.virtual_data_mb >= 1024) {
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB`
}
return `${data.virtual_data_mb} MB`
}
}
])
]
},
{
@@ -217,7 +223,7 @@
fields: [
{
label: '一次性佣金金额',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (
data.one_time_commission_amount === null ||
data.one_time_commission_amount === undefined
@@ -228,21 +234,21 @@
},
{
label: '当前返佣比例',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (!data.tier_info || !data.tier_info.current_rate) return '-'
return data.tier_info.current_rate
}
},
{
label: '下一档位比例',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (!data.tier_info || !data.tier_info.next_rate) return '-'
return data.tier_info.next_rate
}
},
{
label: '下一档位阈值',
formatter: (_, data) => {
formatter: (_: unknown, data: PackageResponse) => {
if (
!data.tier_info ||
data.tier_info.next_threshold === null ||

View File

@@ -353,14 +353,18 @@
<ElInputNumber
v-model="retailPriceForm.retail_price"
:min="0"
:max="999999"
:max="retailPriceUpperLimit"
:step="0.01"
:precision="2"
placeholder="请输入零售价"
style="width: 100%"
/>
<!--<div style="margin-top: 5px; font-size: 12px; color: #909399">-->
<!-- 成本价{{ formatRetailPriceYuan(retailPriceForm.cost_price) }}单位-->
<!--</div>-->
<div style="margin-top: 5px; font-size: 12px; color: #909399">
单位 | 存储值{{ Math.round(retailPriceForm.retail_price * 100) }}
<!--零售价不能大于建议售价 * 2 = {{ formatRetailPriceYuan(retailPriceUpperLimit) }}-->
销售价格不能大于官方建议销售价格的2倍
</div>
</ElFormItem>
</ElForm>
@@ -417,7 +421,9 @@
const userStore = useUserStore()
const router = useRouter()
const isAgentAccount = computed(() => userStore.info.user_type === 3)
const currentUserType = computed(() => Number(userStore.info.user_type ?? 0))
const isAgentAccount = computed(() => currentUserType.value === 3)
const shouldHideVirtualTrafficColumns = computed(() => [3, 4].includes(currentUserType.value))
const dialogVisible = ref(false)
const loading = ref(false)
@@ -435,16 +441,40 @@
const retailPriceForm = reactive({
id: 0,
package_name: '',
retail_price: 0
retail_price: 0,
cost_price: 0,
suggested_retail_price: 0
})
const retailPriceUpperLimit = computed(() =>
Number((Math.max(retailPriceForm.suggested_retail_price, 0) * 2).toFixed(2))
)
const formatRetailPriceYuan = (price: number | null | undefined) =>
`¥${Number(price ?? 0).toFixed(2)}`
const retailPriceRules = reactive<FormRules>({
retail_price: [
{ required: true, message: '请输入零售价', trigger: 'blur' },
{ type: 'number', message: '零售价必须为数字', trigger: 'blur' }
{ type: 'number', message: '零售价必须为数字', trigger: 'blur' },
{
validator: (_rule: any, value: number, callback: any) => {
if (value === undefined || value === null) {
callback()
return
}
if (value > retailPriceUpperLimit.value) {
callback(
new Error(`零售价不能大于建议售价 * 2 = ${formatRetailPriceYuan(retailPriceUpperLimit.value)}`)
)
return
}
callback()
},
trigger: ['blur', 'change']
}
]
})
// 使用表格右键菜单功能
// 搜索表单初始值
const initialSearchState = {
package_name: '',
@@ -549,9 +579,13 @@
]),
{ label: '套餐周期类型', prop: 'calendar_type' },
{ label: '有效期', prop: 'duration_months' },
{ label: '流量', prop: 'real_data_mb' },
{ label: '虚流量', prop: 'virtual_data_mb' },
{ label: '虚流量比例', prop: 'virtual_ratio' },
{ label: '流量', prop: 'real_data_mb' },
...(!shouldHideVirtualTrafficColumns.value
? [
{ label: '虚流量', prop: 'virtual_data_mb' },
{ label: '虚流量比例', prop: 'virtual_ratio' }
]
: []),
{
label: '启用虚流量',
prop: 'enable_virtual_data',
@@ -692,7 +726,7 @@
})
// GB 转 MB 处理
const handleRealDataChange = (value: number | null) => {
const handleRealDataChange = (value: number | null | undefined) => {
if (value === null || value === undefined) {
form.real_data_mb = 0
return
@@ -703,7 +737,7 @@
}
// 虚流量比例变化处理
const handleVirtualRatioChange = (value: number | null) => {
const handleVirtualRatioChange = (value: number | null | undefined) => {
if (value === null || value === undefined || realDataGb.value === 0) {
form.virtual_data_mb = 0
return
@@ -863,7 +897,7 @@
},
{
prop: 'real_data_mb',
label: '流量',
label: '流量',
width: 120,
formatter: (row: PackageResponse) => {
const mb = row.real_data_mb ?? 0
@@ -873,40 +907,44 @@
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%'
}
},
...(!shouldHideVirtualTrafficColumns.value
? [
{
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: '有效期',
@@ -922,7 +960,9 @@
natural_month: '自然月',
by_day: '按天'
}
return map[row.calendar_type] || row.calendar_type || '-'
const calendarType = row.calendar_type
if (!calendarType) return '-'
return map[calendarType] || calendarType
}
},
{
@@ -936,7 +976,9 @@
monthly: '每月',
none: '不重置'
}
return map[row.data_reset_cycle] || row.data_reset_cycle || '-'
const dataResetCycle = row.data_reset_cycle
if (!dataResetCycle) return '-'
return map[dataResetCycle] || dataResetCycle
}
},
{
@@ -948,7 +990,9 @@
from_activation: '实名激活',
from_purchase: '购买时'
}
return map[row.expiry_base] || row.expiry_base || '-'
const expiryBase = row.expiry_base
if (!expiryBase) return '-'
return map[expiryBase] || expiryBase
}
},
{
@@ -968,7 +1012,7 @@
}
// 只有代理账号(user_type=3)才显示修改零售价
if (hasAuth('package:update_retail_price') && userStore.getUserInfo.user_type === 3) {
if (hasAuth('package:update_retail_price') && isAgentAccount.value) {
actions.push({
label: '修改零售价',
handler: () => showRetailPriceDialog(row),
@@ -1022,8 +1066,8 @@
)
// 监听 is_gift 变化,自动设置 suggested_retail_price
const handleIsGiftChange = (isGift: boolean) => {
if (isGift) {
const handleIsGiftChange = (isGift: string | number | boolean) => {
if (Boolean(isGift)) {
form.suggested_retail_price = 0
} else {
form.suggested_retail_price = undefined
@@ -1189,10 +1233,11 @@
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
originalSuggestedRetailPrice.value = data.suggested_retail_price
form.suggested_retail_price =
data.suggested_retail_price !== null && data.suggested_retail_price !== undefined
? data.suggested_retail_price / 100
: undefined
originalSuggestedRetailPrice.value = data.suggested_retail_price ?? null
form.is_gift = data.is_gift || false
form.description = data.description || ''
@@ -1417,8 +1462,10 @@
const showRetailPriceDialog = (row: PackageResponse) => {
retailPriceForm.id = row.id
retailPriceForm.package_name = row.package_name
retailPriceForm.cost_price = (row.cost_price ?? 0) / 100
retailPriceForm.suggested_retail_price = (row.suggested_retail_price ?? 0) / 100
// 零售价值来自代理零售价
retailPriceForm.retail_price = row.retail_price ? row.retail_price / 100 : 0
retailPriceForm.retail_price = (row.retail_price ?? 0) / 100
retailPriceDialogVisible.value = true
}
@@ -1449,6 +1496,11 @@
const handleCloseRetailPriceDialog = () => {
retailPriceDialogVisible.value = false
retailPriceFormRef.value?.resetFields()
retailPriceForm.id = 0
retailPriceForm.package_name = ''
retailPriceForm.retail_price = 0
retailPriceForm.cost_price = 0
retailPriceForm.suggested_retail_price = 0
}
</script>

View File

@@ -256,13 +256,16 @@
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
<ElInputNumber
v-model="packageForm.cost_price_yuan"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
<div v-if="packageForm.original_cost_price" class="form-tip">
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价:
¥{{ packageForm.original_cost_price.toFixed(2) }}
</div>
</ElFormItem>
</ElForm>
@@ -280,7 +283,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
ElCard,
@@ -335,6 +338,7 @@
package_id?: number
package_name?: string
package_code?: string
original_cost_price?: number
cost_price_yuan: number
}>({
cost_price_yuan: 0
@@ -352,6 +356,26 @@
}))
// 梯度配置表格数据(合并授权数据和系列数据)
watch(
() => packageForm.value.package_id,
(packageId) => {
if (packageDialogType.value !== 'add' || !packageId) return
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
if (!selectedPackage) return
const originalCostPrice =
selectedPackage.cost_price !== undefined && selectedPackage.cost_price !== null
? selectedPackage.cost_price / 100
: 0
packageForm.value.package_name = selectedPackage.package_name
packageForm.value.package_code = selectedPackage.package_code
packageForm.value.original_cost_price = originalCostPrice
packageForm.value.cost_price_yuan = originalCostPrice
}
)
const tierTableData = computed(() => {
if (
!detailData.value?.commission_tiers ||
@@ -435,6 +459,9 @@
packageDialogType.value = 'add'
packageForm.value = {
package_id: undefined,
package_name: undefined,
package_code: undefined,
original_cost_price: undefined,
cost_price_yuan: 0
}
@@ -453,6 +480,7 @@
package_id: row.package_id,
package_name: row.package_name,
package_code: row.package_code,
original_cost_price: row.cost_price / 100,
cost_price_yuan: row.cost_price / 100
}
@@ -581,6 +609,9 @@
const handlePackageDialogClosed = () => {
packageFormRef.value?.resetFields()
packageForm.value = {
package_name: undefined,
package_code: undefined,
original_cost_price: undefined,
cost_price_yuan: 0
}
availablePackages.value = []

View File

@@ -45,166 +45,6 @@
</template>
</ArtTable>
<!-- 套餐列表对话框 -->
<ElDialog
v-model="packageListDialogVisible"
title="套餐列表"
width="65%"
:close-on-click-modal="false"
@closed="handlePackageListDialogClosed"
>
<div class="package-list-dialog-content">
<!-- 添加授权套餐按钮 -->
<div class="package-list-header">
<ElButton
type="primary"
@click="showAddPackageDialog"
v-permission="'series_grants:manage_packages'"
>
添加授权套餐
</ElButton>
</div>
<!-- 套餐列表 -->
<ElTable
v-if="currentGrantPackages.length > 0"
:data="currentGrantPackages"
border
stripe
style="margin-top: 12px"
>
<ElTableColumn prop="package_name" label="套餐名称" />
<ElTableColumn prop="package_code" label="套餐编码" />
<ElTableColumn label="成本价">
<template #default="{ row }">
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="上架状态" align="center">
<template #default="{ row }">
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
<span v-else>-</span>
</template>
</ElTableColumn>
<ElTableColumn label="状态" width="100" align="center">
<template #default="{ row }">
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
<span v-else>-</span>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<ElButton
type="primary"
size="small"
link
@click="showEditPackageDialog(row)"
v-permission="'series_grants:edit_packages'"
>
编辑
</ElButton>
<ElButton
type="danger"
size="small"
link
@click="handleDeletePackage(row)"
v-permission="'series_grants:delete_packages'"
>
删除
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<ElEmpty v-else description="暂无套餐" :image-size="80" />
</div>
</ElDialog>
<!-- 添加/编辑套餐对话框 -->
<ElDialog
v-model="packageDialogVisible"
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
width="500px"
:close-on-click-modal="false"
@closed="handlePackageDialogClosed"
>
<ElForm
ref="packageFormRef"
:model="packageForm"
:rules="packageFormRules"
label-width="100px"
>
<!-- 添加模式选择套餐 -->
<ElFormItem label="选择套餐" prop="package_id" v-if="packageDialogType === 'add'">
<ElSelect
v-model="packageForm.package_id"
placeholder="请选择套餐"
style="width: 100%"
filterable
remote
:remote-method="searchAvailablePackages"
:loading="packageLoading"
clearable
>
<template
v-if="availablePackages.length === 0 && !packageLoading && currentGrantSeriesId"
>
<ElOption disabled value="" label="该系列没有可选套餐" />
</template>
<ElOption
v-for="pkg in availablePackages"
:key="pkg.id"
:label="`${pkg.package_name} (${pkg.package_code})`"
:value="pkg.id"
:disabled="isPackageAlreadyAdded(pkg.id)"
/>
</ElSelect>
</ElFormItem>
<!-- 编辑模式显示套餐信息 -->
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
<span>{{ packageForm.package_name }}</span>
</ElFormItem>
<ElFormItem label="套餐编码" v-if="packageDialogType === 'edit'">
<span>{{ packageForm.package_code }}</span>
</ElFormItem>
<!-- 成本价 -->
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
<ElInputNumber
v-model="packageForm.cost_price_yuan"
:min="packageForm.original_cost_price || 0"
:max="
packageForm.original_cost_price
? packageForm.original_cost_price * 1.5
: undefined
"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
<div v-if="packageForm.original_cost_price" class="form-tip">
原价: ¥{{ packageForm.original_cost_price.toFixed(2) }}, 最高: ¥{{
(packageForm.original_cost_price * 1.5).toFixed(2)
}}
(50%溢价)
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="packageDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSavePackage" :loading="packageSubmitLoading">
保存
</ElButton>
</div>
</template>
</ElDialog>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -318,9 +158,7 @@
<ElInputNumber
v-model="pkg.cost_price"
:min="pkg.original_cost_price || 0"
:max="
pkg.original_cost_price ? pkg.original_cost_price * 1.5 : undefined
"
:max="getPackageCostPriceMax(pkg)"
:precision="2"
:step="0.01"
:controls="false"
@@ -670,66 +508,6 @@
</ElFormItem>
</ElCol>
</ElRow>
<!-- 套餐配置(编辑模式下禁用) -->
<!-- <div class="form-section-title">
<span class="title-text">套餐配置(可选)</span>
</div> -->
<!-- 编辑模式下不显示套餐配置区域,请使用右键菜单中的"添加授权套餐"功能 -->
<!-- <ElFormItem label="选择套餐">
<ElSelect
v-model="selectedPackageIds"
placeholder="请选择套餐"
style="width: 100%"
multiple
filterable
remote
:remote-method="searchPackages"
:loading="packageLoading"
clearable
>
<template v-if="packageOptions.length === 0 && !packageLoading && form.series_id">
<ElOption disabled value="" label="该系列没有可选套餐" />
</template>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="pkg.package_name"
:value="pkg.id"
/>
</ElSelect>
<div class="form-tip">选择该授权下包含的套餐</div>
</ElFormItem> -->
<!-- 套餐成本价 -->
<!-- <ElFormItem label="套餐成本价" v-if="form.packages.length > 0">
<div class="package-list">
<div
v-for="(pkg, index) in form.packages"
:key="pkg.package_id"
class="package-item"
>
<span class="package-name">{{ getPackageName(pkg.package_id) }}</span>
<div class="cost-price-input-wrapper">
<ElInputNumber
v-model="pkg.cost_price"
:min="pkg.original_cost_price || 0"
:precision="2"
:step="0.01"
:controls="false"
placeholder="成本价(元)"
style="width: 150px"
/>
<span v-if="pkg.original_cost_price" class="min-cost-hint">
(成本价: ¥{{ pkg.original_cost_price.toFixed(2) }})
</span>
</div>
<ElButton type="danger" size="small" @click="removePackage(index)">删除</ElButton>
</div>
</div>
<div class="form-tip">设置每个套餐的成本价(单位:元),不能低于套餐原始成本价</div>
</ElFormItem> -->
</ElForm>
<template #footer>
@@ -789,9 +567,7 @@
PackageSeriesResponse,
ShopResponse,
PackageResponse,
CommissionTier,
GrantPackageItem,
GrantPackageInfo
CommissionTier
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -826,68 +602,6 @@
const formRef = ref<FormInstance>()
const currentStep = ref(0) // 当前步骤0 为第一步1 为第二步
// 套餐列表对话框相关
const packageListDialogVisible = ref(false)
const currentGrantId = ref<number>(0)
const currentGrantSeriesId = ref<number>(0)
const currentGrantPackages = ref<GrantPackageInfo[]>([])
// 套餐管理相关
const packageDialogVisible = ref(false)
const packageDialogType = ref<'add' | 'edit'>('add')
const packageSubmitLoading = ref(false)
const availablePackages = ref<PackageResponse[]>([])
const packageFormRef = ref<FormInstance>()
// 套餐表单
const packageForm = ref<{
package_id?: number
package_name?: string
package_code?: string
cost_price_yuan: number
original_cost_price?: number // 原始成本价(元)
}>({
cost_price_yuan: 0
})
// 套餐表单验证规则
const packageFormRules = computed<FormRules>(() => ({
package_id: [
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
],
cost_price_yuan: [
{ required: true, message: '请输入成本价', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入成本价'))
} else if (value < 0) {
callback(new Error('成本价不能小于0'))
} else if (
packageForm.value.original_cost_price &&
value < packageForm.value.original_cost_price
) {
callback(
new Error(`成本价不能低于原价 ¥${packageForm.value.original_cost_price.toFixed(2)}`)
)
} else if (
packageForm.value.original_cost_price &&
value > packageForm.value.original_cost_price * 1.5
) {
callback(
new Error(
`成本价不能高于原价的50%溢价(最高:¥${(packageForm.value.original_cost_price * 1.5).toFixed(2)}`
)
)
} else {
callback()
}
},
trigger: 'blur'
}
]
}))
const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([])
@@ -907,7 +621,10 @@
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
const res = await ShopService.getShopsCascade({
parent_id: parentId,
exclude_self: true
} as any)
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
@@ -1401,6 +1118,19 @@
return pkg ? pkg.package_name : `套餐ID: ${packageId}`
}
const getPackageCostPriceMax = (pkg?: {
original_cost_price?: number
suggested_retail_price?: number | null
}) => {
if (pkg?.suggested_retail_price !== undefined && pkg?.suggested_retail_price !== null) {
return Number((pkg.suggested_retail_price * 1.5).toFixed(2))
}
if (pkg?.original_cost_price !== undefined && pkg.original_cost_price !== null) {
return Number((pkg.original_cost_price * 1.5).toFixed(2))
}
return undefined
}
const formatSuggestedRetailPrice = (suggestedRetailPrice?: number | null) => {
if (suggestedRetailPrice === undefined || suggestedRetailPrice === null) {
return '-'
@@ -1536,24 +1266,6 @@
}
)
// 监听套餐选择,自动设置原始成本价和成本价
watch(
() => packageForm.value.package_id,
(packageId) => {
if (packageDialogType.value === 'add' && packageId) {
// 从 availablePackages 中查找选中的套餐
const selectedPackage = availablePackages.value.find((p) => p.id === packageId)
if (selectedPackage) {
const originalCostPrice = selectedPackage.cost_price
? selectedPackage.cost_price / 100
: 0
packageForm.value.original_cost_price = originalCostPrice
packageForm.value.cost_price_yuan = originalCostPrice
}
}
}
)
// 加载系列选项(用于新增对话框,默认加载10条
const loadSeriesOptions = async (seriesName?: string) => {
seriesLoading.value = true
@@ -1630,7 +1342,10 @@
const loadShopListForDialog = async () => {
shopLoading.value = true
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
const res = await ShopService.getShopsCascade({
parent_id: undefined,
exclude_self: true
} as any)
if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
@@ -2034,10 +1749,11 @@
for (const pkg of form.packages) {
const costPrice = pkg.cost_price ?? 0
const originalCostPrice = pkg.original_cost_price ?? 0
const maxCostPrice = getPackageCostPriceMax(pkg)
if (originalCostPrice > 0 && costPrice > originalCostPrice * 1.5) {
if (maxCostPrice !== undefined && costPrice > maxCostPrice) {
ElMessage.error(
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能高于原价的50%溢价(最高:¥${(originalCostPrice * 1.5).toFixed(2)}`
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能高于当前上限(最高:¥${maxCostPrice.toFixed(2)}`
)
submitLoading.value = false
return
@@ -2095,212 +1811,6 @@
}
}
// 显示套餐列表对话框
const showPackageListDialog = async (row: ShopSeriesGrantResponse) => {
currentGrantId.value = row.id
currentGrantSeriesId.value = row.series_id
// 获取该授权的详细信息,包括套餐列表
loading.value = true
try {
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(row.id)
if (res.code === 0) {
currentGrantPackages.value = res.data.packages || []
packageListDialogVisible.value = true
}
} catch (error) {
console.error('获取套餐列表失败:', error)
console.log('获取套餐列表失败')
} finally {
loading.value = false
}
}
// 显示添加套餐对话框
const showAddPackageDialog = () => {
packageDialogType.value = 'add'
packageForm.value = {
package_id: undefined,
cost_price_yuan: 0
}
// 加载可用套餐
if (currentGrantSeriesId.value) {
loadAvailablePackages()
}
packageDialogVisible.value = true
}
// 显示编辑套餐对话框
const showEditPackageDialog = (row: GrantPackageInfo) => {
packageDialogType.value = 'edit'
// 从 packageOptions 中查找原始成本价
const pkgOption = packageOptions.value.find((p) => p.id === row.package_id)
const originalCostPrice = pkgOption?.cost_price ? pkgOption.cost_price / 100 : 0
packageForm.value = {
package_id: row.package_id,
package_name: row.package_name,
package_code: row.package_code,
cost_price_yuan: row.cost_price / 100,
original_cost_price: originalCostPrice
}
packageDialogVisible.value = true
}
// 加载可用套餐
const loadAvailablePackages = async (packageName?: string) => {
if (!currentGrantSeriesId.value) return
packageLoading.value = true
try {
const params: any = {
page: 1,
page_size: 50,
series_id: currentGrantSeriesId.value
}
if (packageName) {
params.package_name = packageName
}
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
// 过滤掉赠送套餐
availablePackages.value = res.data.items.filter((pkg) => !pkg.is_gift)
}
} catch (error) {
console.error('加载套餐选项失败:', error)
} finally {
packageLoading.value = false
}
}
// 搜索可用套餐
const searchAvailablePackages = (query: string) => {
if (query) {
loadAvailablePackages(query)
} else {
loadAvailablePackages()
}
}
// 检查套餐是否已添加
const isPackageAlreadyAdded = (packageId: number) => {
return currentGrantPackages.value.some((p) => p.package_id === packageId)
}
// 保存套餐
const handleSavePackage = async () => {
if (!packageFormRef.value || !currentGrantId.value) return
await packageFormRef.value.validate(async (valid) => {
if (!valid) return
packageSubmitLoading.value = true
try {
const packages: GrantPackageItem[] = []
if (packageDialogType.value === 'add') {
// 添加模式:添加新套餐
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
})
} else {
// 编辑模式:更新套餐成本价
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
})
}
await ShopSeriesGrantService.manageGrantPackages(currentGrantId.value, { packages })
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
packageDialogVisible.value = false
// 刷新套餐列表
await refreshPackageList()
} catch (error) {
console.error(error)
console.log(packageDialogType.value === 'add' ? '添加失败' : '更新失败')
} finally {
packageSubmitLoading.value = false
}
})
}
// 删除套餐
const handleDeletePackage = (row: GrantPackageInfo) => {
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
if (!currentGrantId.value) return
packageSubmitLoading.value = true
try {
const packages: GrantPackageItem[] = [
{
package_id: row.package_id,
remove: true
}
]
await ShopSeriesGrantService.manageGrantPackages(currentGrantId.value, { packages })
ElMessage.success('删除成功')
// 刷新套餐列表
await refreshPackageList()
} catch (error) {
console.error(error)
console.log('删除失败')
} finally {
packageSubmitLoading.value = false
}
})
.catch(() => {
// 用户取消
})
}
// 刷新套餐列表
const refreshPackageList = async () => {
if (!currentGrantId.value) return
try {
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(currentGrantId.value)
if (res.code === 0) {
currentGrantPackages.value = res.data.packages || []
}
} catch (error) {
console.error('刷新套餐列表失败:', error)
}
}
// 关闭套餐对话框
const handlePackageDialogClosed = () => {
packageFormRef.value?.resetFields()
packageForm.value = {
cost_price_yuan: 0
}
availablePackages.value = []
}
// 关闭套餐列表对话框
const handlePackageListDialogClosed = () => {
currentGrantId.value = 0
currentGrantSeriesId.value = 0
currentGrantPackages.value = []
availablePackages.value = []
}
// 使用表格右键菜单功能
// 下一步
const handleNextStep = async () => {
if (!formRef.value) return
@@ -2483,16 +1993,4 @@
color: var(--el-text-color-primary);
}
.package-list-dialog-content {
.package-list-header {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.amount-value {
font-weight: 600;
color: var(--el-color-warning);
}
}
</style>

View File

@@ -29,6 +29,7 @@
:marginTop="10"
:actions="getActions"
:actionsWidth="150"
height="70vh"
:inlineActionsCount="2"
>
<template #default>
@@ -99,10 +100,6 @@
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
<ElInputNumber
v-model="packageForm.cost_price_yuan"
:min="packageForm.original_cost_price || 0"
:max="
packageForm.original_cost_price ? packageForm.original_cost_price * 1.5 : undefined
"
:precision="2"
:step="0.01"
:controls="false"
@@ -110,10 +107,8 @@
placeholder="请输入成本价"
/>
<div v-if="packageForm.original_cost_price" class="form-tip">
原价: ¥{{ packageForm.original_cost_price.toFixed(2) }}, 最高: ¥{{
(packageForm.original_cost_price * 1.5).toFixed(2)
}}
(50%溢价)
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价:
¥{{ packageForm.original_cost_price.toFixed(2) }}
</div>
</ElFormItem>
</ElForm>
@@ -132,7 +127,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
ElCard,
@@ -176,15 +171,42 @@
const packageDialogVisible = ref(false)
const packageDialogType = ref<'add' | 'edit'>('add')
const packageForm = ref<{
type PackageFormState = {
package_id?: number
package_name?: string
package_code?: string
cost_price_yuan: number
original_cost_price?: number
}>({
suggested_retail_price?: number
}
const createDefaultPackageForm = (): PackageFormState => ({
package_id: undefined,
package_name: undefined,
package_code: undefined,
cost_price_yuan: 0
})
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
const assignPackageForm = (values: Partial<PackageFormState> = {}) => {
Object.assign(packageForm.value, createDefaultPackageForm(), values)
}
const resolvePackagePricing = (row?: GrantPackageInfo) => {
const pkgOption = availablePackages.value.find((p) => p.id === row?.package_id)
const currentCostPrice = row?.cost_price !== undefined ? row.cost_price / 100 : 0
return {
original_cost_price:
pkgOption?.cost_price !== undefined && pkgOption?.cost_price !== null
? pkgOption.cost_price / 100
: currentCostPrice,
suggested_retail_price:
pkgOption?.suggested_retail_price !== undefined && pkgOption?.suggested_retail_price !== null
? pkgOption.suggested_retail_price / 100
: undefined
}
}
const packageRules = computed<FormRules>(() => ({
package_id: [
@@ -198,22 +220,6 @@
callback(new Error('请输入成本价'))
} else if (value < 0) {
callback(new Error('成本价不能小于0'))
} else if (
packageForm.value.original_cost_price &&
value < packageForm.value.original_cost_price
) {
callback(
new Error(`成本价不能低于原价 ¥${packageForm.value.original_cost_price.toFixed(2)}`)
)
} else if (
packageForm.value.original_cost_price &&
value > packageForm.value.original_cost_price * 1.5
) {
callback(
new Error(
`成本价不能高于原价的50%溢价(最高:¥${(packageForm.value.original_cost_price * 1.5).toFixed(2)}`
)
)
} else {
callback()
}
@@ -270,28 +276,26 @@
const showAddPackageDialog = () => {
packageDialogType.value = 'add'
packageForm.value = {
package_id: undefined,
cost_price_yuan: 0
}
assignPackageForm()
loadAvailablePackages()
packageDialogVisible.value = true
nextTick(() => packageFormRef.value?.clearValidate())
}
const showEditPackageDialog = (row: GrantPackageInfo) => {
packageDialogType.value = 'edit'
const packagePricing = resolvePackagePricing(row)
const pkgOption = availablePackages.value.find((p) => p.id === row.package_id)
const originalCostPrice = pkgOption?.cost_price ? pkgOption.cost_price / 100 : 0
packageForm.value = {
assignPackageForm({
package_id: row.package_id,
package_name: row.package_name,
package_code: row.package_code,
cost_price_yuan: row.cost_price / 100,
original_cost_price: originalCostPrice
}
original_cost_price: packagePricing.original_cost_price,
suggested_retail_price: packagePricing.suggested_retail_price
})
packageDialogVisible.value = true
nextTick(() => packageFormRef.value?.clearValidate())
}
const loadAvailablePackages = async (packageName?: string) => {
@@ -330,6 +334,28 @@
return packageList.value.some((p) => p.package_id === packageId)
}
watch(
() => packageForm.value.package_id,
(packageId) => {
if (packageDialogType.value === 'add' && packageId) {
const selectedPackage = availablePackages.value.find((p) => p.id === packageId)
if (selectedPackage) {
const originalCostPrice = selectedPackage.cost_price
? selectedPackage.cost_price / 100
: 0
const suggestedRetailPrice =
selectedPackage.suggested_retail_price !== undefined &&
selectedPackage.suggested_retail_price !== null
? selectedPackage.suggested_retail_price / 100
: undefined
packageForm.value.original_cost_price = originalCostPrice
packageForm.value.suggested_retail_price = suggestedRetailPrice
packageForm.value.cost_price_yuan = originalCostPrice
}
}
}
)
const handleSavePackage = async () => {
if (!packageFormRef.value || !grantId.value) return
@@ -338,19 +364,16 @@
submitLoading.value = true
try {
const packages: GrantPackageItem[] = []
if (packageDialogType.value === 'add') {
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
})
} else {
packages.push({
package_id: packageForm.value.package_id,
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
})
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
if (Number.isNaN(currentCostPriceYuan)) {
throw new Error('当前成本价无效,无法保存')
}
const packages: GrantPackageItem[] = [
{
package_id: packageForm.value.package_id,
cost_price: Math.round(currentCostPriceYuan * 100)
}
]
await ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages })
packageDialogVisible.value = false
@@ -392,10 +415,8 @@
}
const handlePackageDialogClosed = () => {
packageFormRef.value?.resetFields()
packageForm.value = {
cost_price_yuan: 0
}
packageFormRef.value?.clearValidate()
assignPackageForm()
availablePackages.value = []
}