Files
one-pipe-system/src/views/package-management/series-grants/index.vue
sexygoat 1ebc0b8929
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 4m36s
新增代理系列授权
2026-03-06 16:28:58 +08:00

1584 lines
51 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ArtTableFullScreen>
<div class="series-grants-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="true"
label-width="85"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showDialog('add')" v-permission="'series_grants:add'"
>新增代理系列授权</ElButton
>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="allocationList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'"
width="50%"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px">
<!-- 新增模式基本信息 - 2列布局 -->
<div v-if="dialogType === 'add'">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="选择套餐系列" prop="series_id">
<ElSelect
v-model="form.series_id"
placeholder="请选择套餐系列"
style="width: 100%"
filterable
remote
:remote-method="searchSeries"
:loading="seriesLoading"
clearable
>
<ElOption
v-for="series in seriesOptions"
:key="series.id"
:label="`${series.series_name} (${series.series_code})`"
:value="series.id"
/>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="选择店铺" prop="shop_id">
<ElTreeSelect
v-model="form.shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
placeholder="请选择店铺"
style="width: 100%"
filterable
clearable
:loading="shopLoading"
check-strictly
:render-after-expand="false"
/>
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 编辑模式显示只读信息 -->
<div v-if="dialogType === 'edit'" class="info-row">
<div class="info-item">
<span class="info-label">系列名称:</span>
<span class="info-value">{{ form.series_name || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">店铺名称:</span>
<span class="info-value">{{ form.shop_name || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">分配者店铺:</span>
<span class="info-value">{{ form.allocator_shop_name || '-' }}</span>
</div>
</div>
<!-- 一次性佣金配置 -->
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<!-- 佣金类型和金额 - 2列布局 -->
<ElRow :gutter="20" v-if="form.series_id">
<ElCol :span="12">
<ElFormItem label="佣金类型">
<div class="commission-type-display">
<ElTag
:type="form.commission_type === 'fixed' ? 'success' : 'warning'"
size="large"
>
{{ form.commission_type === 'fixed' ? '固定佣金' : '梯度佣金' }}
</ElTag>
<span class="type-hint">从套餐系列配置继承</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.commission_type === 'fixed'">
<ElFormItem label="佣金金额(元)" prop="one_time_commission_amount">
<ElInputNumber
v-model="form.one_time_commission_amount"
:min="0"
:max="form.series_max_commission_amount"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入固定佣金金额(元)"
/>
<div class="form-tip">
该代理能获得的固定佣金金额单位
<span v-if="form.series_max_commission_amount > 0" class="max-amount-hint">
<br />
该系列最大佣金金额
<span class="amount-value"
>¥{{ form.series_max_commission_amount.toFixed(2) }}</span
>
</span>
</div>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 梯度佣金配置 -->
<template v-if="form.commission_type === 'tiered'">
<ElFormItem label="梯度配置" prop="commission_tiers">
<ElTable :data="form.commission_tiers" border style="width: 100%">
<ElTableColumn label="比较运算符" width="100" align="center">
<template #default="{ row }">
<ElTag size="small" type="success">{{ row.operator || '>=' }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="达标阈值" width="120">
<template #default="{ row }">
<span class="readonly-value">{{ row.threshold }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="统计维度" width="120">
<template #default="{ row }">
<ElTag size="small" type="info">
{{ row.dimension === 'sales_count' ? '销量' : '销售额' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="统计范围" width="140">
<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="180">
<template #default="{ row }">
<div style="display: flex; flex-direction: column; gap: 4px">
<ElInputNumber
v-model="row.amount"
:min="0"
:max="row.max_amount"
:precision="2"
:step="0.01"
:controls="false"
placeholder="请输入佣金金额"
style="width: 100%"
/>
<span
v-if="row.max_amount"
style="font-size: 12px; color: var(--el-text-color-secondary)"
>
最大: ¥{{ row.max_amount.toFixed(2) }}
</span>
</div>
</template>
</ElTableColumn>
</ElTable>
<div class="form-tip" style="margin-top: 8px">
梯度配置从套餐系列继承达标阈值统计维度统计范围为只读只能修改佣金金额
</div>
</ElFormItem>
</template>
<!-- 强制充值配置 -->
<div class="form-section-title">
<span class="title-text">强制充值配置可选</span>
</div>
<!-- 启用强制充值和强充金额 - 2列布局 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="启用强制充值">
<ElSwitch v-model="form.enable_force_recharge" />
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.enable_force_recharge">
<ElFormItem label="强充金额(元)" prop="force_recharge_amount">
<ElInputNumber
v-model="form.force_recharge_amount"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入强制充值金额(元)"
/>
<div class="form-tip">
用户需要达到的强制充值金额
<span
v-if="form.series_name && form.series_force_recharge_amount > 0"
class="series-force-hint"
>
<br />
可参考
<span class="amount-value">{{ form.series_name }}</span
>系列强充金额
<span class="amount-value"
>¥{{ form.series_force_recharge_amount.toFixed(2) }}</span
>
</span>
</div>
</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>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import {
ShopSeriesGrantService,
PackageSeriesService,
ShopService,
PackageManageService
} from '@/api/modules'
import { ElMessage, ElMessageBox, ElSwitch, ElTag, ElRow, ElCol } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
ShopSeriesGrantResponse,
PackageSeriesResponse,
ShopResponse,
PackageResponse,
CommissionTier
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend
} from '@/config/constants'
defineOptions({ name: 'SeriesGrants' })
const { hasAuth } = useAuth()
const router = useRouter()
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const seriesLoading = ref(false)
const shopLoading = ref(false)
const packageLoading = ref(false)
const tableRef = ref()
const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopSeriesGrantResponse | null>(null)
const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([])
const packageOptions = ref<PackageResponse[]>([])
const selectedPackageIds = ref<number[]>([])
const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
const searchShopOptions = ref<ShopResponse[]>([])
const searchAllocatorShopOptions = ref<ShopResponse[]>([])
// 搜索表单初始值
const initialSearchState = {
shop_id: undefined as number | undefined,
series_id: undefined as number | undefined,
allocator_shop_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '被分配店铺',
prop: 'shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchShop,
loading: shopLoading.value,
placeholder: '请选择或搜索被分配的店铺'
},
options: () =>
searchShopOptions.value.map((s) => ({
label: s.shop_name,
value: s.id
}))
},
{
label: '分配者店铺',
prop: 'allocator_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchAllocatorShop,
loading: shopLoading.value,
placeholder: '请选择或搜索分配者店铺'
},
options: () => [
{ label: '平台', value: 0 },
...searchAllocatorShopOptions.value.map((s) => ({
label: s.shop_name,
value: s.id
}))
]
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeries,
loading: seriesLoading.value,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 2 }
]
}
])
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '系列名称', prop: 'series_name' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '分配者店铺', prop: 'allocator_shop_name' },
{ label: '佣金类型', prop: 'commission_type' },
{ label: '一次性佣金金额', prop: 'one_time_commission_amount' },
{ label: '强制充值', prop: 'force_recharge_enabled' },
{ label: '套餐数量', prop: 'package_count' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }
]
// 表单数据
const form = reactive<any>({
id: 0,
series_id: undefined,
shop_id: undefined,
series_name: '',
shop_name: '',
allocator_shop_name: '',
commission_type: 'fixed' as 'fixed' | 'tiered', // 佣金类型
one_time_commission_amount: 0, // 固定佣金金额(元)
series_max_commission_amount: 0, // 系列最大佣金金额(元)
commission_tiers: [] as Array<{
operator?: '>=' | '>' | '<=' | '<' // 比较运算符
threshold: number // 达标阈值
dimension: 'sales_count' | 'sales_amount' // 统计维度
stat_scope?: 'self' | 'self_and_sub' // 统计范围
amount: number // 佣金金额(元)- 这是唯一可编辑的字段
max_amount?: number // 系列配置的最大佣金金额(元)- 用于验证
}>, // 梯度配置(包含完整字段,从系列继承)
enable_force_recharge: false,
force_recharge_amount: undefined,
series_force_recharge_amount: 0, // 系列强充金额(元)
packages: [] as Array<{ package_id: number; cost_price: number }> // 套餐配置
})
// 动态验证规则
const rules = computed<FormRules>(() => {
const baseRules: FormRules = {
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }]
}
// 根据佣金类型添加验证规则
if (form.commission_type === 'fixed') {
baseRules.one_time_commission_amount = [
{ 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 (
form.series_max_commission_amount > 0 &&
value > form.series_max_commission_amount
) {
callback(
new Error(
`佣金金额不能超过该系列最大值 ¥${form.series_max_commission_amount.toFixed(2)}`
)
)
} else {
callback()
}
},
trigger: 'blur'
}
]
} else if (form.commission_type === 'tiered') {
baseRules.commission_tiers = [
{
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少添加一个梯度配置'))
return
}
// 验证每个梯度的佣金金额不超过最大值
for (let i = 0; i < value.length; i++) {
const tier = value[i]
if (tier.max_amount && tier.amount > tier.max_amount) {
callback(
new Error(
`${i + 1}个梯度的佣金金额¥${tier.amount.toFixed(2)}不能超过系列配置的最大值¥${tier.max_amount.toFixed(2)}`
)
)
return
}
}
callback()
},
trigger: 'change'
}
]
}
// 如果启用了强制充值,添加验证规则
if (form.enable_force_recharge) {
baseRules.force_recharge_amount = [
{ required: true, message: '请输入强制充值金额', trigger: 'blur' }
]
}
return baseRules
})
const allocationList = ref<ShopSeriesGrantResponse[]>([])
const dialogType = ref('add')
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'series_name',
label: '系列名称',
minWidth: 150
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 140
},
{
prop: 'allocator_shop_name',
label: '分配者店铺',
minWidth: 120,
formatter: (row: ShopSeriesGrantResponse) => {
// 如果是平台分配(allocator_shop_id为0),显示"平台"标签
if (row.allocator_shop_id === 0) {
return h(
'span',
{ style: 'color: #409eff; font-weight: bold' },
row.allocator_shop_name || '平台'
)
}
return row.allocator_shop_name || '-'
}
},
{
prop: 'commission_type',
label: '佣金类型',
width: 110,
formatter: (row: ShopSeriesGrantResponse) => {
const typeMap = {
fixed: '固定佣金',
tiered: '梯度佣金'
}
return h(
ElTag,
{ type: row.commission_type === 'fixed' ? 'success' : 'warning', size: 'small' },
() => typeMap[row.commission_type] || '-'
)
}
},
{
prop: 'one_time_commission_amount',
label: '一次性佣金金额',
width: 140,
formatter: (row: ShopSeriesGrantResponse) => {
if (row.commission_type === 'fixed' && row.one_time_commission_amount > 0) {
return h(
'span',
{ style: 'color: #f56c6c; font-weight: bold' },
`¥${(row.one_time_commission_amount / 100).toFixed(2)}`
)
}
return '-'
}
},
{
prop: 'force_recharge_enabled',
label: '强制充值',
width: 100,
formatter: (row: ShopSeriesGrantResponse) => {
return h(
ElTag,
{ type: row.force_recharge_enabled ? 'warning' : 'info', size: 'small' },
() => (row.force_recharge_enabled ? '已启用' : '未启用')
)
}
},
{
prop: 'package_count',
label: '套餐数量',
width: 100
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: ShopSeriesGrantResponse) => {
const frontendStatus = apiStatusToFrontend(row.status)
return h(ElSwitch, {
modelValue: frontendStatus,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
disabled: !hasAuth('series_grants:update_status'),
'onUpdate:modelValue': (val: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: ShopSeriesGrantResponse) => formatDateTime(row.created_at)
}
])
// 构建树形结构数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
onMounted(() => {
loadSeriesOptions()
loadShopOptions()
loadSearchSeriesOptions()
loadSearchShopOptions()
loadSearchAllocatorShopOptions()
getTableData()
})
// 加载套餐选项(用于新增对话框)
const loadPackageOptions = async (packageName?: string) => {
packageLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20
}
// 如果已选择套餐系列则根据系列ID过滤套餐
if (form.series_id) {
params.series_id = form.series_id
}
if (packageName) {
params.package_name = packageName
}
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
packageOptions.value = res.data.items
}
} catch (error) {
console.error('加载套餐选项失败:', error)
} finally {
packageLoading.value = false
}
}
// 搜索套餐
const searchPackages = (query: string) => {
if (query) {
loadPackageOptions(query)
} else {
loadPackageOptions()
}
}
// 移除套餐
const removePackage = (index: number) => {
form.packages.splice(index, 1)
// 同步更新选中的套餐ID
const removedPackageId = form.packages[index]?.package_id
if (removedPackageId) {
const idIndex = selectedPackageIds.value.indexOf(removedPackageId)
if (idIndex > -1) {
selectedPackageIds.value.splice(idIndex, 1)
}
}
}
// 获取套餐名称
const getPackageName = (packageId: number) => {
const pkg = packageOptions.value.find((p) => p.id === packageId)
return pkg ? pkg.package_name : `套餐ID: ${packageId}`
}
// 监听套餐选择变化
watch(selectedPackageIds, (newIds, oldIds) => {
// 找出新增的ID
const addedIds = newIds.filter((id) => !oldIds?.includes(id))
// 找出删除的ID
const removedIds = oldIds?.filter((id) => !newIds.includes(id)) || []
// 添加新选中的套餐
addedIds.forEach((id) => {
if (!form.packages.find((p: any) => p.package_id === id)) {
// 从 packageOptions 中查找套餐,获取其成本价
const pkg = packageOptions.value.find((p) => p.id === id)
const costPrice = pkg?.cost_price ? pkg.cost_price / 100 : 0 // 将分转换为元
form.packages.push({
package_id: id,
cost_price: costPrice,
original_cost_price: costPrice // 保存原始成本价用于验证
})
}
})
// 删除取消选中的套餐
removedIds.forEach((id) => {
const index = form.packages.findIndex((p: any) => p.package_id === id)
if (index > -1) {
form.packages.splice(index, 1)
}
})
})
// 监听套餐系列选择变化,重新加载套餐列表并获取佣金类型
watch(
() => form.series_id,
async (newSeriesId, oldSeriesId) => {
// 只在新增模式且系列ID发生变化时处理
if (dialogType.value === 'add' && newSeriesId !== oldSeriesId) {
// 清空已选套餐
form.packages = []
selectedPackageIds.value = []
packageOptions.value = []
// 如果选择了系列
if (newSeriesId) {
// 获取系列详情,读取佣金配置
const selectedSeries = seriesOptions.value.find((s) => s.id === newSeriesId)
if (selectedSeries) {
// 设置系列名称
form.series_name = selectedSeries.series_name
// 从系列配置中获取佣金类型
const commissionConfig = selectedSeries.one_time_commission_config
if (commissionConfig && commissionConfig.commission_type) {
form.commission_type = commissionConfig.commission_type
// 根据佣金类型初始化对应字段
if (commissionConfig.commission_type === 'fixed') {
form.one_time_commission_amount = 0
// 设置系列最大佣金金额(从分转换为元)
form.series_max_commission_amount = commissionConfig.commission_amount
? commissionConfig.commission_amount / 100
: 0
form.commission_tiers = [{ threshold: 0, amount: 0 }]
} else if (commissionConfig.commission_type === 'tiered') {
form.one_time_commission_amount = 0
form.series_max_commission_amount = 0
// 梯度配置从系列继承,包含完整字段,佣金金额也默认继承
if (commissionConfig.tiers && commissionConfig.tiers.length > 0) {
form.commission_tiers = commissionConfig.tiers.map((tier) => ({
operator: tier.operator || '>=', // 比较运算符(只读)
threshold: tier.threshold, // 达标阈值(只读)
dimension: tier.dimension, // 统计维度(只读)
stat_scope: tier.stat_scope, // 统计范围(只读)
amount: tier.amount / 100, // 佣金金额(元)- 默认显示系列的值,可编辑
max_amount: tier.amount / 100 // 系列配置的最大佣金金额(元)- 用于验证
}))
} else {
form.commission_tiers = []
}
}
// 设置系列强充金额(从分转换为元)
if (commissionConfig.force_amount) {
form.series_force_recharge_amount = commissionConfig.force_amount / 100
} else {
form.series_force_recharge_amount = 0
}
} else {
// 默认固定佣金
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
}
}
// 重新加载该系列下的套餐
loadPackageOptions()
} else {
// 未选择系列,重置佣金类型
form.series_name = ''
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
form.commission_tiers = []
}
}
}
)
// 监听启用强制充值开关,自动设置默认强充金额
watch(
() => form.enable_force_recharge,
(enabled) => {
if (enabled && form.series_force_recharge_amount > 0 && !form.force_recharge_amount) {
// 启用强充时,如果系列有强充金额且用户未输入,则使用系列的强充金额作为默认值
form.force_recharge_amount = form.series_force_recharge_amount
}
}
)
// 加载系列选项(用于新增对话框,默认加载10条
const loadSeriesOptions = async (seriesName?: string) => {
seriesLoading.value = true
try {
const params: any = {
page: 1,
page_size: 10,
status: 1
}
if (seriesName) {
params.series_name = seriesName
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
seriesOptions.value = res.data.items
}
} catch (error) {
console.error('加载系列选项失败:', error)
} finally {
seriesLoading.value = false
}
}
// 加载店铺选项(用于新增对话框,加载所有店铺并构建树形结构)
const loadShopOptions = async () => {
shopLoading.value = true
try {
// 加载所有店铺,不分页
const res = await ShopService.getShops({
page: 1,
page_size: 10000 // 使用较大的值获取所有店铺
})
if (res.code === 0) {
shopOptions.value = res.data.items
// 构建树形结构数据
shopTreeData.value = buildTreeData(shopOptions.value)
}
} catch (error) {
console.error('加载店铺选项失败:', error)
} finally {
shopLoading.value = false
}
}
// 加载搜索栏系列选项(默认加载10条)
const loadSearchSeriesOptions = async () => {
try {
const res = await PackageSeriesService.getPackageSeries({
page: 1,
page_size: 10,
status: 1
})
if (res.code === 0) {
searchSeriesOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏系列选项失败:', error)
}
}
// 加载搜索栏店铺选项(默认加载10条)
const loadSearchShopOptions = async () => {
try {
const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) {
searchShopOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏店铺选项失败:', error)
}
}
// 加载搜索栏分配者店铺选项(默认加载10条)
const loadSearchAllocatorShopOptions = async () => {
try {
const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏分配者店铺选项失败:', error)
}
}
// 搜索系列(用于新增对话框)
const searchSeries = (query: string) => {
if (query) {
loadSeriesOptions(query)
} else {
loadSeriesOptions()
}
}
// 搜索系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
if (!query) {
loadSearchSeriesOptions()
return
}
try {
const res = await PackageSeriesService.getPackageSeries({
page: 1,
page_size: 10,
series_name: query,
status: 1
})
if (res.code === 0) {
searchSeriesOptions.value = res.data.items
}
} catch (error) {
console.error('搜索系列失败:', error)
}
}
// 搜索店铺(用于搜索栏-被分配的店铺)
const handleSearchShop = async (query: string) => {
if (!query) {
loadSearchShopOptions()
return
}
try {
const res = await ShopService.getShops({
page: 1,
page_size: 10,
shop_name: query
})
if (res.code === 0) {
searchShopOptions.value = res.data.items
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
// 搜索分配者店铺(用于搜索栏)
const handleSearchAllocatorShop = async (query: string) => {
if (!query) {
loadSearchAllocatorShopOptions()
return
}
try {
const res = await ShopService.getShops({
page: 1,
page_size: 10,
shop_name: query
})
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items
}
} catch (error) {
console.error('搜索分配者店铺失败:', error)
}
}
// 获取分配列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.page_size,
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
allocator_shop_id:
searchForm.allocator_shop_id !== undefined ? searchForm.allocator_shop_id : undefined,
status: searchForm.status || undefined
}
const res = await ShopSeriesGrantService.getShopSeriesGrants(params)
if (res.code === 0) {
allocationList.value = res.data.items
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error('获取系列分配列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示新增/编辑对话框
const showDialog = async (type: string, row?: ShopSeriesGrantResponse) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式:先调用详情接口获取完整数据
loading.value = true
try {
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(row.id)
if (res.code === 0) {
const detail = res.data
// 设置基本信息
form.id = detail.id
form.series_id = detail.series_id
form.shop_id = detail.shop_id
form.series_name = detail.series_name
form.shop_name = detail.shop_name
form.allocator_shop_name = detail.allocator_shop_name
form.commission_type = detail.commission_type
// 获取系列配置信息
const selectedSeries = seriesOptions.value.find((s) => s.id === detail.series_id)
// 设置佣金配置
if (detail.commission_type === 'fixed') {
form.one_time_commission_amount = detail.one_time_commission_amount / 100
// 设置系列最大佣金金额
if (selectedSeries?.one_time_commission_config?.commission_amount) {
form.series_max_commission_amount =
selectedSeries.one_time_commission_config.commission_amount / 100
} else {
form.series_max_commission_amount = 0
}
} else if (detail.commission_type === 'tiered' && detail.commission_tiers) {
// 梯度配置:从响应中获取完整数据
// 获取系列配置以获取 dimension 和 stat_scope
const selectedSeries = seriesOptions.value.find((s) => s.id === detail.series_id)
const seriesTiers = selectedSeries?.one_time_commission_config?.tiers || []
form.commission_tiers = detail.commission_tiers.map((tier, index) => {
const seriesTier = seriesTiers[index]
return {
operator: tier.operator || seriesTier?.operator || '>=', // 比较运算符(只读,优先使用授权的,否则从系列获取)
threshold: tier.threshold, // 达标阈值(只读)
dimension: seriesTier?.dimension || 'sales_count', // 统计维度(只读,从系列获取)
stat_scope: seriesTier?.stat_scope, // 统计范围(只读,从系列获取)
amount: tier.amount / 100, // 佣金金额(元)- 可编辑,将分转换为元
max_amount: seriesTier?.amount ? seriesTier.amount / 100 : undefined // 系列配置的最大佣金金额(元)- 用于验证
}
})
form.series_max_commission_amount = 0
}
// 设置系列强充金额
if (selectedSeries?.one_time_commission_config?.force_amount) {
form.series_force_recharge_amount =
selectedSeries.one_time_commission_config.force_amount / 100
} else {
form.series_force_recharge_amount = 0
}
// 设置强制充值配置
form.enable_force_recharge = detail.force_recharge_enabled
form.force_recharge_amount = detail.force_recharge_amount
? detail.force_recharge_amount / 100
: undefined
// 设置套餐数据
if (detail.packages && detail.packages.length > 0) {
// 加载该系列下的套餐选项,以便获取原始成本价
if (detail.series_id) {
await loadPackageOptions()
}
form.packages = detail.packages.map((pkg) => {
// 从 packageOptions 中查找套餐以获取原始成本价
const pkgOption = packageOptions.value.find((p) => p.id === pkg.package_id)
const originalCostPrice = pkgOption?.cost_price ? pkgOption.cost_price / 100 : 0
return {
package_id: pkg.package_id,
cost_price: pkg.cost_price / 100, // 将分转换为元
original_cost_price: originalCostPrice // 保存原始成本价用于验证
}
})
selectedPackageIds.value = detail.packages.map((pkg) => pkg.package_id)
} else {
form.packages = []
selectedPackageIds.value = []
}
}
} catch (error) {
console.error('获取详情失败:', error)
ElMessage.error('获取详情失败')
dialogVisible.value = false
} finally {
loading.value = false
}
} else {
// 新增模式:重置表单
form.id = 0
form.series_id = undefined
form.shop_id = undefined
form.series_name = ''
form.shop_name = ''
form.allocator_shop_name = ''
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
form.commission_tiers = []
form.enable_force_recharge = false
form.force_recharge_amount = undefined
form.packages = []
selectedPackageIds.value = []
packageOptions.value = [] // 清空套餐选项
}
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 处理弹窗关闭事件
const handleDialogClosed = () => {
// 清除表单验证状态
formRef.value?.clearValidate()
// 重置表单数据
form.id = 0
form.series_id = undefined
form.shop_id = undefined
form.series_name = ''
form.shop_name = ''
form.allocator_shop_name = ''
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
form.commission_tiers = []
form.enable_force_recharge = false
form.force_recharge_amount = undefined
form.packages = []
selectedPackageIds.value = []
}
// 查看详情
const handleViewDetail = (row: ShopSeriesGrantResponse) => {
router.push(`/package-management/series-grants/detail/${row.id}`)
}
// 删除分配
const deleteAllocation = (row: ShopSeriesGrantResponse) => {
ElMessageBox.confirm(
`确定删除系列 ${row.series_name} 对店铺 ${row.shop_name} 的授权吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}
)
.then(async () => {
try {
await ShopSeriesGrantService.deleteShopSeriesGrant(row.id)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
// 构建请求数据
const data: any = {}
// 根据佣金类型携带不同参数
if (form.commission_type === 'fixed') {
// 固定模式:携带 one_time_commission_amount将元转换为分
data.one_time_commission_amount = Math.round(form.one_time_commission_amount * 100)
} else if (form.commission_type === 'tiered') {
// 梯度模式:携带 commission_tiers将元转换为分
// 注意:请求时不传 operatoroperator 是响应中从 PackageSeries 合并过来的
data.commission_tiers = form.commission_tiers.map((tier: CommissionTier) => ({
threshold: tier.threshold,
amount: Math.round(tier.amount * 100) // 将元转换为分
}))
}
// 可选:强制充值配置
if (form.enable_force_recharge) {
data.enable_force_recharge = true
data.force_recharge_amount = Math.round((form.force_recharge_amount || 0) * 100)
}
// 可选:套餐配置(将元转换为分)
if (form.packages.length > 0) {
data.packages = form.packages.map((pkg: any) => ({
package_id: pkg.package_id,
cost_price: Math.round(pkg.cost_price * 100) // 将元转换为分
}))
}
if (dialogType.value === 'add') {
// 新增时需要必填字段
data.series_id = form.series_id
data.shop_id = form.shop_id
await ShopSeriesGrantService.createShopSeriesGrant(data)
ElMessage.success('新增成功')
} else {
// 编辑时调用更新接口
await ShopSeriesGrantService.updateShopSeriesGrant(form.id, data)
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 状态切换
const handleStatusChange = async (row: ShopSeriesGrantResponse, newFrontendStatus: number) => {
const oldStatus = row.status
const newApiStatus = frontendStatusToApi(newFrontendStatus)
row.status = newApiStatus
try {
await ShopSeriesGrantService.updateShopSeriesGrant(row.id, { status: newApiStatus } as any)
ElMessage.success('状态切换成功')
} catch (error) {
row.status = oldStatus
console.error(error)
}
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('series_grants:detail')) {
items.push({ key: 'detail', label: '详情' })
}
if (hasAuth('series_grants:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('series_grants:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: ShopSeriesGrantResponse, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
handleViewDetail(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAllocation(currentRow.value)
break
}
}
</script>
<style lang="scss" scoped>
.series-grants-page {
// 可以添加特定样式
}
.dialog-footer {
text-align: right;
}
.form-tip {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
.max-amount-hint {
color: var(--el-text-color-regular);
.amount-value {
font-weight: 600;
color: var(--el-color-warning);
}
}
.series-force-hint {
color: var(--el-text-color-regular);
.amount-value {
font-weight: 600;
color: var(--el-color-primary);
}
}
}
.form-section-title {
margin: 20px 0 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--el-border-color);
.title-text {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.commission-type-display {
display: flex;
align-items: center;
gap: 12px;
.type-hint {
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
.tier-list {
width: 100%;
}
.tier-item {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
.tier-label {
font-size: 14px;
color: var(--el-text-color-regular);
white-space: nowrap;
}
}
.package-list {
width: 100%;
}
.package-item {
display: flex;
gap: 12px;
align-items: center;
padding: 8px;
margin-bottom: 8px;
background-color: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
transition:
background-color 0.3s,
border-color 0.3s;
&:hover {
background-color: var(--el-fill-color);
}
.package-name {
flex: 1;
font-size: 14px;
color: var(--el-text-color-regular);
}
.cost-price-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
.min-cost-hint {
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
}
}
}
.info-row {
display: flex;
gap: 20px;
padding: 12px;
margin-bottom: 18px;
background-color: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
}
.info-item {
display: flex;
flex: 1;
align-items: center;
}
.info-label {
margin-right: 8px;
font-size: 14px;
white-space: nowrap;
color: var(--el-text-color-regular);
}
.info-value {
font-size: 14px;
font-weight: 500;
color: var(--el-color-primary);
}
.readonly-value {
font-size: 14px;
color: var(--el-text-color-primary);
font-weight: 500;
}
</style>