将单套餐分配和套餐系列分配改成代理系列授权
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m23s

This commit is contained in:
sexygoat
2026-03-04 17:22:47 +08:00
parent 237eeed87a
commit 08d5043b3f
14 changed files with 2367 additions and 2654 deletions

View File

@@ -0,0 +1,663 @@
<template>
<div class="series-grants-detail">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">代理系列授权详情</h2>
</div>
<!-- 详情内容 -->
<div v-if="detailData" class="detail-content">
<!-- 基本信息 -->
<div class="info-section">
<div class="section-title">基本信息</div>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="ID">{{ detailData.id }}</ElDescriptionsItem>
<ElDescriptionsItem label="系列编码">{{
detailData.series_code || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="系列名称">{{ detailData.series_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="店铺名称">{{ detailData.shop_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="分配者店铺">
<ElTag v-if="detailData.allocator_shop_id === 0" type="primary">
{{ detailData.allocator_shop_name || '平台' }}
</ElTag>
<span v-else>{{ detailData.allocator_shop_name || '-' }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="detailData.status === 1 ? 'success' : 'danger'">
{{ detailData.status === 1 ? '启用' : '禁用' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
formatDateTime(detailData.created_at)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">{{
formatDateTime(detailData.updated_at)
}}</ElDescriptionsItem>
</ElDescriptions>
</div>
<!-- 佣金配置 -->
<div class="info-section">
<div class="section-title">佣金配置</div>
<ElDescriptions :column="2" border label-width="120">
<ElDescriptionsItem label="佣金类型">
<ElTag :type="detailData.commission_type === 'fixed' ? 'success' : 'warning'">
{{ detailData.commission_type === 'fixed' ? '固定佣金' : '梯度佣金' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="固定佣金金额" v-if="detailData.commission_type === 'fixed'">
<span v-if="detailData.one_time_commission_amount" class="amount-value">
¥{{ (detailData.one_time_commission_amount / 100).toFixed(2) }}
</span>
<span v-else>-</span>
</ElDescriptionsItem>
<ElDescriptionsItem
label="梯度配置"
:span="1"
v-if="detailData.commission_type === 'tiered'"
>
<div
v-if="detailData.commission_tiers && detailData.commission_tiers.length > 0"
class="tier-table-wrapper"
>
<ElTable :data="tierTableData" border size="small" style="width: 100%">
<ElTableColumn label="档位" width="80" align="center">
<template #default="{ $index }">
<ElTag type="primary" size="small">档位{{ $index + 1 }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="达标阈值" width="120">
<template #default="{ row }">
<span class="threshold-value"
>{{ row.operator }} {{ formatThreshold(row) }}</span
>
</template>
</ElTableColumn>
<ElTableColumn label="统计维度" width="110">
<template #default="{ row }">
<ElTag size="small" type="info">
{{ row.dimension === 'sales_count' ? '销量' : '销售额' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="统计范围" width="120">
<template #default="{ row }">
<ElTag size="small" type="warning">
{{
row.stat_scope === 'self'
? '仅自己'
: row.stat_scope === 'self_and_sub'
? '自己+下级'
: '-'
}}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="佣金金额" min-width="100">
<template #default="{ row }">
<span class="amount-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
</template>
</ElTableColumn>
</ElTable>
</div>
<span v-else>-</span>
</ElDescriptionsItem>
</ElDescriptions>
</div>
<!-- 强制充值配置 -->
<div class="info-section">
<div class="section-title">强制充值配置</div>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="启用状态">
<ElTag :type="detailData.force_recharge_enabled ? 'warning' : 'info'">
{{ detailData.force_recharge_enabled ? '已启用' : '未启用' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="锁定状态">
<ElTag :type="detailData.force_recharge_locked ? 'danger' : 'success'">
{{ detailData.force_recharge_locked ? '已锁定(平台控制)' : '未锁定' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="强充金额" :span="2">
<span v-if="detailData.force_recharge_amount" class="amount-value">
¥{{ (detailData.force_recharge_amount / 100).toFixed(2) }}
</span>
<span v-else>-</span>
</ElDescriptionsItem>
</ElDescriptions>
</div>
<!-- 套餐信息 -->
<div class="info-section">
<div class="section-title">
<span>套餐列表</span>
<ElButton
type="primary"
size="small"
@click="showAddPackageDialog"
v-permission="'series_grants:manage_packages'"
>
添加授权套餐
</ElButton>
</div>
<!-- 套餐列表 -->
<div v-if="detailData.packages && detailData.packages.length > 0" class="package-table">
<ElTable :data="detailData.packages" border stripe>
<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>
</div>
<ElEmpty v-else description="暂无套餐" :image-size="80" />
</div>
</div>
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
<!-- 添加/编辑套餐对话框 -->
<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="searchPackages"
:loading="packageLoading"
clearable
>
<template
v-if="availablePackages.length === 0 && !packageLoading && detailData?.series_id"
>
<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="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="packageDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSavePackage" :loading="submitLoading">
保存
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
ElCard,
ElButton,
ElIcon,
ElMessage,
ElMessageBox,
ElDescriptions,
ElDescriptionsItem,
ElTag,
ElTable,
ElTableColumn,
ElEmpty,
ElDialog,
ElSelect,
ElOption,
ElInputNumber,
ElForm,
ElFormItem,
FormInstance,
FormRules
} from 'element-plus'
import { ArrowLeft, Loading, Setting, List, Plus } from '@element-plus/icons-vue'
import { ShopSeriesGrantService, PackageManageService, PackageSeriesService } from '@/api/modules'
import type {
ShopSeriesGrantResponse,
PackageResponse,
GrantPackageItem,
GrantPackageInfo,
PackageSeriesResponse
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'SeriesGrantsDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const submitLoading = ref(false)
const packageLoading = ref(false)
const detailData = ref<ShopSeriesGrantResponse | null>(null)
const seriesData = ref<PackageSeriesResponse | null>(null)
const packageDialogVisible = ref(false)
const packageDialogType = ref<'add' | 'edit'>('add')
const availablePackages = ref<PackageResponse[]>([])
const packageFormRef = ref<FormInstance>()
// 套餐表单
const packageForm = ref<{
package_id?: number
package_name?: string
package_code?: string
cost_price_yuan: number
}>({
cost_price_yuan: 0
})
// 表单验证规则
const packageRules = computed<FormRules>(() => ({
package_id: [
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
],
cost_price_yuan: [
{ required: true, message: '请输入成本价', trigger: 'blur' },
{ type: 'number', min: 0, message: '成本价不能小于0', trigger: 'blur' }
]
}))
// 梯度配置表格数据(合并授权数据和系列数据)
const tierTableData = computed(() => {
if (
!detailData.value?.commission_tiers ||
!seriesData.value?.one_time_commission_config?.tiers
) {
return []
}
const grantTiers = detailData.value.commission_tiers
const seriesTiers = seriesData.value.one_time_commission_config.tiers
return grantTiers.map((grantTier, index) => {
const seriesTier = seriesTiers[index] || {}
return {
operator: grantTier.operator || '>=',
threshold: grantTier.threshold,
dimension: seriesTier.dimension || 'sales_count',
stat_scope: seriesTier.stat_scope,
amount: grantTier.amount
}
})
})
// 格式化达标阈值显示
const formatThreshold = (row: any) => {
if (row.dimension === 'sales_amount') {
// 销售额维度,阈值单位是分,需要转换为元显示
return `¥${(row.threshold / 100).toFixed(2)}`
} else {
// 销量维度,直接显示数字
return `${row.threshold}`
}
}
// 返回上一页
const handleBack = () => {
router.back()
}
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
return
}
loading.value = true
try {
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(id)
if (res.code === 0) {
detailData.value = res.data
// 如果是梯度佣金类型,获取系列配置以显示完整的梯度信息
if (res.data.commission_type === 'tiered' && res.data.series_id) {
await fetchSeriesData(res.data.series_id)
}
}
} catch (error) {
console.error(error)
ElMessage.error('获取详情失败')
} finally {
loading.value = false
}
}
// 获取系列配置数据
const fetchSeriesData = async (seriesId: number) => {
try {
const res = await PackageSeriesService.getPackageSeriesDetail(seriesId)
if (res.code === 0) {
seriesData.value = res.data
}
} catch (error) {
console.error('获取系列配置失败:', error)
}
}
// 显示添加套餐对话框
const showAddPackageDialog = () => {
packageDialogType.value = 'add'
packageForm.value = {
package_id: undefined,
cost_price_yuan: 0
}
// 加载可用套餐
if (detailData.value?.series_id) {
loadAvailablePackages()
}
packageDialogVisible.value = true
}
// 显示编辑套餐对话框
const showEditPackageDialog = (row: GrantPackageInfo) => {
packageDialogType.value = 'edit'
packageForm.value = {
package_id: row.package_id,
package_name: row.package_name,
package_code: row.package_code,
cost_price_yuan: row.cost_price / 100
}
packageDialogVisible.value = true
}
// 加载可用套餐
const loadAvailablePackages = async (packageName?: string) => {
if (!detailData.value?.series_id) return
packageLoading.value = true
try {
const params: any = {
page: 1,
page_size: 50,
series_id: detailData.value.series_id
}
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 searchPackages = (query: string) => {
if (query) {
loadAvailablePackages(query)
} else {
loadAvailablePackages()
}
}
// 检查套餐是否已添加
const isPackageAlreadyAdded = (packageId: number) => {
return detailData.value?.packages?.some((p) => p.package_id === packageId) || false
}
// 保存套餐
const handleSavePackage = async () => {
if (!packageFormRef.value || !detailData.value) return
await packageFormRef.value.validate(async (valid) => {
if (!valid) return
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)
})
}
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages })
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
packageDialogVisible.value = false
// 刷新详情数据
await fetchDetail()
} catch (error) {
console.error(error)
ElMessage.error(packageDialogType.value === 'add' ? '添加失败' : '更新失败')
} finally {
submitLoading.value = false
}
})
}
// 删除套餐
const handleDeletePackage = (row: GrantPackageInfo) => {
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
if (!detailData.value) return
submitLoading.value = true
try {
const packages: GrantPackageItem[] = [
{
package_id: row.package_id,
remove: true
}
]
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages })
ElMessage.success('删除成功')
// 刷新详情数据
await fetchDetail()
} catch (error) {
console.error(error)
ElMessage.error('删除失败')
} finally {
submitLoading.value = false
}
})
.catch(() => {
// 用户取消
})
}
// 关闭对话框
const handlePackageDialogClosed = () => {
packageFormRef.value?.resetFields()
packageForm.value = {
cost_price_yuan: 0
}
availablePackages.value = []
}
onMounted(() => {
fetchDetail()
})
</script>
<style scoped lang="scss">
.series-grants-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
margin-bottom: 20px;
border-bottom: 1px solid var(--el-border-color);
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.detail-content {
.info-section {
margin-bottom: 24px;
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.package-table {
margin-top: 12px;
}
.tier-table-wrapper {
.threshold-value {
font-weight: 500;
color: var(--el-text-color-primary);
}
}
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
.amount-value {
font-weight: 600;
color: var(--el-color-warning);
}
.dialog-footer {
text-align: right;
}
</style>