Files
mp-10584/src/views/shop/shopDealerSetting/index.vue
赵忠林 355eada582 refactor(shop): 重构分销商设置存储结构并优化数据处理逻辑
- 将单个设置对象改为按类型分组的映射结构,支持基础设置、分销商条件、结算等功能模块
- 新增 getBooleanFlag、getNumberFlag、pickEnumValue 等工具函数统一数据转换逻辑
- 实现 ensurePath 和 setWordValue 函数用于深层对象路径操作和文案配置
- 优化图片上传相关函数 getUploadUrl 和 toUploadFileList 的 URL 获取逻辑
- 将整体保存逻辑拆分为按设置类型分别保存的 saveSettingItem 方法
- 更新分销商类型显示,将"配送员"修改为"总分销商"以符合业务需求
- 调整精度控制从4位小数改为3位小数以匹配实际业务场景
2026-02-05 16:10:37 +08:00

626 lines
19 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>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '24px' }">
<!-- 设置标签页 -->
<a-tabs v-model:activeKey="activeTab" type="card" class="setting-tabs">
<a-tab-pane key="basic" tab="基础设置">
<a-form
:model="basicSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 是否开启分销功能 -->
<a-form-item label="是否开启分销功能">
<a-radio-group v-model:value="basicSettings.enableDistribution">
<a-radio :value="true">开启</a-radio>
<a-radio :value="false">关闭</a-radio>
</a-radio-group>
<div class="setting-desc">开启后用户可以申请成为分销商</div>
</a-form-item>
<!-- 分销层级 -->
<a-form-item label="分销层级">
<a-radio-group v-model:value="basicSettings.distributionLevel">
<a-radio :value="1">一级</a-radio>
<a-radio :value="2">二级</a-radio>
<a-radio :value="3">三级</a-radio>
</a-radio-group>
<div class="setting-desc">设置分销商推荐层级关系</div>
</a-form-item>
<!-- 分销商内购 -->
<a-form-item label="分销商内购">
<a-radio-group v-model:value="basicSettings.dealerSelfBuy">
<a-radio :value="true">开启</a-radio>
<a-radio :value="false">关闭</a-radio>
</a-radio-group>
<div class="setting-desc"
>分销商自己购买是否获得佣金开启一般佣金</div
>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="commission" tab="分销条件">
<a-form
:model="commissionSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 申请方式 -->
<a-form-item label="申请方式">
<a-radio-group v-model:value="commissionSettings.applyType">
<a-radio :value="10">需后台审核</a-radio>
<a-radio :value="20">无需审核</a-radio>
</a-radio-group>
<div class="setting-desc">设置用户申请分销商的审核方式</div>
</a-form-item>
<!-- 佣金结算 -->
<a-form-item label="佣金结算">
<a-radio-group v-model:value="commissionSettings.settlementType">
<a-radio :value="10">订单完成</a-radio>
<a-radio :value="20">订单确认收货</a-radio>
</a-radio-group>
<div class="setting-desc">设置佣金何时结算到分销商账户</div>
</a-form-item>
<!-- 最低提现金额 -->
<a-form-item label="最低提现金额">
<a-input-number
v-model:value="commissionSettings.minWithdrawAmount"
:min="0"
:precision="2"
style="width: 200px"
>
<template #addonAfter></template>
</a-input-number>
<div class="setting-desc">分销商申请提现的最低金额限制</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="withdraw" tab="提现设置">
<a-form
:model="withdrawSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 提现方式 -->
<a-form-item label="提现方式">
<a-checkbox-group
v-model:value="withdrawSettings.withdrawMethods"
>
<a-checkbox :value="10">微信</a-checkbox>
<a-checkbox :value="20">支付宝</a-checkbox>
<a-checkbox :value="30">银行卡</a-checkbox>
</a-checkbox-group>
<div class="setting-desc">设置支持的提现方式</div>
</a-form-item>
<!-- 提现手续费 -->
<a-form-item label="提现手续费">
<a-input-number
v-model:value="withdrawSettings.withdrawFeeRate"
:min="0"
:max="100"
:precision="2"
style="width: 200px"
>
<template #addonAfter>%</template>
</a-input-number>
<div class="setting-desc">提现时收取的手续费比例</div>
</a-form-item>
<!-- 提现审核 -->
<a-form-item label="提现审核">
<a-radio-group v-model:value="withdrawSettings.withdrawAudit">
<a-radio :value="true">需要审核</a-radio>
<a-radio :value="false">无需审核</a-radio>
</a-radio-group>
<div class="setting-desc">设置提现申请是否需要人工审核</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="agreement" tab="协议">
<a-form
:model="agreementSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 分销商协议 -->
<a-form-item label="分销商协议">
<a-textarea
v-model:value="agreementSettings.dealerAgreement"
:rows="10"
placeholder="请输入分销商协议内容..."
/>
<div class="setting-desc">用户申请分销商时需要同意的协议内容</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="notification" tab="自定义文字">
<a-form
:model="notificationSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 申请成功提示 -->
<a-form-item label="申请成功提示">
<a-textarea
v-model:value="notificationSettings.applySuccessText"
:rows="3"
placeholder="请输入申请成功后的提示文字..."
/>
</a-form-item>
<!-- 申请失败提示 -->
<a-form-item label="申请失败提示">
<a-textarea
v-model:value="notificationSettings.applyFailText"
:rows="3"
placeholder="请输入申请失败后的提示文字..."
/>
</a-form-item>
<!-- 提现成功提示 -->
<a-form-item label="提现成功提示">
<a-textarea
v-model:value="notificationSettings.withdrawSuccessText"
:rows="3"
placeholder="请输入提现成功后的提示文字..."
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="page" tab="页面设置">
<a-form
:model="pageSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 分销中心标题 -->
<a-form-item label="分销中心标题">
<a-input
v-model:value="pageSettings.centerTitle"
placeholder="请输入分销中心页面标题"
/>
</a-form-item>
<!-- 分销中心背景图 -->
<a-form-item label="分销中心背景图">
<a-upload
v-model:file-list="pageSettings.backgroundImages"
list-type="picture-card"
:max-count="1"
@preview="handlePreview"
>
<div v-if="pageSettings.backgroundImages.length < 1">
<PlusOutlined />
<div style="margin-top: 8px">上传</div>
</div>
</a-upload>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
<!-- 保存按钮 -->
<div class="setting-footer">
<a-button
type="primary"
size="large"
@click="saveSettings"
:loading="saving"
>
保存设置
</a-button>
</div>
</a-card>
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { getPageTitle } from '@/utils/common';
import {
addShopDealerSetting,
updateShopDealerSetting,
listShopDealerSetting
} from '@/api/shop/shopDealerSetting';
import type { ShopDealerSetting } from '@/api/shop/shopDealerSetting/model';
// 当前激活的标签页
const activeTab = ref('basic');
// 保存状态
const saving = ref(false);
const settingMap = ref<Record<string, ShopDealerSetting>>({});
const settingValuesMap = ref<Record<string, any>>({});
const settingMeta = {
basic: '基础设置',
condition: '分销商条件',
settlement: '结算',
license: '申请协议',
words: '自定义文字',
background: '页面背景图'
};
// 基础设置
const basicSettings = reactive({
enableDistribution: true,
distributionLevel: 3,
dealerSelfBuy: false
});
// 分销条件设置
const commissionSettings = reactive({
applyType: 10,
settlementType: 10,
minWithdrawAmount: 100
});
// 提现设置
const withdrawSettings = reactive({
withdrawMethods: [10, 20, 30],
withdrawFeeRate: 0,
withdrawAudit: true
});
// 协议设置
const agreementSettings = reactive({
dealerAgreement: '分销商协议内容...'
});
// 通知设置
const notificationSettings = reactive({
applySuccessText: '恭喜您成功成为分销商!',
applyFailText: '很抱歉,您的申请未通过审核。',
withdrawSuccessText: '提现申请已提交,请耐心等待处理。'
});
// 页面设置
const pageSettings = reactive({
centerTitle: '分销中心',
backgroundImages: []
});
const getBooleanFlag = (value: any) => value === 1 || value === true;
const getNumberFlag = (value: any) => (value ? 1 : 0);
const pickEnumValue = (value: any, options: number[], fallback: number) =>
options.includes(value) ? value : fallback;
const ensurePath = (root: Record<string, any>, path: string[]) => {
let current = root;
path.forEach((key) => {
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
});
return current;
};
const setWordValue = (
root: Record<string, any>,
path: string[],
value: string
) => {
const node = ensurePath(root, path);
if (typeof node.default !== 'string') {
node.default = value;
}
node.value = value;
};
const getUploadUrl = (fileList: any[]) => {
if (!Array.isArray(fileList) || fileList.length === 0) {
return '';
}
const file = fileList[0];
return (
file?.url ||
file?.thumbUrl ||
file?.response?.url ||
file?.response?.data?.url ||
file?.response?.data?.path ||
''
);
};
const toUploadFileList = (url?: string) => {
if (!url) {
return [];
}
return [
{
uid: 'background-0',
name: '背景图',
status: 'done',
url
}
];
};
/* 图片预览 */
const handlePreview = (file: any) => {
console.log('预览图片:', file);
};
/* 加载设置 */
const loadSettings = async () => {
try {
const list = await listShopDealerSetting();
const nextMap: Record<string, ShopDealerSetting> = {};
const nextValues: Record<string, any> = {};
list.forEach((item) => {
if (!item.key) {
return;
}
nextMap[item.key] = item;
if (item.values) {
try {
nextValues[item.key] = JSON.parse(item.values);
} catch (error) {
console.warn('解析设置失败:', item.key, error);
}
}
});
settingMap.value = nextMap;
settingValuesMap.value = nextValues;
const basicValue = nextValues.basic || {};
basicSettings.enableDistribution = getBooleanFlag(basicValue.is_open);
basicSettings.distributionLevel = basicValue.level ?? 3;
basicSettings.dealerSelfBuy = getBooleanFlag(basicValue.self_buy);
const conditionValue = nextValues.condition || {};
commissionSettings.applyType = pickEnumValue(
conditionValue.become,
[10, 20],
commissionSettings.applyType
);
const settlementValue = nextValues.settlement || {};
commissionSettings.settlementType = pickEnumValue(
settlementValue.settle_days ?? settlementValue.settlement_type,
[10, 20],
commissionSettings.settlementType
);
commissionSettings.minWithdrawAmount =
settlementValue.min_money ?? commissionSettings.minWithdrawAmount;
withdrawSettings.withdrawMethods = Array.isArray(settlementValue.pay_type)
? settlementValue.pay_type
: withdrawSettings.withdrawMethods;
withdrawSettings.withdrawFeeRate =
typeof settlementValue.fee_rate === 'number'
? settlementValue.fee_rate
: withdrawSettings.withdrawFeeRate;
withdrawSettings.withdrawAudit =
settlementValue.audit === undefined
? withdrawSettings.withdrawAudit
: getBooleanFlag(settlementValue.audit);
const licenseValue = nextValues.license || {};
if (licenseValue.license) {
agreementSettings.dealerAgreement = licenseValue.license;
}
const wordsValue = nextValues.words || {};
if (wordsValue.index?.title?.value) {
pageSettings.centerTitle = wordsValue.index.title.value;
}
if (wordsValue.apply?.words?.wait_audit?.value) {
notificationSettings.applySuccessText =
wordsValue.apply.words.wait_audit.value;
}
if (wordsValue.apply?.words?.fail?.value) {
notificationSettings.applyFailText = wordsValue.apply.words.fail.value;
}
if (wordsValue.withdraw_apply?.words?.success?.value) {
notificationSettings.withdrawSuccessText =
wordsValue.withdraw_apply.words.success.value;
}
const backgroundValue = nextValues.background || {};
const backgroundUrl =
backgroundValue.index ||
backgroundValue.apply ||
backgroundValue.withdraw_apply;
pageSettings.backgroundImages = toUploadFileList(backgroundUrl);
} catch (error) {
console.error('加载设置失败:', error);
message.error('加载设置失败');
}
};
const saveSettingItem = async (
key: keyof typeof settingMeta,
values: Record<string, any>
) => {
const existSetting = settingMap.value[key];
const payload: ShopDealerSetting = {
...(existSetting || {}),
key,
describe: existSetting?.describe ?? settingMeta[key],
values: JSON.stringify(values),
updateTime: Date.now()
};
const saveOrUpdate = existSetting
? updateShopDealerSetting
: addShopDealerSetting;
await saveOrUpdate(payload);
settingMap.value[key] = payload;
settingValuesMap.value[key] = values;
};
/* 保存设置 */
const saveSettings = async () => {
saving.value = true;
try {
const basicValues = {
is_open: getNumberFlag(basicSettings.enableDistribution),
level: basicSettings.distributionLevel,
self_buy: getNumberFlag(basicSettings.dealerSelfBuy)
};
const conditionValues = {
...(settingValuesMap.value.condition || {}),
become: commissionSettings.applyType
};
const settlementValues = {
...(settingValuesMap.value.settlement || {}),
pay_type: [...withdrawSettings.withdrawMethods],
min_money: commissionSettings.minWithdrawAmount,
settle_days: commissionSettings.settlementType,
fee_rate: withdrawSettings.withdrawFeeRate,
audit: getNumberFlag(withdrawSettings.withdrawAudit)
};
const licenseValues = {
license: agreementSettings.dealerAgreement
};
const wordsValues = {
...(settingValuesMap.value.words || {})
};
setWordValue(wordsValues, ['index', 'title'], pageSettings.centerTitle);
setWordValue(
wordsValues,
['apply', 'words', 'wait_audit'],
notificationSettings.applySuccessText
);
setWordValue(
wordsValues,
['apply', 'words', 'fail'],
notificationSettings.applyFailText
);
setWordValue(
wordsValues,
['withdraw_apply', 'words', 'success'],
notificationSettings.withdrawSuccessText
);
const backgroundUrl = getUploadUrl(pageSettings.backgroundImages);
const backgroundValues = {
...(settingValuesMap.value.background || {}),
index: backgroundUrl || settingValuesMap.value.background?.index || '',
apply: backgroundUrl || settingValuesMap.value.background?.apply || '',
withdraw_apply:
backgroundUrl || settingValuesMap.value.background?.withdraw_apply || ''
};
await saveSettingItem('basic', basicValues);
await saveSettingItem('condition', conditionValues);
await saveSettingItem('settlement', settlementValues);
await saveSettingItem('license', licenseValues);
await saveSettingItem('words', wordsValues);
await saveSettingItem('background', backgroundValues);
await loadSettings();
message.success('设置保存成功');
} catch (error) {
console.error('保存设置失败:', error);
message.error('保存设置失败');
} finally {
saving.value = false;
}
};
// 页面加载时获取设置数据
onMounted(() => {
loadSettings();
});
</script>
<script lang="ts">
export default {
name: 'ShopDealerSetting'
};
</script>
<style lang="less" scoped>
.dealer-setting-container {
max-width: 1200px;
margin: 0 auto;
}
.setting-tabs {
:deep(.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab) {
border-radius: 6px 6px 0 0;
background: #fafafa;
border: 1px solid #d9d9d9;
margin-right: 8px;
&.ant-tabs-tab-active {
background: #fff;
border-bottom-color: #fff;
}
}
:deep(.ant-tabs-content-holder) {
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 0 6px 6px 6px;
padding: 24px;
min-height: 500px;
}
}
.setting-desc {
color: #999;
font-size: 12px;
margin-top: 4px;
line-height: 1.4;
}
.setting-footer {
text-align: center;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
:deep(.ant-form-item-label > label) {
font-weight: 500;
}
:deep(.ant-radio-group) {
.ant-radio-wrapper {
margin-right: 16px;
}
}
:deep(.ant-checkbox-group) {
.ant-checkbox-wrapper {
margin-right: 16px;
margin-bottom: 8px;
}
}
:deep(.ant-upload-select-picture-card) {
width: 120px;
height: 120px;
}
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
width: 120px;
height: 120px;
}
</style>