579 lines
18 KiB
Vue
579 lines
18 KiB
Vue
<template>
|
||
<ArtTableFullScreen>
|
||
<div class="grant-package-list-page" id="table-full-screen">
|
||
<ElCard shadow="never" class="art-table-card">
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<ElButton @click="handleBack">
|
||
<template #icon>
|
||
<ElIcon><ArrowLeft /></ElIcon>
|
||
</template>
|
||
返回
|
||
</ElButton>
|
||
<span class="grant-name">{{ grantName }}</span>
|
||
</div>
|
||
<ElButton
|
||
type="primary"
|
||
@click="showAddPackageDialog"
|
||
v-permission="'series_grants:manage_packages_list'"
|
||
>
|
||
添加授权套餐
|
||
</ElButton>
|
||
</div>
|
||
|
||
<ArtTable
|
||
ref="tableRef"
|
||
row-key="package_id"
|
||
:loading="loading"
|
||
:data="packageList"
|
||
:marginTop="10"
|
||
:pagination="false"
|
||
:actions="getActions"
|
||
:actionsWidth="150"
|
||
:inlineActionsCount="2"
|
||
>
|
||
<template #default>
|
||
<ElTableColumn prop="package_name" label="套餐名称" minWidth="150" />
|
||
<ElTableColumn prop="package_code" label="套餐编码" minWidth="120" />
|
||
<ElTableColumn label="成本价" width="120">
|
||
<template #default="{ row }">
|
||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="套餐默认生效条件" minWidth="150">
|
||
<template #default="{ row }">
|
||
{{ row.default_expiry_base_name || '-' }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="覆盖生效条件" minWidth="150">
|
||
<template #default="{ row }">
|
||
{{ row.expiry_base_override_name || '-' }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="最终生效条件" minWidth="150">
|
||
<template #default="{ row }">
|
||
{{ row.effective_expiry_base_name || '-' }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="上架状态" width="100" 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>
|
||
</template>
|
||
</ArtTable>
|
||
</ElCard>
|
||
|
||
<ElDialog
|
||
v-model="packageDialogVisible"
|
||
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
@closed="handlePackageDialogClosed"
|
||
>
|
||
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" 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 && seriesId">
|
||
<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"
|
||
: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>
|
||
<ElFormItem label="生效条件">
|
||
<ElRadioGroup v-model="packageForm.expiry_base_override">
|
||
<ElRadio value="default">跟随套餐默认</ElRadio>
|
||
<ElRadio value="from_purchase">购买即生效</ElRadio>
|
||
<ElRadio value="from_realname">实名即生效</ElRadio>
|
||
</ElRadioGroup>
|
||
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||
</ElFormItem>
|
||
<template v-if="packageDialogType === 'edit'">
|
||
<ElFormItem label="套餐默认生效条件">
|
||
{{ packageForm.default_expiry_base_name || '-' }}
|
||
</ElFormItem>
|
||
<ElFormItem label="覆盖生效条件">
|
||
{{ packageForm.expiry_base_override_name || '-' }}
|
||
</ElFormItem>
|
||
<ElFormItem label="最终生效条件">
|
||
{{ packageForm.effective_expiry_base_name || '-' }}
|
||
</ElFormItem>
|
||
</template>
|
||
</ElForm>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="packageDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleSavePackage" :loading="submitLoading">
|
||
保存
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
</div>
|
||
</ArtTableFullScreen>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import {
|
||
ElCard,
|
||
ElButton,
|
||
ElIcon,
|
||
ElTableColumn,
|
||
ElTag,
|
||
ElDialog,
|
||
ElSelect,
|
||
ElOption,
|
||
ElInputNumber,
|
||
ElForm,
|
||
ElFormItem,
|
||
ElRadioGroup,
|
||
ElRadio,
|
||
ElMessage,
|
||
FormInstance,
|
||
FormRules
|
||
} from 'element-plus'
|
||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||
import {
|
||
ShopSeriesGrantService,
|
||
ShopPackageAllocationService,
|
||
PackageManageService
|
||
} from '@/api/modules'
|
||
import type {
|
||
GrantPackageInfo,
|
||
GrantPackageItem,
|
||
PackageAllocationExpiryBaseOverride,
|
||
PackageResponse
|
||
} from '@/types/api'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
|
||
defineOptions({ name: 'SeriesGrantPackages' })
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const { hasAuth } = useAuth()
|
||
|
||
const loading = ref(false)
|
||
const submitLoading = ref(false)
|
||
const packageLoading = ref(false)
|
||
const tableRef = ref()
|
||
|
||
const grantId = computed(() => Number(route.params.id))
|
||
const seriesId = ref<number>(0)
|
||
const shopId = ref<number>(0)
|
||
const grantName = ref<string>('')
|
||
const packageList = ref<GrantPackageInfo[]>([])
|
||
const availablePackages = ref<PackageResponse[]>([])
|
||
const packageFormRef = ref<FormInstance>()
|
||
|
||
const packageDialogVisible = ref(false)
|
||
const packageDialogType = ref<'add' | 'edit'>('add')
|
||
|
||
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||
|
||
type PackageFormState = {
|
||
package_id?: number
|
||
package_name?: string
|
||
package_code?: string
|
||
cost_price_yuan: number
|
||
original_cost_price?: number
|
||
suggested_retail_price?: number
|
||
allocation_id?: 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
|
||
}
|
||
|
||
const createDefaultPackageForm = (): PackageFormState => ({
|
||
package_id: undefined,
|
||
package_name: undefined,
|
||
package_code: undefined,
|
||
cost_price_yuan: 0,
|
||
expiry_base_override: 'default',
|
||
initial_expiry_base_override: 'default'
|
||
})
|
||
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: [
|
||
{ 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 {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
}))
|
||
|
||
const getActions = (row: GrantPackageInfo) => {
|
||
const actions: any[] = []
|
||
|
||
if (hasAuth('series_grants:edit_packages_list')) {
|
||
actions.push({
|
||
label: '编辑',
|
||
handler: () => showEditPackageDialog(row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('series_grants:delete_packages_list')) {
|
||
actions.push({
|
||
label: '删除',
|
||
handler: () => handleDeletePackage(row),
|
||
type: 'danger'
|
||
})
|
||
}
|
||
|
||
return actions
|
||
}
|
||
|
||
// 返回上一页
|
||
const handleBack = () => {
|
||
router.back()
|
||
}
|
||
|
||
const fetchPackageList = async () => {
|
||
if (!grantId.value) return
|
||
|
||
loading.value = true
|
||
try {
|
||
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(grantId.value)
|
||
if (res.code === 0) {
|
||
packageList.value = res.data.packages || []
|
||
seriesId.value = res.data.series_id
|
||
shopId.value = res.data.shop_id
|
||
grantName.value = `${res.data.series_name} - ${res.data.shop_name}`
|
||
}
|
||
} catch (error) {
|
||
console.error('获取套餐列表失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const showAddPackageDialog = () => {
|
||
packageDialogType.value = 'add'
|
||
assignPackageForm()
|
||
loadAvailablePackages()
|
||
packageDialogVisible.value = true
|
||
nextTick(() => packageFormRef.value?.clearValidate())
|
||
}
|
||
|
||
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
||
packageDialogType.value = 'edit'
|
||
const packagePricing = resolvePackagePricing(row)
|
||
|
||
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: 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,
|
||
expiry_base_override_name: row.expiry_base_override_name,
|
||
effective_expiry_base_name: row.effective_expiry_base_name
|
||
})
|
||
packageDialogVisible.value = true
|
||
nextTick(() => packageFormRef.value?.clearValidate())
|
||
}
|
||
|
||
const loadAvailablePackages = async (packageName?: string) => {
|
||
if (!seriesId.value) return
|
||
|
||
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 = res.data.items
|
||
}
|
||
} catch (error) {
|
||
console.error('加载套餐选项失败:', error)
|
||
} finally {
|
||
packageLoading.value = false
|
||
}
|
||
}
|
||
|
||
const searchAvailablePackages = (query: string) => {
|
||
if (query) {
|
||
loadAvailablePackages(query)
|
||
} else {
|
||
loadAvailablePackages()
|
||
}
|
||
}
|
||
|
||
const isPackageAlreadyAdded = (packageId: number) => {
|
||
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
|
||
|
||
await packageFormRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
|
||
submitLoading.value = true
|
||
try {
|
||
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
|
||
if (Number.isNaN(currentCostPriceYuan)) {
|
||
throw new Error('当前成本价无效,无法保存')
|
||
}
|
||
const costPrice = Math.round(currentCostPriceYuan * 100)
|
||
if (packageDialogType.value === 'add') {
|
||
await ShopPackageAllocationService.createShopPackageAllocation({
|
||
shop_id: shopId.value,
|
||
package_id: packageForm.value.package_id!,
|
||
cost_price: costPrice,
|
||
expiry_base_override:
|
||
packageForm.value.expiry_base_override === 'default'
|
||
? null
|
||
: packageForm.value.expiry_base_override
|
||
})
|
||
} else {
|
||
const expiryBaseChanged =
|
||
packageForm.value.expiry_base_override !==
|
||
packageForm.value.initial_expiry_base_override
|
||
if (expiryBaseChanged && !packageForm.value.allocation_id) {
|
||
ElMessage.error('该套餐分配记录缺少ID,无法更新生效条件')
|
||
return
|
||
}
|
||
|
||
const packages: GrantPackageItem[] = [
|
||
{ package_id: packageForm.value.package_id, cost_price: costPrice }
|
||
]
|
||
await Promise.all([
|
||
ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages }),
|
||
...(expiryBaseChanged
|
||
? [
|
||
ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||
packageForm.value.allocation_id!,
|
||
{
|
||
expiry_base_override:
|
||
packageForm.value.expiry_base_override === 'default'
|
||
? null
|
||
: packageForm.value.expiry_base_override
|
||
}
|
||
)
|
||
]
|
||
: [])
|
||
])
|
||
}
|
||
packageDialogVisible.value = false
|
||
await fetchPackageList()
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleDeletePackage = (row: GrantPackageInfo) => {
|
||
import('element-plus').then(({ ElMessageBox }) => {
|
||
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(async () => {
|
||
submitLoading.value = true
|
||
try {
|
||
const packages: GrantPackageItem[] = [
|
||
{
|
||
package_id: row.package_id,
|
||
remove: true
|
||
}
|
||
]
|
||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages })
|
||
await fetchPackageList()
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
})
|
||
.catch(() => {})
|
||
})
|
||
}
|
||
|
||
const handlePackageDialogClosed = () => {
|
||
packageFormRef.value?.clearValidate()
|
||
assignPackageForm()
|
||
availablePackages.value = []
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchPackageList()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.grant-package-list-page {
|
||
height: 100%;
|
||
padding: 16px;
|
||
|
||
:deep(.art-table-card) {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
|
||
.el-card__body {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
}
|
||
|
||
.grant-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
|
||
.amount-value {
|
||
font-weight: 600;
|
||
color: var(--el-color-warning);
|
||
}
|
||
|
||
.form-tip {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.dialog-footer {
|
||
text-align: right;
|
||
}
|
||
}
|
||
</style>
|