feat: 7月迭代
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m21s

This commit is contained in:
luo
2026-07-28 14:00:22 +08:00
parent f0a2b84e53
commit 2706c52a47
63 changed files with 4113 additions and 3945 deletions

View File

@@ -274,7 +274,7 @@
}}
</div>
</ElFormItem>
<ElFormItem label="生效条件">
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
<ElSelect
v-model="packageForm.expiry_base_override"
placeholder="请选择生效条件"
@@ -352,7 +352,8 @@
PackageResponse
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { getEnableStatusText } from '@/config/constants'
import { getEnableStatusText, JULY_PERMISSIONS } from '@/config/constants'
import { useAuth } from '@/composables/useAuth'
import {
mergeGrantPackageCandidates,
type GrantPackageCandidate
@@ -362,6 +363,8 @@
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
const canUpdateExpiryBase = hasAuth(JULY_PERMISSIONS.seriesGrants.updateExpiryBase)
const loading = ref(false)
const submitLoading = ref(false)
@@ -378,11 +381,11 @@
// 套餐表单
const packageForm = ref<{
package_id?: number
allocation_id?: number
package_name?: string
package_code?: string
original_cost_price?: number
cost_price_yuan: number
allocation_id?: number
expiry_base_override: ExpiryBaseSelection
initial_expiry_base_override: ExpiryBaseSelection
default_expiry_base_name?: string | null
@@ -509,6 +512,7 @@
packageDialogType.value = 'add'
packageForm.value = {
package_id: undefined,
allocation_id: undefined,
package_name: undefined,
package_code: undefined,
original_cost_price: undefined,
@@ -530,11 +534,11 @@
packageDialogType.value = 'edit'
packageForm.value = {
package_id: row.package_id,
allocation_id: row.allocation_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,
allocation_id: row.allocation_id,
expiry_base_override: row.expiry_base_override ?? 'default',
initial_expiry_base_override: row.expiry_base_override ?? 'default',
default_expiry_base_name: row.default_expiry_base_name,
@@ -619,8 +623,12 @@
const expiryBaseChanged =
packageForm.value.expiry_base_override !==
packageForm.value.initial_expiry_base_override
if (expiryBaseChanged && !packageForm.value.allocation_id) {
ElMessage.error('该套餐分配记录缺少ID无法更新生效条件')
if (
expiryBaseChanged &&
(packageForm.value.allocation_id === undefined ||
packageForm.value.allocation_id === null)
) {
ElMessage.error('该套餐分配记录缺少套餐分配 ID无法修改生效条件')
return
}

View File

@@ -29,12 +29,22 @@
:marginTop="10"
:pagination="false"
:actions="getActions"
:actionsWidth="150"
:inlineActionsCount="2"
:actionsWidth="240"
:inlineActionsCount="3"
>
<template #default>
<ElTableColumn prop="package_name" label="套餐名称" minWidth="150" />
<ElTableColumn prop="package_code" label="套餐编码" minWidth="120" />
<ElTableColumn
prop="package_name"
label="套餐名称"
minWidth="220"
show-overflow-tooltip
/>
<ElTableColumn
prop="package_code"
label="套餐编码"
minWidth="180"
show-overflow-tooltip
/>
<ElTableColumn label="成本价" width="120">
<template #default="{ row }">
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
@@ -81,16 +91,18 @@
@closed="handlePackageDialogClosed"
>
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" label-width="140px">
<ElFormItem label="选择套餐" prop="package_id" v-if="packageDialogType === 'add'">
<ElFormItem label="选择套餐" prop="package_ids" v-if="packageDialogType === 'add'">
<ElSelect
v-model="packageForm.package_id"
placeholder="请选择套餐"
v-model="packageForm.package_ids"
placeholder="请选择套餐(可多选)"
style="width: 100%"
filterable
remote
:remote-method="searchAvailablePackages"
:loading="packageLoading"
clearable
multiple
:multiple-limit="100"
>
<template v-if="availablePackages.length === 0 && !packageLoading && seriesId">
<ElOption disabled value="" label="该系列没有可选套餐" />
@@ -105,6 +117,33 @@
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="packageDialogType === 'add' && packageForm.packages.length"
label="套餐配置"
prop="packages"
>
<div class="package-config-list">
<div
v-for="pkg in packageForm.packages"
:key="pkg.package_id"
class="package-config-item"
>
<div class="package-config-name" :title="pkg.package_name || ''">
{{ pkg.package_name || '套餐名称不可用' }}
</div>
<ElInputNumber
v-model="pkg.cost_price_yuan"
:min="pkg.original_cost_price || 0"
:max="getPackageCostPriceMax(pkg)"
:precision="2"
:step="0.01"
:controls="false"
placeholder="成本价(元)"
/>
</div>
</div>
</ElFormItem>
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
<span>{{ packageForm.package_name }}</span>
</ElFormItem>
@@ -127,7 +166,7 @@
}}
</div>
</ElFormItem>
<ElFormItem label="生效条件">
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
<ElSelect
v-model="packageForm.expiry_base_override"
placeholder="请选择生效条件"
@@ -161,6 +200,49 @@
</div>
</template>
</ElDialog>
<ElDialog
v-model="expiryDialogVisible"
title="修改生效条件"
width="460px"
:close-on-click-modal="false"
>
<ElForm label-width="130px">
<ElFormItem label="套餐名称">
<span class="ellipsis-value" :title="expiryForm.package_name || ''">
{{ expiryForm.package_name || '-' }}
</span>
</ElFormItem>
<ElFormItem label="套餐编码">
<span class="ellipsis-value" :title="expiryForm.package_code || ''">
{{ expiryForm.package_code || '-' }}
</span>
</ElFormItem>
<ElFormItem label="套餐默认生效条件">
{{ expiryForm.default_expiry_base_name || '-' }}
</ElFormItem>
<ElFormItem label="覆盖生效条件">
<ElSelect v-model="expiryForm.expiry_base_override" style="width: 100%">
<ElOption label="跟随套餐默认" value="default" />
<ElOption label="购买即生效" value="from_purchase" />
<ElOption label="实名激活时生效" value="from_activation" />
</ElSelect>
</ElFormItem>
<ElFormItem label="最终生效条件">
{{ expiryForm.effective_expiry_base_name || '-' }}
</ElFormItem>
<div class="form-tip">仅影响后续新订单不影响已购买套餐</div>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="expiryDialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="expirySubmitLoading" @click="saveExpiryBase">
保存
</ElButton>
</div>
</template>
</ElDialog>
</div>
</ArtTableFullScreen>
</template>
@@ -196,6 +278,7 @@
PackageAllocationExpiryBaseOverride
} from '@/types/api'
import { useAuth } from '@/composables/useAuth'
import { JULY_PERMISSIONS } from '@/config/constants'
import {
mergeGrantPackageCandidates,
type GrantPackageCandidate
@@ -206,6 +289,7 @@
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
const canUpdateExpiryBase = hasAuth(JULY_PERMISSIONS.seriesGrants.updateExpiryBase)
const loading = ref(false)
const submitLoading = ref(false)
@@ -221,33 +305,61 @@
const packageDialogVisible = ref(false)
const packageDialogType = ref<'add' | 'edit'>('add')
const expiryDialogVisible = ref(false)
const expirySubmitLoading = ref(false)
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
type PackageFormState = {
package_id?: number
type PackageCostFormItem = {
package_id: number
package_name?: string
package_code?: string
cost_price_yuan: number
original_cost_price?: number
suggested_retail_price?: number
}
type PackageFormState = {
package_id?: number
allocation_id?: number
package_ids: number[]
package_name?: string
package_code?: string
cost_price_yuan: number
original_cost_price?: number
suggested_retail_price?: number
expiry_base_override: ExpiryBaseSelection
initial_expiry_base_override: ExpiryBaseSelection
default_expiry_base_name?: string | null
expiry_base_override_name?: string | null
effective_expiry_base_name?: string | null
packages: PackageCostFormItem[]
}
type ExpiryFormState = {
package_id?: number
allocation_id?: number
package_name?: string
package_code?: string
default_expiry_base_name?: string | null
expiry_base_override: ExpiryBaseSelection
expiry_base_override_name?: string | null
effective_expiry_base_name?: string | null
}
const createDefaultPackageForm = (): PackageFormState => ({
package_id: undefined,
allocation_id: undefined,
package_ids: [],
package_name: undefined,
package_code: undefined,
cost_price_yuan: 0,
expiry_base_override: 'default',
initial_expiry_base_override: 'default'
initial_expiry_base_override: 'default',
packages: []
})
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
const expiryForm = ref<ExpiryFormState>({ expiry_base_override: 'default' })
const assignPackageForm = (values: Partial<PackageFormState> = {}) => {
Object.assign(packageForm.value, createDefaultPackageForm(), values)
@@ -271,11 +383,30 @@
}
const packageRules = computed<FormRules>(() => ({
package_id: [
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
package_ids: [
{
validator: (_rule, value, callback) => {
if (packageDialogType.value !== 'add') {
callback()
return
}
if (!Array.isArray(value) || value.length === 0) {
callback(new Error('请至少选择一个套餐'))
} else if (value.length > 100) {
callback(new Error('最多选择100个套餐'))
} else {
callback()
}
},
trigger: 'change'
}
],
cost_price_yuan: [
{ required: true, message: '请输入成本价', trigger: 'blur' },
{
required: packageDialogType.value === 'edit',
message: '请输入成本价',
trigger: 'blur'
},
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
@@ -294,6 +425,14 @@
const getActions = (row: GrantPackageInfo) => {
const actions: any[] = []
if (canUpdateExpiryBase) {
actions.push({
label: '修改生效条件',
handler: () => showExpiryBaseDialog(row),
type: 'primary'
})
}
if (hasAuth('series_grants:edit_packages_list')) {
actions.push({
label: '编辑',
@@ -338,7 +477,7 @@
const showAddPackageDialog = () => {
packageDialogType.value = 'add'
assignPackageForm()
assignPackageForm({ package_ids: [], packages: [] })
loadAvailablePackages()
packageDialogVisible.value = true
nextTick(() => packageFormRef.value?.clearValidate())
@@ -350,12 +489,12 @@
assignPackageForm({
package_id: row.package_id,
allocation_id: row.allocation_id,
package_name: row.package_name,
package_code: row.package_code,
cost_price_yuan: row.cost_price / 100,
original_cost_price: packagePricing.original_cost_price,
suggested_retail_price: packagePricing.suggested_retail_price,
allocation_id: row.allocation_id,
expiry_base_override: row.expiry_base_override ?? 'default',
initial_expiry_base_override: row.expiry_base_override ?? 'default',
default_expiry_base_name: row.default_expiry_base_name,
@@ -366,25 +505,43 @@
nextTick(() => packageFormRef.value?.clearValidate())
}
const showExpiryBaseDialog = (row: GrantPackageInfo) => {
if (row.allocation_id === undefined || row.allocation_id === null) {
ElMessage.error('该套餐分配记录缺少套餐分配 ID无法修改生效条件')
return
}
expiryForm.value = {
package_id: row.package_id,
allocation_id: row.allocation_id,
package_name: row.package_name,
package_code: row.package_code,
default_expiry_base_name: row.default_expiry_base_name,
expiry_base_override: row.expiry_base_override ?? 'default',
expiry_base_override_name: row.expiry_base_override_name,
effective_expiry_base_name: row.effective_expiry_base_name
}
expiryDialogVisible.value = true
}
const loadAvailablePackages = async (packageName?: string) => {
if (!seriesId.value) return
packageLoading.value = true
try {
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)
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 {
@@ -400,6 +557,50 @@
}
}
const createPackageCostFormItem = (packageId: number): PackageCostFormItem => {
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
const originalCostPrice = selectedPackage?.cost_price ? selectedPackage.cost_price / 100 : 0
return {
package_id: packageId,
package_name: selectedPackage?.package_name,
package_code: selectedPackage?.package_code,
cost_price_yuan: originalCostPrice,
original_cost_price: originalCostPrice,
suggested_retail_price:
selectedPackage?.suggested_retail_price !== undefined &&
selectedPackage?.suggested_retail_price !== null
? selectedPackage.suggested_retail_price / 100
: undefined
}
}
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
}
watch(
() => packageForm.value.package_ids,
(packageIds) => {
if (packageDialogType.value !== 'add') return
const existingItems = new Map(
packageForm.value.packages.map((item) => [item.package_id, item])
)
packageForm.value.packages = packageIds.map(
(packageId) => existingItems.get(packageId) || createPackageCostFormItem(packageId)
)
},
{ deep: true }
)
watch(
() => packageForm.value.package_id,
(packageId) => {
@@ -436,19 +637,51 @@
}
const costPrice = Math.round(currentCostPriceYuan * 100)
if (packageDialogType.value === 'add') {
if (packageForm.value.packages.length === 0) {
ElMessage.error('请至少选择一个套餐')
return
}
const packages: GrantPackageItem[] = packageForm.value.packages.map((pkg) => {
const currentCostPrice = Number(pkg.cost_price_yuan)
if (!Number.isFinite(currentCostPrice) || currentCostPrice < 0) {
throw new Error(`套餐 ${pkg.package_name || pkg.package_id} 的成本价无效`)
}
const maxCostPrice = getPackageCostPriceMax(pkg)
if (maxCostPrice !== undefined && currentCostPrice > maxCostPrice) {
throw new Error(
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
)
}
if (
pkg.original_cost_price !== undefined &&
currentCostPrice < pkg.original_cost_price
) {
throw new Error(
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
)
}
return {
package_id: pkg.package_id,
cost_price: Math.round(currentCostPrice * 100)
}
})
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
expiry_base_override:
packageForm.value.expiry_base_override === 'default'
? null
: packageForm.value.expiry_base_override,
packages: [{ package_id: packageForm.value.package_id!, cost_price: costPrice }]
packages
})
} else {
const expiryBaseChanged =
packageForm.value.expiry_base_override !==
packageForm.value.initial_expiry_base_override
if (expiryBaseChanged && !packageForm.value.allocation_id) {
ElMessage.error('该套餐分配记录缺少ID无法更新生效条件')
if (
expiryBaseChanged &&
(packageForm.value.allocation_id === undefined ||
packageForm.value.allocation_id === null)
) {
ElMessage.error('该套餐分配记录缺少套餐分配 ID无法修改生效条件')
return
}
@@ -477,13 +710,39 @@
packageDialogVisible.value = false
await fetchPackageList()
} catch (error) {
console.error(error)
ElMessage.error(error instanceof Error ? error.message : '保存套餐失败')
} finally {
submitLoading.value = false
}
})
}
const saveExpiryBase = async () => {
if (expiryForm.value.allocation_id === undefined || expiryForm.value.allocation_id === null) {
return
}
expirySubmitLoading.value = true
try {
const response = await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
expiryForm.value.allocation_id,
{
expiry_base_override:
expiryForm.value.expiry_base_override === 'default'
? null
: expiryForm.value.expiry_base_override
}
)
if (response.code === 0) {
ElMessage.success('生效条件更新成功')
expiryDialogVisible.value = false
await fetchPackageList()
}
} finally {
expirySubmitLoading.value = false
}
}
const handleDeletePackage = (row: GrantPackageInfo) => {
import('element-plus').then(({ ElMessageBox }) => {
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
@@ -572,6 +831,38 @@
color: var(--el-text-color-secondary);
}
.package-config-list {
width: 100%;
}
.package-config-item {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.package-config-name,
.ellipsis-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.package-config-name {
flex: 1;
}
.ellipsis-value {
display: block;
width: 260px;
}
.dialog-footer {
text-align: right;
}