Files
one-pipe-system/src/views/package-management/series-grants/index.vue
sexygoat e04a283319
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m38s
fix: 新增代理系列授权-建议售价使用套餐
2026-06-12 10:41:37 +08:00

2008 lines
68 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"
:actions="getActions"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'"
width="50%"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<!-- 新增模式分步骤表单 -->
<template v-if="dialogType === 'add'">
<!-- 步骤条 -->
<ElSteps
:active="currentStep"
finish-status="success"
align-center
style="margin-bottom: 30px"
>
<ElStep title="基本信息" description="选择系列和店铺" />
<ElStep title="佣金和充值配置" description="设置佣金和强制充值" />
</ElSteps>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px">
<!-- 第一步基本信息 -->
<div v-show="currentStep === 0">
<ElRow :gutter="20">
<ElCol :span="24">
<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>
</ElRow>
<ElRow :gutter="20" v-if="form.series_id">
<ElCol :span="24">
<ElFormItem label="选择店铺" prop="shop_id">
<ElCascader
v-model="form.shop_id"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择或搜索店铺"
style="width: 100%"
filterable
clearable
:loading="shopLoading"
@change="handleShopChange"
/>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 套餐配置 -->
<div v-if="form.shop_id" class="form-section-title">
<span class="title-text">套餐配置可选</span>
</div>
<template v-if="form.shop_id">
<!-- 选择套餐 -->
<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"
:max="getPackageCostPriceMax(pkg)"
: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) }}, 建议售价:
{{ formatSuggestedRetailPrice(pkg.suggested_retail_price) }})
</span>
</div>
<ElButton type="danger" size="small" @click="removePackage(index)"
>删除</ElButton
>
</div>
</div>
<div class="form-tip"
>设置每个套餐的成本价单位不能低于成本价不能高于建议售价的50%溢价</div
>
</ElFormItem>
</template>
</div>
<!-- 第二步佣金和强制充值配置 -->
<div v-show="currentStep === 1">
<!-- 一次性佣金配置 -->
<template v-if="form.enable_one_time_commission">
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<!-- 佣金类型和金额 - 2列布局 -->
<ElRow :gutter="20">
<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 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"> 仅自己 </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>
</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"
:disabled="form.force_recharge_locked && isAgentAccount"
/>
</ElFormItem>
</ElCol>
<ElCol
:span="12"
v-if="
form.enable_force_recharge && !(form.force_recharge_locked && isAgentAccount)
"
>
<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>
</ElForm>
</template>
<!-- 编辑模式:原有的表单布局 -->
<ElForm v-else ref="formRef" :model="form" :rules="rules" label-width="130px">
<!-- 编辑模式:显示只读信息 -->
<div 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>
<!-- 一次性佣金配置 -->
<template v-if="form.enable_one_time_commission">
<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 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"> 仅自己 </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>
</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"
:disabled="form.force_recharge_locked && isAgentAccount"
/>
</ElFormItem>
</ElCol>
<ElCol
:span="12"
v-if="form.enable_force_recharge && !(form.force_recharge_locked && isAgentAccount)"
>
<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>
</ElForm>
<template #footer>
<div class="dialog-footer">
<template v-if="dialogType === 'add'">
<ElButton v-if="currentStep > 0" @click="handlePrevStep">上一步</ElButton>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton v-if="currentStep < 1" type="primary" @click="handleNextStep"
>下一步</ElButton
>
<ElButton
v-else
type="primary"
@click="handleSubmit(formRef)"
:loading="submitLoading"
>
提交
</ElButton>
</template>
<template v-else>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</template>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h, ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import {
ShopSeriesGrantService,
PackageSeriesService,
ShopService,
PackageManageService
} from '@/api/modules'
import {
ElMessage,
ElMessageBox,
ElSwitch,
ElTag,
ElRow,
ElCol,
ElSteps,
ElStep,
ElEmpty
} 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 { useUserStore } from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend,
STATUS_SELECT_OPTIONS,
getEnableStatusText
} from '@/config/constants'
defineOptions({ name: 'SeriesGrants' })
const { hasAuth } = useAuth()
const canUpdateSeriesGrantStatus = hasAuth('series_grants:update_status')
const router = useRouter()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isAgentAccount = computed(() => userInfo.value.user_type === 3)
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 currentStep = ref(0) // 当前步骤0 为第一步1 为第二步
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 shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
lazy: true,
checkStrictly: true, // 允许选择任意一级
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({
parent_id: parentId,
exclude_self: true
} as any)
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
// 搜索表单初始值
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: () =>
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: STATUS_SELECT_OPTIONS
}
])
// 分页
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' },
...(canUpdateSeriesGrantStatus ? [{ 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: '',
enable_one_time_commission: false, // 是否启用一次性佣金(从系列继承)
commission_type: 'fixed' as 'fixed' | 'tiered', // 佣金类型
one_time_commission_amount: undefined, // 固定佣金金额(元)
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,
force_recharge_locked: false, // 强充是否被套餐系列锁定
series_force_recharge_amount: 0, // 系列强充金额(元)
packages: [] as Array<{
package_id: number
cost_price: number
original_cost_price?: number
suggested_retail_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.enable_one_time_commission) {
// 根据佣金类型添加验证规则
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: '系列名称',
width: 200,
showOverflowTooltip: true,
formatter: (row: ShopSeriesGrantResponse) => {
const hasPermission = hasAuth('series_grants:detail')
return h(
'span',
{
style: hasPermission
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
: '',
onClick: hasPermission
? (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
: undefined
},
row.series_name
)
}
},
{
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' },
() => getEnableStatusText(row.force_recharge_enabled || false)
)
}
},
{
prop: 'force_recharge_locked',
label: '强充锁定',
width: 100,
formatter: (row: ShopSeriesGrantResponse) => {
return h(
ElTag,
{ type: row.force_recharge_locked ? 'danger' : 'success', size: 'small' },
() => (row.force_recharge_locked ? '是' : '否')
)
}
},
{
prop: 'force_recharge_amount',
label: '强充金额',
width: 100,
formatter: (row: ShopSeriesGrantResponse) => {
if (!row.force_recharge_enabled) return '-'
return `¥${((row.force_recharge_amount || 0) / 100).toFixed(2)}`
}
},
{
prop: 'package_count',
label: '套餐数量',
width: 100
},
...(canUpdateSeriesGrantStatus
? [
{
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,
'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 getActions = (row: ShopSeriesGrantResponse) => {
const actions: any[] = []
if (hasAuth('series_grants:packages_list')) {
actions.push({
label: '套餐列表',
handler: () => router.push({ name: 'SeriesGrantPackages', params: { id: row.id } }),
type: 'primary'
})
}
if (hasAuth('series_grants:edit')) {
actions.push({ label: '编辑', handler: () => showDialog('edit', row), type: 'primary' })
}
if (hasAuth('series_grants:delete')) {
actions.push({ label: '删除', handler: () => deleteAllocation(row), type: 'danger' })
}
return actions
}
// 构建树形结构数据
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
}
// 店铺树节点过滤方法
const filterShopNode = (value: string, data: any) => {
if (!value) return true
return data.shop_name.toLowerCase().includes(value.toLowerCase())
}
onMounted(() => {
getTableData()
})
let isFirstActivation = true
onActivated(() => {
if (!isFirstActivation) {
getTableData()
}
isFirstActivation = false
})
// 加载套餐选项(用于新增对话框)
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.filter((pkg) => !pkg.is_gift)
}
} 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}`
}
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
}
const formatSuggestedRetailPrice = (suggestedRetailPrice?: number | null) => {
if (suggestedRetailPrice === undefined || suggestedRetailPrice === null) {
return '-'
}
return `¥${suggestedRetailPrice.toFixed(2)}`
}
// 监听套餐选择变化
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, // 保存原始成本价用于验证
suggested_retail_price:
pkg?.suggested_retail_price !== undefined && pkg?.suggested_retail_price !== null
? pkg.suggested_retail_price / 100
: undefined
})
}
})
// 删除取消选中的套餐
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
// 获取系列是否启用一次性佣金
form.enable_one_time_commission = selectedSeries.enable_one_time_commission || false
// 从系列配置中获取佣金类型
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 = undefined
// 设置系列最大佣金金额(从分转换为元)
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 = undefined
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 = undefined
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 = undefined
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
}
}
// 加载搜索栏系列选项(默认加载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 loadShopListForDialog = async () => {
shopLoading.value = true
try {
const res = await ShopService.getShopsCascade({
parent_id: undefined,
exclude_self: true
} as any)
if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载店铺列表失败:', error)
} finally {
shopLoading.value = false
}
}
// 处理店铺选择变化
const handleShopChange = (value: any) => {
// 级联选择器返回的是数组,取最后一个值作为 shop_id
if (Array.isArray(value) && value.length > 0) {
form.shop_id = value[value.length - 1]
} else {
form.shop_id = undefined
}
}
// 搜索系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
if (!query) {
await 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) {
await 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) {
await 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)
console.log('获取系列分配列表失败')
} 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
// 每次打开对话框时重新加载店铺列表,确保获取最新的店铺数据
await loadShopListForDialog()
// 加载系列选项
await loadSeriesOptions()
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)
// 设置是否启用一次性佣金
form.enable_one_time_commission = selectedSeries?.enable_one_time_commission || false
// 设置佣金配置
if (detail.commission_type === 'fixed') {
const amount = detail.one_time_commission_amount / 100
form.one_time_commission_amount = amount > 0 ? amount : undefined
// 设置系列最大佣金金额
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
form.force_recharge_locked = detail.force_recharge_locked || false
// 设置套餐数据
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, // 保存原始成本价用于验证
suggested_retail_price:
pkgOption?.suggested_retail_price !== undefined &&
pkgOption?.suggested_retail_price !== null
? pkgOption.suggested_retail_price / 100
: undefined
}
})
selectedPackageIds.value = detail.packages.map((pkg) => pkg.package_id)
} else {
form.packages = []
selectedPackageIds.value = []
}
}
} catch (error) {
console.error('获取详情失败:', error)
console.log('获取详情失败')
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.enable_one_time_commission = false
form.commission_type = 'fixed'
form.one_time_commission_amount = undefined
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.force_recharge_locked = false
form.packages = []
selectedPackageIds.value = []
packageOptions.value = [] // 清空套餐选项
}
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 处理弹窗关闭事件
const handleDialogClosed = () => {
// 清除表单验证状态
formRef.value?.clearValidate()
// 重置步骤
currentStep.value = 0
// 重置表单数据
form.id = 0
form.series_id = undefined
form.shop_id = undefined
form.series_name = ''
form.shop_name = ''
form.allocator_shop_name = ''
form.enable_one_time_commission = false
form.commission_type = 'fixed'
form.one_time_commission_amount = undefined
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.force_recharge_locked = false
form.packages = []
selectedPackageIds.value = []
}
// 查看详情
const handleViewDetail = (row: ShopSeriesGrantResponse) => {
router.push(`/package-management/series-grants/detail/${row.id}`)
}
// 处理名称点击
const handleNameClick = (row: ShopSeriesGrantResponse) => {
if (hasAuth('series_grants:detail')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 删除分配
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.enable_one_time_commission) {
// 根据佣金类型携带不同参数
if (form.commission_type === 'fixed') {
// 固定模式:携带 one_time_commission_amount将元转换为分
data.one_time_commission_amount = Math.round(
(form.one_time_commission_amount || 0) * 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) // 将元转换为分
}))
}
}
// 强制充值配置(无论开关状态都要传递)
data.enable_force_recharge = form.enable_force_recharge
if (form.enable_force_recharge) {
// 启用时传递强充金额(分)
data.force_recharge_amount = Math.round((form.force_recharge_amount || 0) * 100)
} else {
// 关闭时传递 0
data.force_recharge_amount = 0
}
// 可选:套餐配置(将元转换为分)
if (form.packages.length > 0) {
// 验证是否包含赠送套餐
for (const pkg of form.packages) {
const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id)
if (pkgInfo?.is_gift) {
ElMessage.error(`套餐 ${getPackageName(pkg.package_id)} 是赠送套餐,不能参与授权`)
submitLoading.value = false
return
}
}
// 验证成本价是否在允许范围内
for (const pkg of form.packages) {
const costPrice = pkg.cost_price ?? 0
const originalCostPrice = pkg.original_cost_price ?? 0
const maxCostPrice = getPackageCostPriceMax(pkg)
if (maxCostPrice !== undefined && costPrice > maxCostPrice) {
ElMessage.error(
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能高于当前上限(最高:¥${maxCostPrice.toFixed(2)}`
)
submitLoading.value = false
return
}
if (originalCostPrice > 0 && costPrice < originalCostPrice) {
ElMessage.error(
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能低于原价(¥${originalCostPrice.toFixed(2)}`
)
submitLoading.value = false
return
}
}
data.packages = form.packages.map((pkg: any) => ({
package_id: pkg.package_id,
cost_price: Math.round((pkg.cost_price ?? 0) * 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 handleNextStep = async () => {
if (!formRef.value) return
// 根据当前步骤验证不同的字段
let fieldsToValidate: string[] = []
if (currentStep.value === 0) {
// 第一步:验证系列和店铺选择
fieldsToValidate = ['series_id', 'shop_id']
}
try {
// 验证当前步骤的字段
await formRef.value.validateField(fieldsToValidate)
// 验证通过,进入下一步
currentStep.value++
} catch (error) {
console.error('表单验证失败:', error)
}
}
// 上一步
const handlePrevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
</script>
<style lang="scss" scoped>
.series-grants-page {
// 可以添加特定样式
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.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 {
padding-bottom: 8px;
margin: 20px 0 16px;
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;
gap: 12px;
align-items: center;
.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;
gap: 8px;
align-items: center;
.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;
color: var(--el-text-color-regular);
white-space: nowrap;
}
.info-value {
font-size: 14px;
font-weight: 500;
color: var(--el-color-primary);
}
.readonly-value {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
}
</style>