Files
one-pipe-system/src/views/package-management/series-grants/packages.vue
2026-07-09 17:38:58 +08:00

483 lines
14 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="上架状态" 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>
</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,
FormInstance,
FormRules
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { ShopSeriesGrantService, PackageManageService } from '@/api/modules'
import type { GrantPackageInfo, GrantPackageItem, 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 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 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: [
{ 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
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
})
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 packages: GrantPackageItem[] = [
{
package_id: packageForm.value.package_id,
cost_price: Math.round(currentCostPriceYuan * 100)
}
]
await ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages })
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>