增加二级分销相关设置以及页面

This commit is contained in:
pikachu1995@126.com
2026-08-05 18:03:45 +08:00
parent 24f829033f
commit 4be36d61c7
37 changed files with 5961 additions and 904 deletions

View File

@@ -356,33 +356,19 @@ export function distribution () {
} }
/** /**
* 申请成为分销商 * 提交招募申请
* @param idNumber 身份证号
* @param name 名字
*/ */
export function applyDistribution (params) { export function submitRecruitApplication (data) {
return request({ return request({
url: `/buyer/distribution/distribution`, url: `/buyer/distribution/distribution/recruit/apply`,
method: Method.POST, method: Method.POST,
needToken: true, needToken: true,
params data
}); });
} }
/** /**
* 获取分销商订单列表 * 获取分销员商品列表
*/
export function getDistOrderList (params) {
return request({
url: `/buyer/distribution/order`,
method: Method.GET,
needToken: true,
params
});
}
/**
* 获取分销商商品列表
*/ */
export function getDistGoodsList (params) { export function getDistGoodsList (params) {
return request({ return request({
@@ -408,7 +394,7 @@ export function selectDistGoods (params) {
} }
/** /**
* 分销提现历史 * 分销提现历史
*/ */
export function distCashHistory (params) { export function distCashHistory (params) {
return request({ return request({
@@ -420,7 +406,7 @@ export function distCashHistory (params) {
} }
/** /**
* 分销提现 * 分销提现
*/ */
export function distCash (params) { export function distCash (params) {
return request({ return request({

View File

@@ -4,24 +4,11 @@
<!-- 分销申请 --> <!-- 分销申请 -->
<div v-if="status === 0"> <div v-if="status === 0">
<el-alert type="warning">分销申请</el-alert> <el-alert type="warning">分销申请</el-alert>
<el-form ref="form" :model="applyForm" :rules="rules"> <el-form ref="form" :model="applyForm" :rules="rules">
<el-form-item label="姓名" prop="name"> <el-form-item>
<el-input v-model="applyForm.name"></el-input> <el-alert type="info" show-icon :closable="false" title="分销员申请已改为招募流程,请通过招募入口加入。" />
</el-form-item> </el-form-item>
<el-form-item label="身份证号" prop="idNumber">
<el-input v-model="applyForm.idNumber"></el-input>
</el-form-item>
<el-form-item label="银行开户行" prop="settlementBankBranchName">
<el-input v-model="applyForm.settlementBankBranchName"></el-input>
</el-form-item>
<el-form-item label="银行开户名" prop="settlementBankAccountName">
<el-input v-model="applyForm.settlementBankAccountName"></el-input>
</el-form-item>
<el-form-item label="银行账号" prop="settlementBankAccountNum">
<el-input v-model="applyForm.settlementBankAccountNum"></el-input>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="applyLoading" @click="apply" <el-button type="primary" :loading="applyLoading" @click="apply"
>提交申请</el-button >提交申请</el-button
@@ -266,14 +253,12 @@ import { Message } from "@/utils/message";
import { Edit } from '@element-plus/icons-vue'; import { Edit } from '@element-plus/icons-vue';
import { import {
distribution, distribution,
applyDistribution, submitRecruitApplication,
distCash, distCash,
distCashHistory, distCashHistory,
getDistGoodsList, getDistGoodsList,
selectDistGoods, selectDistGoods,
} from "@/api/member.js"; } from "@/api/member.js";
import { IDCard } from "@/plugins/RegExp.js";
import { checkBankno } from "@/plugins/Foundation";
import vueQr from "vue-qr"; import vueQr from "vue-qr";
import config from "@/config"; import config from "@/config";
export default { export default {
@@ -285,37 +270,7 @@ export default {
status: 0, // 申请状态0为未申请 1 申请中 2 申请完成 3 功能暂未开启 status: 0, // 申请状态0为未申请 1 申请中 2 申请完成 3 功能暂未开启
applyForm: {}, // 申请表单 applyForm: {}, // 申请表单
rules: { rules: {
// 验证规则 // 验证规则V2 招募流程,旧实名认证字段已移除)
name: [{ required: true, message: "请输入真实姓名" }],
idNumber: [
{ required: true, message: "请输入身份证号" },
{ pattern: IDCard, message: "请输入正确的身份证号" },
],
settlementBankBranchName: [
{
required: true,
message: "请输入银行开户行",
// 可以单个或者同时写两个触发验证方式
trigger: "blur",
},
],
settlementBankAccountName: [
{
required: true,
message: "请输入银行开户名",
// 可以单个或者同时写两个触发验证方式
trigger: "blur",
},
],
//银行账号
settlementBankAccountNum: [
{
required: true,
message: "银行账号不正确",
// 可以单个或者同时写两个触发验证方式
trigger: "blur",
},
],
}, },
tabName: "goodsChecked", // 当前所在tab tabName: "goodsChecked", // 当前所在tab
result: {}, // 审核结果 result: {}, // 审核结果
@@ -356,11 +311,11 @@ export default {
}, },
methods: { methods: {
apply() { apply() {
// 申请成为分销 // 申请成为分销
this.$refs.form.validate((valid) => { this.$refs.form.validate((valid) => {
if (valid) { if (valid) {
this.applyLoading = true; this.applyLoading = true;
applyDistribution(this.applyForm).then((res) => { submitRecruitApplication({ agreementAccepted: true }).then((res) => {
this.applyLoading = false; this.applyLoading = false;
if (res.success) { if (res.success) {
Message.success("申请已提交,请等待管理员审核"); Message.success("申请已提交,请等待管理员审核");
@@ -496,7 +451,7 @@ export default {
}, },
//申诉 //申诉
repaying(){ repaying(){
applyDistribution().then((res) => { submitRecruitApplication({ agreementAccepted: true }).then((res) => {
this.applyLoading = false; this.applyLoading = false;
if (res.success) { if (res.success) {
Message.success("申诉已提交,请等待管理员审核"); Message.success("申诉已提交,请等待管理员审核");
@@ -505,7 +460,7 @@ export default {
}); });
}, },
distribution() { distribution() {
// 获取分销信息 // 获取分销信息
distribution() distribution()
.then((res) => { .then((res) => {
if (res?.result) { if (res?.result) {

View File

@@ -3,7 +3,7 @@ var BASE = {
* @description api请求基础路径 * @description api请求基础路径
*/ */
API_DEV: { API_DEV: {
manager: "https://admin-api.pickmall.cn", manager: "http://127.0.0.1:8887",
}, },
API_PROD: { API_PROD: {
manager: "https://admin-api.pickmall.cn" manager: "https://admin-api.pickmall.cn"

View File

@@ -1,11 +1,45 @@
// 统一请求路径前缀在libs/axios.js中修改 // 统一请求路径前缀在libs/axios.js中修改
import {getRequest, postRequest, putRequest, deleteRequest} from '@/libs/axios'; import {getRequest, postRequest, postRequestWithNoForm, putRequest, putRequestWithNoForm, deleteRequest, importRequest} from '@/libs/axios';
//查询分销员 //查询分销员
export const getDistributionListData = (params) => { export const getDistributionListData = (params) => {
return getRequest('/distribution/distribution/getByPage', params) return getRequest('/distribution/distribution/getByPage', params)
} }
// 分销员聚合分页(V2)
export const getDistributionMemberPage = (params) => {
return getRequest('/distribution/distribution/memberPage', params)
}
export const getDistributionMemberDetail = (id) => {
return getRequest(`/distribution/distribution/memberDetail/${id}`)
}
export const updateDistributionMember = (id, params) => {
return putRequestWithNoForm(`/distribution/distribution/member/${id}`, params)
}
export const createDistributionMember = (params) => {
return postRequestWithNoForm('/distribution/distribution/member', params)
}
export const batchUpdateDistributionGrade = (params) => {
return putRequestWithNoForm('/distribution/distribution/member/batchGrade', params)
}
export const exportDistributionMembers = (params) => {
return getRequest('/distribution/distribution/memberExport', params, 'blob')
}
export const downloadDistributionImportTemplate = () => {
return getRequest('/distribution/memberImport/template', {}, 'blob')
}
export const importDistributionMembers = (formData) => {
return importRequest('/distribution/memberImport/import', formData)
}
export const getDistributionImportBatches = (params) => {
return getRequest('/distribution/memberImport/batch', params)
}
export const getDistributionImportItems = (batchId, params) => {
return getRequest(`/distribution/memberImport/batch/${batchId}/items`, params)
}
export const downloadDistributionImportFailItems = (batchId) => {
return getRequest(`/distribution/memberImport/batch/${batchId}/failExport`, {}, 'blob')
}
//审核分销员 //审核分销员
export const auditDistribution = (id, params) => { export const auditDistribution = (id, params) => {
return putRequest(`/distribution/distribution/audit/${id}`, params) return putRequest(`/distribution/distribution/audit/${id}`, params)
@@ -34,6 +68,50 @@ export const getDistributionSetting = (id, params) => {
return getRequest(`/distribution/setting/get/${id}`, params) return getRequest(`/distribution/setting/get/${id}`, params)
} }
// V2客户关系设置
export const getDistributionCustomerSetting = () => getRequest('/distribution/setting/v2/customer')
export const putDistributionCustomerSetting = (params) => putRequestWithNoForm('/distribution/setting/v2/customer', params)
// V2结算方案设置
export const getDistributionSettlementSetting = () => getRequest('/distribution/setting/v2/settlement')
export const putDistributionSettlementSetting = (params) => putRequestWithNoForm('/distribution/setting/v2/settlement', params)
// 分销商品设置
export const getDistributionFeaturedGoodsSetting = () => getRequest('/distribution/setting/v2/featured-goods')
export const putDistributionFeaturedGoodsSetting = (params) => putRequestWithNoForm('/distribution/setting/v2/featured-goods', params)
// 平台海报设置
export const getDistributionPromotionPosterSetting = () => getRequest('/distribution/setting/v2/promotion-poster')
export const putDistributionPromotionPosterSetting = (params) => putRequestWithNoForm('/distribution/setting/v2/promotion-poster', params)
// 邀请卡设置
export const getDistributionInviteCardSetting = () => getRequest('/distribution/setting/v2/invite-card')
export const putDistributionInviteCardSetting = (params) => putRequestWithNoForm('/distribution/setting/v2/invite-card', params)
// 分销等级
export const getDistributionGrades = () => getRequest('/distribution/grade')
export const saveAllDistributionGrades = (params) => putRequestWithNoForm('/distribution/grade/batch', params)
export const deleteDistributionGrade = (id) => deleteRequest(`/distribution/grade/${id}`)
export const getDistributionGradeHistoryPage = (params) => getRequest('/distribution/gradeHistory', params)
// 客户关系
export const getCustomerRelations = (params) => getRequest('/distribution/customerRelation', params)
export const getCustomerRelationDetail = (id) => getRequest(`/distribution/customerRelation/${id}`)
export const getCustomerRelationLogs = (id) => getRequest(`/distribution/customerRelation/${id}/logs`)
export const rebindCustomerRelation = (params) => putRequest('/distribution/customerRelation/rebind', params)
export const unbindCustomerRelation = (params) => putRequest('/distribution/customerRelation/unbind', params)
// 分销招募设置
export const getCurrentRecruitPlan = () => getRequest('/distribution/recruit/plan/current')
export const getPublishedRecruitPlan = () => getRequest('/distribution/recruit/plan/published')
export const saveRecruitPlan = (params) => postRequestWithNoForm('/distribution/recruit/plan', params)
export const getDistributionRecruitSetting = () => getRequest('/distribution/setting/v2/recruit')
export const putDistributionRecruitSetting = (params) => putRequestWithNoForm('/distribution/setting/v2/recruit', params)
export const publishRecruitPlan = (planId) => putRequest(`/distribution/recruit/plan/publish/${planId}`)
export const auditRecruitApplication = (applicationId, params) => putRequest(`/distribution/recruit/application/audit/${applicationId}`, params)
// 统计V2
export const getDistributionOverviewV2 = (params) => getRequest('/statistics/distribution/v2/overview', params)
export const getDistributionBehaviorV2 = (params) => getRequest('/statistics/distribution/v2/behavior', params)
export const getDistributionGoodsV2 = (params) => getRequest('/statistics/distribution/v2/goods', params)
// 钱包流水
export const getDistributionWalletLogs = (params) => getRequest('/distribution/walletLog', params)
//获取分销订单列表 //获取分销订单列表
export const getDistributionOrder = (params) => { export const getDistributionOrder = (params) => {
@@ -48,6 +126,10 @@ export const getDistributionGoods = (params) => {
export const delDistributionGoods = (ids) => { export const delDistributionGoods = (ids) => {
return deleteRequest(`/distribution/goods/delByIds/${ids}`) return deleteRequest(`/distribution/goods/delByIds/${ids}`)
} }
export const auditDistributionGoods = (params) => {
return putRequestWithNoForm('/distribution/goods/audit', params)
}
//获取分销佣金信息 //获取分销佣金信息
export const getDistributionCash = (params) => { export const getDistributionCash = (params) => {
return getRequest('/distribution/cash/getByPage', params) return getRequest('/distribution/cash/getByPage', params)
@@ -56,6 +138,3 @@ export const getDistributionCash = (params) => {
export const auditDistributionCash = (id,params) => { export const auditDistributionCash = (id,params) => {
return postRequest(`/distribution/cash/audit/${id}`, params) return postRequest(`/distribution/cash/audit/${id}`, params)
} }

View File

@@ -0,0 +1,188 @@
<template>
<el-dialog
:model-value="modelValue"
title="选择分销员"
width="900px"
:close-on-click-modal="false"
destroy-on-close
append-to-body
@close="handleClose"
>
<el-form inline label-width="70px" class="search-form" @keyup.enter="handleSearch">
<el-form-item label="手机号">
<el-input v-model="searchForm.mobile" placeholder="请输入手机号" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="searchForm.nickname" placeholder="请输入昵称" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
border
:data="data"
row-key="id"
highlight-current-row
max-height="420"
@row-click="handleRowClick"
@current-change="handleCurrentChange"
>
<el-table-column width="55" align="center">
<template #default="{ row }">
<el-radio v-model="selectedId" :label="row.id" @click.stop @change="handleRadioChange(row)">
<span />
</el-radio>
</template>
</el-table-column>
<el-table-column prop="mobile" label="手机号" min-width="120" />
<el-table-column prop="nickname" label="昵称" min-width="120" show-overflow-tooltip />
<el-table-column prop="gradeName" label="等级" min-width="100" />
<el-table-column prop="joinTime" label="加入时间" min-width="160" />
</el-table>
<div class="pagination-wrap">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next"
size="small"
@current-change="getDataList"
@size-change="changePageSize"
/>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :disabled="!selectedRow" @click="handleConfirm">确定</el-button>
</template>
</el-dialog>
</template>
<script>
import { getDistributionMemberPage } from "@/api/distribution";
export default {
name: "distributionSelector",
props: {
modelValue: {
type: Boolean,
default: false,
},
excludeId: {
type: String,
default: "",
},
},
emits: ["update:modelValue", "confirm"],
data() {
return {
loading: false,
searchForm: {
pageNumber: 1,
pageSize: 10,
mobile: "",
nickname: "",
},
data: [],
total: 0,
selectedId: "",
selectedRow: null,
};
},
watch: {
modelValue(val) {
if (val) {
this.resetState();
this.getDataList();
}
},
},
methods: {
resetState() {
this.searchForm = {
pageNumber: 1,
pageSize: 10,
mobile: "",
nickname: "",
};
this.selectedId = "";
this.selectedRow = null;
},
buildParams() {
const params = {
pageNumber: this.searchForm.pageNumber,
pageSize: this.searchForm.pageSize,
distributionStatus: "PASS",
};
if (this.searchForm.mobile) params.mobile = this.searchForm.mobile;
if (this.searchForm.nickname) params.nickname = this.searchForm.nickname;
return params;
},
getDataList() {
this.loading = true;
getDistributionMemberPage(this.buildParams())
.then((res) => {
this.loading = false;
if (!res.success) return;
const result = res.result || {};
let records = result.records || [];
const beforeCount = records.length;
if (this.excludeId) {
records = records.filter((item) => item.id !== this.excludeId);
}
this.data = records;
const excludedCount = beforeCount - records.length;
this.total = Math.max(0, (result.total || 0) - excludedCount);
})
.catch(() => {
this.loading = false;
});
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleRowClick(row) {
if (!row) return;
this.selectedRow = row;
this.selectedId = row.id;
},
handleCurrentChange(row) {
if (!row) return;
this.selectedRow = row;
this.selectedId = row.id;
},
handleRadioChange(row) {
this.selectedRow = row;
},
handleClose() {
this.$emit("update:modelValue", false);
},
handleConfirm() {
if (!this.selectedRow) {
this.$Message.warning("请选择分销员");
return;
}
this.$emit("confirm", this.selectedRow);
this.handleClose();
},
},
};
</script>
<style scoped lang="scss">
.pagination-wrap {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
</style>

View File

@@ -7,7 +7,6 @@ let util = {};
/** 已弃用、前端未实现或已移除的菜单 frontRoute注册路由时跳过 */ /** 已弃用、前端未实现或已移除的菜单 frontRoute注册路由时跳过 */
const DEPRECATED_FRONT_ROUTES = new Set([ const DEPRECATED_FRONT_ROUTES = new Set([
"goods/goods-manage/spec", "goods/goods-manage/spec",
"distribution/distrbutionGrade",
]); ]);
function isDeprecatedMenu(menu) { function isDeprecatedMenu(menu) {

View File

@@ -455,6 +455,20 @@ export const otherRouter = {
meta: { title: "商品销售汇总报表" }, meta: { title: "商品销售汇总报表" },
component: () => import("@/views/statistics/goods-sales-summary.vue") component: () => import("@/views/statistics/goods-sales-summary.vue")
}, },
{
path: "distributionImportRecord",
title: "分销员导入记录",
name: "distributionImportRecord",
meta: { title: "分销员导入记录" },
component: () => import("@/views/distribution/distributionImportRecord.vue")
},
{
path: "distributionGradeHistory",
title: "等级变更记录",
name: "distributionGradeHistory",
meta: { title: "等级变更记录" },
component: () => import("@/views/distribution/distributionGradeHistory.vue")
},
{ {
path: "procurement/purchase-order/detail", path: "procurement/purchase-order/detail",
title: "采购单详情", title: "采购单详情",

View File

@@ -47,6 +47,20 @@
background-color: transparent; background-color: transparent;
} }
/* 全局表格表头 */
.el-table {
.el-table__header th.el-table__cell {
background: var(--el-color-primary-light-9, #ecf5ff);
color: #303133;
font-weight: normal;
}
.el-table__header th.el-table__cell .cell {
color: #303133;
font-weight: normal;
}
}
/* 列表操作列统一样式 */ /* 列表操作列统一样式 */
.link-text { .link-text {
color: #409eff; color: #409eff;

View File

@@ -1,4 +1,4 @@
// 分销状态列表 // 分销状态列表
export const distributionStatusList= [ export const distributionStatusList= [
{ {
value:'APPLY', value:'APPLY',
@@ -32,6 +32,43 @@ export const cashStatusList = [
label:'通过' label:'通过'
} }
] ]
// 分销客户关系有效状态筛选
export const relationEffectiveList = [
{ value: true, label: '有效' },
{ value: false, label: '失效' },
]
export const hasDeadlineList = [
{ value: true, label: '是' },
{ value: false, label: '否' },
]
// 分销客户关系状态列表
export const relationStatusList = [
{ value: 'PENDING', label: '待支付转永久' },
{ value: 'TEMPORARY', label: '短期有效' },
{ value: 'PERMANENT', label: '永久有效' },
{ value: 'EXPIRED', label: '已到期' },
{ value: 'UNBOUND', label: '已解绑' },
{ value: 'SELF_BIND', label: '自绑定' },
]
export const customerRelationChangeTypeList = [
{ value: 'CREATE', label: '绑定' },
{ value: 'EXPIRE', label: '过期' },
{ value: 'ADMIN_REBIND', label: '换绑' },
{ value: 'ADMIN_UNBIND', label: '解绑' },
]
export const bindModeList = [
{ value: 'PERMANENT', label: '永久锁客' },
{ value: 'SHORT_TERM', label: '短期锁客' },
]
export const permanentBindModeList = [
{ value: 'SCAN_IMMEDIATE', label: '扫码立即永久' },
{ value: 'PAID_WITHIN_DAYS', label: '规定天数内支付转永久' },
]
export const bindSourceList = [
{ value: 'SCAN', label: '扫码绑定' },
{ value: 'ORDER', label: '订单触发' },
{ value: 'ADMIN', label: '后台操作' },
]
// 分销订单状态列表 // 分销订单状态列表
export const orderStatusList = [ export const orderStatusList = [
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +0,0 @@
<template>
<div class="search">
<el-card>
<el-form
ref="searchForm"
:model="searchForm"
inline
label-width="70px"
class="search-form"
@keyup.enter="handleSearch"
>
<el-form-item label="会员名称" prop="memberName">
<el-input
v-model="searchForm.memberName"
placeholder="请输入会员名称"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table
ref="table"
v-loading="loading"
border
:data="data"
class="mt_10"
style="width: 100%"
>
<el-table-column prop="memberName" label="会员名称" min-width="100" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="idNumber" label="身份证号" min-width="120" />
<el-table-column prop="settlementBankAccountName" label="结算银行开户行名称" min-width="120" />
<el-table-column prop="settlementBankAccountNum" label="结算银行开户账号" min-width="120" />
<el-table-column prop="settlementBankBranchName" label="结算银行开户支行名称" min-width="120" />
<el-table-column prop="createTime" label="提交时间" min-width="100" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<div v-if="row" class="ops">
<a class="link-text" @click="audit(row, 'PASS')">通过</a>
<span class="op-split">|</span>
<a class="link-text" @click="audit(row, 'REFUSE')">拒绝</a>
</div>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</el-card>
</div>
</template>
<script>
import { auditDistribution, getDistributionListData } from "@/api/distribution";
export default {
name: "distributionApply",
data() {
return {
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
},
data: [],
total: 0,
};
},
methods: {
init() {
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = 20;
this.getDataList();
},
getDataList() {
this.loading = true;
this.searchForm.distributionStatus = "APPLY";
getDistributionListData(this.searchForm).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
audit(v, status) {
let test = "拒绝";
if (status == "PASS") {
test = "通过";
}
let params = {
status: status,
};
this.$Modal.confirm({
title: "确认" + test,
content: "您确认要" + test + " " + v.memberName + " ?",
loading: true,
onOk: () => {
auditDistribution(v.id, params).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
},
},
mounted() {
this.init();
},
};
</script>
<style lang="scss" scoped>
.ops a {
color: #2d8cf0;
cursor: pointer;
text-decoration: none;
}
.ops span {
display: inline-block;
margin: 0 8px;
color: #dcdee2;
}
</style>

View File

@@ -0,0 +1,648 @@
<template>
<div class="search">
<el-card>
<el-form
ref="searchForm"
:model="searchForm"
inline
label-width="110px"
class="search-form"
@keyup.enter="handleSearch"
>
<el-form-item label="客户手机号" prop="memberMobile">
<el-input v-model="searchForm.memberMobile" placeholder="请输入客户手机号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="分销员手机号" prop="distributionMobile">
<el-input v-model="searchForm.distributionMobile" placeholder="请输入分销员手机号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="绑定时间">
<el-date-picker
v-model="bindTimeRange"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
placeholder="选择时间"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="当前关系">
<el-select v-model="searchForm.relationEffective" clearable placeholder="全部" style="width: 200px">
<el-option
v-for="item in relationEffectiveList"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有截止时间">
<el-select v-model="searchForm.hasDeadline" clearable placeholder="全部" style="width: 200px">
<el-option
v-for="item in hasDeadlineList"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table-column prop="memberId" label="客户ID" min-width="200" class-name="id-column" show-overflow-tooltip />
<el-table-column prop="memberNickname" label="客户昵称" min-width="120" show-overflow-tooltip />
<el-table-column prop="memberMobile" label="客户手机号" min-width="120" />
<el-table-column prop="distributionNickname" label="分销员昵称" min-width="120" show-overflow-tooltip />
<el-table-column prop="distributionMobile" label="分销员手机号" min-width="120" />
<el-table-column label="当前关系" width="120">
<template #default="{ row }">
<div v-if="row" class="relation-status-cell">
<span :class="isRelationEffective(row) ? 'relation-effective' : 'relation-invalid'">
{{ isRelationEffective(row) ? "有效" : "失效" }}
</span>
<el-tooltip v-if="!isRelationEffective(row)" placement="top">
<template #content>
<div>失效时间{{ formatInvalidTime(row) }}</div>
<div>失效原因{{ row.invalidReason || "-" }}</div>
</template>
<el-icon class="relation-tip-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column prop="bindTime" label="绑定时间" min-width="160" />
<el-table-column label="是否有截止日期" width="150" align="center">
<template #default="{ row }">
<span v-if="row">{{ row.hasDeadline ? "是" : "否" }}</span>
</template>
</el-table-column>
<el-table-column label="有效截止日期" min-width="160">
<template #default="{ row }">
<span v-if="row">{{ row.hasDeadline && row.expireAt ? row.expireAt : "-" }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center" fixed="right">
<template #default="{ row }">
<div v-if="row" class="ops">
<a class="link-text" @click="openDetail(row)">详情</a>
<span class="op-split">|</span>
<a class="link-text" @click="openLogs(row)">变更记录</a>
<span class="op-split">|</span>
<a class="link-text" @click="openRebind(row)">换绑</a>
<span class="op-split">|</span>
<a class="link-text" @click="openUnbind(row)">解绑</a>
</div>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</el-card>
<el-drawer
v-model="detailVisible"
title="客户关系详情"
size="640px"
direction="rtl"
:close-on-click-modal="false"
append-to-body
destroy-on-close
>
<div v-loading="detailLoading" class="detail-body">
<template v-if="detail">
<div class="detail-meta">
<div class="detail-meta-row">
<span class="detail-meta-label">关系ID</span>
<span class="detail-meta-value">{{ detail.id || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">客户ID</span>
<span class="detail-meta-value">{{ detail.memberId || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">客户手机号</span>
<span class="detail-meta-value">{{ detail.memberMobile || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">客户昵称</span>
<span class="detail-meta-value">{{ detail.memberNickname || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">分销员ID</span>
<span class="detail-meta-value">{{ detail.distributionId || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">分销员手机号</span>
<span class="detail-meta-value">{{ detail.distributionMobile || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">分销员昵称</span>
<span class="detail-meta-value">{{ detail.distributionNickname || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">绑定时间</span>
<span class="detail-meta-value">{{ detail.bindTime || "-" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">当前关系</span>
<span
class="detail-meta-value"
:class="isRelationEffective(detail) ? 'relation-effective' : 'relation-invalid'"
>
{{ isRelationEffective(detail) ? "有效" : "失效" }}
</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">失效时间</span>
<span class="detail-meta-value">{{ formatDetailInvalidTime(detail) }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">失效原因</span>
<span class="detail-meta-value">{{ formatDetailInvalidReason(detail) }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">是否有截止日期</span>
<span class="detail-meta-value">{{ detail.hasDeadline ? "是" : "否" }}</span>
</div>
<div class="detail-meta-row">
<span class="detail-meta-label">有效截止日期</span>
<span class="detail-meta-value">{{ detail.hasDeadline && detail.expireAt ? detail.expireAt : "-" }}</span>
</div>
</div>
</template>
</div>
</el-drawer>
<el-drawer
v-model="logVisible"
title="变更记录"
size="1100px"
direction="rtl"
:close-on-click-modal="false"
append-to-body
destroy-on-close
class="relation-log-drawer"
>
<div v-loading="logLoading" class="log-body">
<el-table
v-if="logList.length || logLoading"
border
:data="logList"
style="width: 100%"
empty-text="暂无变更记录"
>
<el-table-column label="客户名称" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.memberNickname || logTarget?.memberNickname || "-" }}
</template>
</el-table-column>
<el-table-column label="客户手机号" min-width="120">
<template #default="{ row }">
{{ row.memberMobile || logTarget?.memberMobile || "-" }}
</template>
</el-table-column>
<el-table-column label="变更前分销员" min-width="140">
<template #default="{ row }">
<div class="log-distributor-cell">
{{ formatDistributorCell(row.beforeDistributionNickname, row.beforeDistributionMobile, row.beforeDistributionId) }}
</div>
</template>
</el-table-column>
<el-table-column label="变更后分销员" min-width="140">
<template #default="{ row }">
<div class="log-distributor-cell">
{{ formatDistributorCell(row.afterDistributionNickname, row.afterDistributionMobile, row.afterDistributionId) }}
</div>
</template>
</el-table-column>
<el-table-column prop="createTime" label="变更时间" min-width="160" />
<el-table-column prop="changeTypeLabel" label="变更类型" width="90" align="center" />
<el-table-column label="原因" min-width="160">
<template #default="{ row }">
<div class="log-reason-cell">{{ row.reason || "-" }}</div>
</template>
</el-table-column>
</el-table>
<el-empty v-else-if="!logLoading" description="暂无变更记录" />
</div>
</el-drawer>
<el-dialog
v-model="rebindVisible"
title="换绑客户关系"
width="520px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="rebindForm" :model="rebindForm" :rules="rebindRules" label-width="110px">
<el-form-item label="客户">
<span>{{ rebindForm.memberNickname || rebindForm.memberMobile || rebindForm.memberId }}</span>
</el-form-item>
<el-form-item label="新分销员" prop="distributionId">
<div class="distributor-picker">
<el-input
:model-value="selectedDistributorLabel"
placeholder="请选择分销员"
readonly
/>
<el-button type="primary" plain @click="distributorSelectorVisible = true">选择分销员</el-button>
<el-button v-if="rebindForm.distributionId" link type="primary" @click="clearSelectedDistributor">清除</el-button>
</div>
</el-form-item>
<el-form-item label="换绑原因" prop="reason">
<el-input v-model="rebindForm.reason" type="textarea" :rows="3" placeholder="请输入换绑原因" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rebindVisible = false">取消</el-button>
<el-button type="primary" :loading="rebindLoading" @click="submitRebind">确定</el-button>
</template>
</el-dialog>
<distribution-selector
v-model="distributorSelectorVisible"
:exclude-id="rebindForm.currentDistributionId"
@confirm="onDistributorSelected"
/>
<el-dialog
v-model="unbindVisible"
title="解绑客户关系"
width="520px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="unbindForm" :model="unbindForm" label-width="110px">
<el-form-item label="客户">
<span>{{ unbindForm.memberNickname || unbindForm.memberMobile || unbindForm.memberId }}</span>
</el-form-item>
<el-form-item label="解绑原因">
<el-input v-model="unbindForm.reason" type="textarea" :rows="3" placeholder="请输入解绑原因" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="unbindVisible = false">取消</el-button>
<el-button type="primary" :loading="unbindLoading" @click="submitUnbind">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import {
getCustomerRelationDetail,
getCustomerRelationLogs,
getCustomerRelations,
rebindCustomerRelation,
unbindCustomerRelation,
} from "@/api/distribution";
import { relationEffectiveList, hasDeadlineList } from "./dataJson.js";
import DistributionSelector from "@/components/distribution/distribution-selector.vue";
import { QuestionFilled } from "@element-plus/icons-vue";
export default {
name: "distributionCustomerRelation",
components: { QuestionFilled, DistributionSelector },
data() {
return {
relationEffectiveList,
hasDeadlineList,
bindTimeRange: [],
loading: true,
searchForm: {
pageNumber: 1,
pageSize: 20,
memberMobile: "",
distributionMobile: "",
distributionId: "",
relationEffective: "",
hasDeadline: "",
},
data: [],
total: 0,
detailVisible: false,
detailLoading: false,
detail: null,
logVisible: false,
logLoading: false,
logList: [],
logTarget: null,
rebindVisible: false,
distributorSelectorVisible: false,
unbindVisible: false,
rebindLoading: false,
unbindLoading: false,
rebindForm: {
memberId: "",
memberNickname: "",
memberMobile: "",
currentDistributionId: "",
distributionId: "",
distributionNickname: "",
distributionMobile: "",
reason: "",
},
unbindForm: {
memberId: "",
memberNickname: "",
memberMobile: "",
reason: "",
},
rebindRules: {
distributionId: [{
required: true,
message: "请选择分销员",
trigger: "change",
}],
},
};
},
computed: {
selectedDistributorLabel() {
if (!this.rebindForm.distributionId) return "";
const name = this.rebindForm.distributionNickname || "-";
const mobile = this.rebindForm.distributionMobile || "-";
return `${name}${mobile}`;
},
},
mounted() {
this.applyRouteQuery();
this.getDataList();
},
watch: {
$route() {
this.applyRouteQuery();
this.getDataList();
},
},
methods: {
applyRouteQuery() {
const query = this.$route.query || {};
this.searchForm.distributionId = query.distributionId ? String(query.distributionId) : "";
this.searchForm.distributionMobile = query.distributionMobile ? String(query.distributionMobile) : "";
},
formatInvalidTime(row) {
return row?.invalidTime || "-";
},
formatDetailInvalidTime(detail) {
if (!detail || this.isRelationEffective(detail)) return "-";
return detail.invalidTime || "-";
},
formatDetailInvalidReason(detail) {
if (!detail || this.isRelationEffective(detail)) return "-";
return detail.invalidReason || "客户关系失效";
},
isRelationEffective(row) {
if (!row) return false;
if (row.relationEffective === true) return true;
if (row.relationEffective === false) return false;
return !["EXPIRED", "UNBOUND"].includes(row.relationStatus);
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
const params = { ...this.searchForm };
if (!params.memberMobile) delete params.memberMobile;
if (!params.distributionMobile) delete params.distributionMobile;
if (!params.distributionId) delete params.distributionId;
if (params.relationEffective === "" || params.relationEffective === null || params.relationEffective === undefined) {
delete params.relationEffective;
} else {
params.relationEffective = params.relationEffective === true || params.relationEffective === "true";
}
if (params.hasDeadline === "" || params.hasDeadline === null || params.hasDeadline === undefined) {
delete params.hasDeadline;
} else {
params.hasDeadline = params.hasDeadline === true || params.hasDeadline === "true";
}
if (this.bindTimeRange && this.bindTimeRange[0] && this.bindTimeRange[1]) {
const startTime = new Date(this.bindTimeRange[0]).getTime();
const endTime = new Date(this.bindTimeRange[1]).getTime();
params.bindTimeStart = this.$filters.unixToDate(startTime / 1000);
params.bindTimeEnd = this.$filters.unixToDate(endTime / 1000);
}
getCustomerRelations(params).then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records;
this.total = res.result.total;
}
});
},
openDetail(row) {
this.detailVisible = true;
this.detailLoading = true;
this.detail = null;
getCustomerRelationDetail(row.id).then((res) => {
this.detailLoading = false;
if (res.success) {
this.detail = res.result;
}
}).catch(() => {
this.detailLoading = false;
});
},
openLogs(row) {
this.logVisible = true;
this.logTarget = row;
this.logLoading = true;
this.logList = [];
getCustomerRelationLogs(row.id).then((res) => {
this.logLoading = false;
if (res.success) {
this.logList = res.result || [];
}
}).catch(() => {
this.logLoading = false;
});
},
formatDistributorCell(nickname, mobile, id) {
if ((!nickname || nickname === "-") && !id) return "-";
const name = nickname && nickname !== "-" ? nickname : id;
if (mobile) return `${name}\n${mobile}`;
return name;
},
openRebind(row) {
this.rebindForm = {
memberId: row.memberId,
memberNickname: row.memberNickname,
memberMobile: row.memberMobile,
currentDistributionId: row.distributionId || "",
distributionId: "",
distributionNickname: "",
distributionMobile: "",
reason: "",
};
this.rebindVisible = true;
},
onDistributorSelected(row) {
this.rebindForm.distributionId = row.id;
this.rebindForm.distributionNickname = row.nickname;
this.rebindForm.distributionMobile = row.mobile;
this.$nextTick(() => {
this.$refs.rebindForm?.validateField("distributionId");
});
},
clearSelectedDistributor() {
this.rebindForm.distributionId = "";
this.rebindForm.distributionNickname = "";
this.rebindForm.distributionMobile = "";
this.$nextTick(() => {
this.$refs.rebindForm?.validateField("distributionId");
});
},
openUnbind(row) {
this.unbindForm = {
memberId: row.memberId,
memberNickname: row.memberNickname,
memberMobile: row.memberMobile,
reason: "",
};
this.unbindVisible = true;
},
submitRebind() {
this.$refs.rebindForm.validate((valid) => {
if (!valid) return;
if (this.rebindForm.distributionId === this.rebindForm.currentDistributionId) {
this.$Message.warning("新分销员不能与当前分销员相同");
return;
}
this.rebindLoading = true;
rebindCustomerRelation({
memberId: this.rebindForm.memberId,
distributionId: this.rebindForm.distributionId,
reason: this.rebindForm.reason,
}).then((res) => {
this.rebindLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.rebindVisible = false;
this.getDataList();
}
}).catch(() => {
this.rebindLoading = false;
});
});
},
submitUnbind() {
this.unbindLoading = true;
unbindCustomerRelation({
memberId: this.unbindForm.memberId,
reason: this.unbindForm.reason,
}).then((res) => {
this.unbindLoading = false;
if (res.success) {
this.$Message.success("操作成功");
this.unbindVisible = false;
this.getDataList();
}
}).catch(() => {
this.unbindLoading = false;
});
},
},
};
</script>
<style scoped lang="scss">
.ops a {
color: #2d8cf0;
cursor: pointer;
text-decoration: none;
}
.ops .op-split {
display: inline-block;
margin: 0 6px;
color: #dcdee2;
}
.distributor-picker {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.distributor-picker .el-input {
flex: 1;
}
.relation-status-cell {
display: inline-flex;
align-items: center;
gap: 6px;
}
.relation-effective {
color: #19be6b;
}
.relation-invalid {
color: #999;
}
.relation-tip-icon {
color: #909399;
font-size: 14px;
cursor: pointer;
}
.detail-body {
padding: 0 8px 16px;
}
.detail-meta-row {
display: flex;
margin-bottom: 10px;
line-height: 22px;
}
.detail-meta-label {
flex: 0 0 120px;
color: #909399;
}
.detail-meta-value {
flex: 1;
color: #303133;
word-break: break-all;
}
.log-body {
padding: 0 8px 16px;
}
.log-distributor-cell,
.log-reason-cell {
white-space: pre-wrap;
word-break: break-word;
line-height: 22px;
}
</style>

View File

@@ -17,15 +17,58 @@
style="width: 240px" style="width: 240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="店铺名称" prop="storeName">
<el-input
v-model="searchForm.storeName"
placeholder="请输入店铺名称"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item label="平台类目" prop="categoryId">
<el-cascader
v-model="searchForm.categoryId"
:options="categoryList"
:props="categoryProps"
clearable
filterable
placeholder="请选择平台类目"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="商品品牌" prop="brandId">
<el-select
v-model="searchForm.brandId"
clearable
filterable
placeholder="请选择商品品牌"
style="width: 240px"
>
<el-option
v-for="item in brandList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button> <el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<el-card> <el-card class="list-card">
<el-tabs v-model="currentAuthStatus" class="auth-tabs" @tab-change="onAuthStatusChange">
<el-tab-pane label="全部" name="ALL" />
<el-tab-pane label="待审核" name="TOBEAUDITED" />
<el-tab-pane label="已审核" name="PASS" />
<el-tab-pane label="审核未通过" name="REFUSE" />
</el-tabs>
<div class="operation" style="margin: 10px 0"> <div class="operation" style="margin: 10px 0">
<el-button type="primary" @click="delAll">批量下架</el-button> <el-button type="primary" @click="openBatchAudit" :disabled="!hasPendingAuditSelection">批量审核</el-button>
<el-button type="primary" style="margin-left: 10px" @click="delAll">批量下架</el-button>
</div> </div>
<el-table <el-table
@@ -33,46 +76,41 @@
v-loading="loading" v-loading="loading"
border border
:data="data" :data="data"
class="mt_10" class="mt_10 distribution-goods-table"
style="width: 100%" style="width: 100%"
@selection-change="changeSelect" @selection-change="changeSelect"
> >
<el-table-column type="selection" width="55" align="center" fixed="left" /> <el-table-column type="selection" width="55" align="center" fixed="left" />
<el-table-column label="商品图片" width="120" align="center" fixed="left"> <el-table-column label="商品图片" width="80" align="center" fixed="left">
<template #default="{ row }"> <template #default="{ row }">
<img <img
v-if="row" v-if="row?.thumbnail"
:src="row.thumbnail || ''" :src="row.thumbnail"
alt="商品图" alt="商品图"
style="cursor: pointer; width: 80px; height: 60px; margin: 10px 0; object-fit: contain" class="goods-thumbnail"
/> />
<span v-else class="goods-thumbnail-empty">无图</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品名称" min-width="220" show-overflow-tooltip> <el-table-column label="商品名称" min-width="200" show-overflow-tooltip fixed="left">
<template #default="{ row }"> <template #default="{ row }">
<template v-if="row"> <span v-if="row">{{ row.goodsName }}</span>
<div class="div-zoom">
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
</div>
<el-popover trigger="hover" title="扫码在手机中查看" placement="top" width="180">
<template #reference>
<img
src="../../assets/qrcode.svg"
class="hover-pointer"
width="20"
height="20"
alt="qrcode"
/>
</template> </template>
<vue-qr </el-table-column>
:text="wapLinkTo(row.goodsId, row.skuId)" <el-table-column prop="goodsId" label="商品编号" min-width="200" show-overflow-tooltip />
:margin="0" <el-table-column label="商品类型" min-width="110">
color-dark="#000" <template #default="{ row }">
color-light="#fff" <span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
:size="150"
/>
</el-popover>
</template> </template>
</el-table-column>
<el-table-column label="平台类目" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row">{{ row.platformCategoryName || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="品牌" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row">{{ row.brandName || "-" }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品价格" min-width="110"> <el-table-column label="商品价格" min-width="110">
@@ -80,17 +118,26 @@
<span v-if="row" :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, "") }}</span> <span v-if="row" :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, "") }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="quantity" label="库存" min-width="80" />
<el-table-column prop="createTime" label="添加时间" min-width="160" />
<el-table-column prop="storeName" label="店铺名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="storeName" label="店铺名称" min-width="120" show-overflow-tooltip />
<el-table-column label="佣金金额" min-width="110"> <el-table-column label="佣金金额" min-width="110">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">{{ $filters.unitPrice(row.commission, "") }}</span> <span v-if="row" :style="{ color: $mainColor }">{{ $filters.unitPrice(row.commission, "") }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-width="100" align="center" fixed="right"> <el-table-column label="审核状态" min-width="110" align="center">
<template #default="{ row }"> <template #default="{ row }">
<a v-if="row" class="link-text" @click="remove(row)">下架</a> <el-tag v-if="row" :type="authStatusTagType(row.authStatus)" size="small">
{{ authStatusText(row.authStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="120" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row">
<a v-if="row.authStatus === 'TOBEAUDITED'" class="link-text" @click.prevent="openAudit(row)">审核</a>
<span v-if="row.authStatus === 'TOBEAUDITED'" class="op-split">|</span>
<a class="link-text" @click.prevent="remove(row)">下架</a>
</template>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -108,34 +155,162 @@
/> />
</div> </div>
</el-card> </el-card>
<el-dialog
v-model="auditModalVisible"
:title="auditTargetIds.length > 1 ? '批量审核分销商品' : '审核分销商品'"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form :model="auditForm" label-width="90px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.authStatus">
<el-radio value="PASS">通过</el-radio>
<el-radio value="REFUSE">驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="auditForm.authStatus === 'REFUSE'" label="审核备注">
<el-input
v-model="auditForm.authRemark"
type="textarea"
:rows="3"
maxlength="255"
show-word-limit
placeholder="请输入驳回原因"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="auditModalVisible = false">取消</el-button>
<el-button type="primary" :loading="auditLoading" @click="submitAudit">确定</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { delDistributionGoods, getDistributionGoods } from "@/api/distribution"; import { auditDistributionGoods, delDistributionGoods, getDistributionGoods } from "@/api/distribution";
import vueQr from "vue-qr"; import { getBrandListData, getCategoryTree } from "@/api/goods";
export default { export default {
name: "distributionGoods", name: "distributionGoods",
components: { vueQr },
data() { data() {
return { return {
loading: true, loading: true,
currentAuthStatus: "ALL",
auditModalVisible: false,
auditLoading: false,
auditTargetIds: [],
auditForm: {
authStatus: "PASS",
authRemark: "",
},
searchForm: { searchForm: {
pageNumber: 1, pageNumber: 1,
pageSize: 20, pageSize: 20,
sort: "createTime", sort: "createTime",
order: "desc", order: "desc",
goodsName: "",
storeName: "",
categoryId: "",
brandId: "",
authStatus: "",
}, },
categoryList: [],
categoryProps: {
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
},
brandList: [],
selectList: [], selectList: [],
selectCount: 0, selectCount: 0,
data: [], data: [],
total: 0, total: 0,
}; };
}, },
computed: {
hasPendingAuditSelection() {
return this.selectList.some((item) => item.authStatus === "TOBEAUDITED");
},
},
methods: { methods: {
init() { authStatusText(value) {
if (value === "TOBEAUDITED") return "待审核";
if (value === "REFUSE") return "审核未通过";
return "已审核";
},
authStatusTagType(value) {
if (value === "TOBEAUDITED") return "warning";
if (value === "REFUSE") return "danger";
return "success";
},
onAuthStatusChange(name) {
this.searchForm.authStatus = name === "ALL" ? "" : name;
this.searchForm.pageNumber = 1;
this.getDataList(); this.getDataList();
this.clearSelectAll();
},
openAudit(row) {
this.auditTargetIds = [row.id];
this.auditForm = { authStatus: "PASS", authRemark: "" };
this.auditModalVisible = true;
},
openBatchAudit() {
const pendingRows = this.selectList.filter((item) => item.authStatus === "TOBEAUDITED");
if (!pendingRows.length) {
this.$Message.warning("请选择待审核的分销商品");
return;
}
this.auditTargetIds = pendingRows.map((item) => item.id);
this.auditForm = { authStatus: "PASS", authRemark: "" };
this.auditModalVisible = true;
},
submitAudit() {
if (this.auditForm.authStatus === "REFUSE" && !this.auditForm.authRemark.trim()) {
this.$Message.warning("请填写驳回原因");
return;
}
this.auditLoading = true;
auditDistributionGoods({
ids: this.auditTargetIds,
authStatus: this.auditForm.authStatus,
authRemark: this.auditForm.authRemark.trim(),
})
.then((res) => {
if (res.success) {
this.$Message.success("审核成功");
this.auditModalVisible = false;
this.clearSelectAll();
this.getDataList();
}
})
.finally(() => {
this.auditLoading = false;
});
},
goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
},
init() {
this.loadFilterOptions();
this.getDataList();
},
loadFilterOptions() {
getCategoryTree().then((res) => {
if (res.success) {
this.categoryList = res.result || [];
}
});
getBrandListData().then((res) => {
this.brandList = Array.isArray(res) ? res : res.result || [];
});
}, },
changePage(v) { changePage(v) {
this.searchForm.pageNumber = v; this.searchForm.pageNumber = v;
@@ -222,17 +397,39 @@ export default {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
} }
.div-zoom {
overflow: hidden; .goods-thumbnail {
text-overflow: ellipsis; width: 50px;
white-space: nowrap; height: 50px;
margin-bottom: 4px; object-fit: cover;
}
.hover-pointer {
cursor: pointer;
vertical-align: middle; vertical-align: middle;
} }
.goods-thumbnail-empty {
display: inline-flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
background: #f5f7fa;
color: #909399;
font-size: 12px;
}
.mt_10 { .mt_10 {
margin-top: 10px; margin-top: 10px;
} }
.auth-tabs {
margin-bottom: 4px;
}
.list-card {
margin-top: 16px;
}
.op-split {
margin: 0 6px;
color: #dcdfe6;
}
</style> </style>

View File

@@ -0,0 +1,450 @@
<template>
<div class="search">
<el-card>
<div class="operation padding-row">
<el-button type="primary" :disabled="data.length >= 5" @click="addRow">添加分销等级</el-button>
<span v-if="data.length >= 5" class="limit-tip">最多添加5个等级</span>
</div>
<el-table v-loading="loading" border :data="data" class="mt_10 grade-table" style="width: 100%">
<el-table-column label="等级值" width="100" align="center">
<template #default="{ row }">
<span>{{ row.sortOrder }}</span>
</template>
</el-table-column>
<el-table-column label="等级名" min-width="140">
<template #default="{ row }">
<el-input v-model="row.gradeName" maxlength="50" placeholder="请输入等级名" />
</template>
</el-table-column>
<el-table-column label="升级规则" min-width="300">
<template #default="{ row }">
<div v-if="isBaseGrade(row)" class="no-condition-tip">初始等级无需设置</div>
<div v-else class="grade-edit-lines">
<div class="grade-edit-line">
<el-checkbox v-model="row.enableSalesCondition" />
<span class="line-label">销售额达到</span>
<el-input-number
v-model="row.salesThreshold"
:min="0"
:precision="2"
:disabled="!row.enableSalesCondition"
controls-position="right"
class="inline-number"
/>
<span class="line-unit"></span>
</div>
<div class="grade-edit-line">
<el-checkbox v-model="row.enableCommissionCondition" />
<span class="line-label">收益额</span>
<el-input-number
v-model="row.commissionThreshold"
:min="0"
:precision="2"
:disabled="!row.enableCommissionCondition"
controls-position="right"
class="inline-number"
/>
<span class="line-unit"></span>
</div>
<div class="grade-edit-line">
<el-checkbox v-model="row.enableInviteCondition" />
<span class="line-label">邀请人数</span>
<el-input-number
v-model="row.inviteThreshold"
:min="0"
:precision="0"
:disabled="!row.enableInviteCondition"
controls-position="right"
class="inline-number"
/>
<span class="line-unit"></span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="佣金返利" min-width="220">
<template #default="{ row }">
<div class="grade-edit-lines">
<div class="grade-edit-line">
<span class="line-label">佣金比例</span>
<el-input-number
v-model="row.goodsCommissionRate"
:min="0"
:max="100"
:precision="2"
controls-position="right"
class="inline-number"
/>
<span class="line-unit">%</span>
</div>
<div class="grade-edit-line">
<span class="line-label">邀请奖励</span>
<el-input-number
v-model="row.inviteCommissionRate"
:min="0"
:max="100"
:precision="2"
controls-position="right"
class="inline-number"
/>
<span class="line-unit">%</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row, $index }">
<span v-if="isBaseGrade(row)" class="disabled-text">-</span>
<a v-else class="link-text" @click.prevent="remove(row, $index)">删除</a>
</template>
</el-table-column>
</el-table>
<div class="grade-footer">
<el-button type="primary" :loading="saveLoading" @click="saveAll">保存</el-button>
</div>
</el-card>
</div>
</template>
<script>
import {
getDistributionGrades,
saveAllDistributionGrades,
deleteDistributionGrade,
} from "@/api/distribution";
const MAX_GRADE_COUNT = 5;
const GRADE_VALUE_OPTIONS = [1, 2, 3, 4, 5];
const BASE_GRADE_VALUE = 1;
const BASE_GRADE_NAME = "普通分销员";
const buildDefaultRow = (sortOrder = null) => {
const isBaseGrade = sortOrder === BASE_GRADE_VALUE;
return {
_key: `grade_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
id: "",
gradeName: isBaseGrade ? BASE_GRADE_NAME : "",
sortOrder,
salesThreshold: 0,
commissionThreshold: 0,
inviteThreshold: 0,
enableSalesCondition: false,
enableCommissionCondition: false,
enableInviteCondition: false,
goodsCommissionRate: 0,
inviteCommissionRate: 0,
};
};
export default {
name: "distributionGrade",
data() {
return {
loading: true,
saveLoading: false,
data: [],
};
},
mounted() {
this.getDataList();
},
methods: {
normalizeRow(row, index) {
const normalized = {
...buildDefaultRow(row.sortOrder),
...row,
enableSalesCondition: row.enableSalesCondition !== false,
enableCommissionCondition: row.enableCommissionCondition !== false,
enableInviteCondition: row.enableInviteCondition !== false,
_key: row._key || row.id || `grade_${index}_${Date.now()}`,
};
if (this.isBaseGrade(normalized)) {
normalized.enableSalesCondition = false;
normalized.enableCommissionCondition = false;
normalized.enableInviteCondition = false;
if (!normalized.gradeName || !normalized.gradeName.trim()) {
normalized.gradeName = BASE_GRADE_NAME;
}
}
return normalized;
},
isBaseGrade(row) {
return row && row.sortOrder === BASE_GRADE_VALUE;
},
getSortedRows() {
return this.data.slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
},
getGradeLabel(row) {
return row.gradeName && row.gradeName.trim() ? row.gradeName.trim() : `等级值${row.sortOrder}`;
},
getNextGradeValue() {
const used = new Set(this.data.map((item) => item.sortOrder));
return GRADE_VALUE_OPTIONS.find((value) => value !== BASE_GRADE_VALUE && !used.has(value)) || null;
},
getDataList() {
this.loading = true;
getDistributionGrades().then((res) => {
this.loading = false;
if (res.success) {
this.data = (res.result || [])
.slice()
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
.map((item, index) => this.normalizeRow(item, index));
}
});
},
addRow() {
if (this.data.length >= MAX_GRADE_COUNT) {
this.$Message.warning("最多添加5个分销等级");
return;
}
const nextGradeValue = this.getNextGradeValue();
if (!nextGradeValue) {
this.$Message.warning("等级值1-5已全部使用");
return;
}
this.data.push(buildDefaultRow(nextGradeValue));
},
validateGradeProgression() {
const rows = this.getSortedRows();
for (let i = 1; i < rows.length; i++) {
const lower = rows[i - 1];
const higher = rows[i];
const lowerLabel = this.getGradeLabel(lower);
const higherLabel = this.getGradeLabel(higher);
if (lower.enableSalesCondition) {
if (!higher.enableSalesCondition) {
this.$Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的销售额升级条件`);
return false;
}
if (Number(higher.salesThreshold || 0) < Number(lower.salesThreshold || 0)) {
this.$Message.warning(`${higherLabel}销售额阈值不能小于${lowerLabel}`);
return false;
}
}
if (lower.enableCommissionCondition) {
if (!higher.enableCommissionCondition) {
this.$Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的收益额升级条件`);
return false;
}
if (Number(higher.commissionThreshold || 0) < Number(lower.commissionThreshold || 0)) {
this.$Message.warning(`${higherLabel}收益额阈值不能小于${lowerLabel}`);
return false;
}
}
if (lower.enableInviteCondition) {
if (!higher.enableInviteCondition) {
this.$Message.warning(`${higherLabel}必须启用${lowerLabel}已启用的邀请人数升级条件`);
return false;
}
if (Number(higher.inviteThreshold || 0) < Number(lower.inviteThreshold || 0)) {
this.$Message.warning(`${higherLabel}邀请人数阈值不能小于${lowerLabel}`);
return false;
}
}
}
return true;
},
validateRows() {
if (!this.data.length) {
this.$Message.warning("请至少添加一个分销等级");
return false;
}
const usedValues = [];
for (let i = 0; i < this.data.length; i++) {
const row = this.data[i];
if (!row.sortOrder) {
this.$Message.warning(`请填写第${i + 1}行等级值`);
return false;
}
if (usedValues.includes(row.sortOrder)) {
this.$Message.warning("等级值不能重复");
return false;
}
usedValues.push(row.sortOrder);
if (!row.gradeName || !row.gradeName.trim()) {
this.$Message.warning(`请填写第${i + 1}行等级名`);
return false;
}
if (this.isBaseGrade(row)) {
continue;
}
if (!row.enableSalesCondition && !row.enableCommissionCondition && !row.enableInviteCondition) {
this.$Message.warning(`${i + 1}行请至少启用一项升级规则`);
return false;
}
const rowLabel = this.getGradeLabel(row);
if (row.enableSalesCondition && Number(row.salesThreshold || 0) <= 0) {
this.$Message.warning(`${rowLabel}销售额必须大于0`);
return false;
}
if (row.enableCommissionCondition && Number(row.commissionThreshold || 0) <= 0) {
this.$Message.warning(`${rowLabel}收益额必须大于0`);
return false;
}
if (row.enableInviteCondition && Number(row.inviteThreshold || 0) <= 0) {
this.$Message.warning(`${rowLabel}邀请人数必须大于0`);
return false;
}
}
return this.validateGradeProgression() && this.validateCrossRates();
},
validateCrossRates() {
let maxGoods = 0;
let maxInvite = 0;
for (const row of this.data) {
maxGoods = Math.max(maxGoods, Number(row.goodsCommissionRate || 0));
maxInvite = Math.max(maxInvite, Number(row.inviteCommissionRate || 0));
}
if (maxGoods + maxInvite > 100) {
this.$Message.warning(
`所有等级中最高佣金比例(${maxGoods}%)与最高邀请奖励(${maxInvite}%之和不能超过100%`
);
return false;
}
for (const row of this.data) {
const goods = Number(row.goodsCommissionRate || 0);
const invite = Number(row.inviteCommissionRate || 0);
if (goods + invite > 100) {
this.$Message.warning(`${this.getGradeLabel(row)}的佣金比例与邀请奖励之和不能超过100%`);
return false;
}
}
return true;
},
buildPayload(row) {
const isBaseGrade = this.isBaseGrade(row);
return {
id: row.id || undefined,
gradeName: row.gradeName.trim(),
sortOrder: row.sortOrder,
salesThreshold: row.salesThreshold,
commissionThreshold: row.commissionThreshold,
inviteThreshold: row.inviteThreshold,
enableSalesCondition: isBaseGrade ? false : row.enableSalesCondition,
enableCommissionCondition: isBaseGrade ? false : row.enableCommissionCondition,
enableInviteCondition: isBaseGrade ? false : row.enableInviteCondition,
goodsCommissionRate: row.goodsCommissionRate,
inviteCommissionRate: row.inviteCommissionRate,
};
},
async saveAll() {
if (!this.validateRows()) {
return;
}
this.saveLoading = true;
try {
const payload = this.getSortedRows().map((row) => this.buildPayload(row));
const res = await saveAllDistributionGrades(payload);
if (res.success) {
this.$Message.success("保存成功");
this.getDataList();
}
} catch (e) {
// request layer handles error message
} finally {
this.saveLoading = false;
}
},
remove(row, index) {
if (this.isBaseGrade(row)) {
this.$Message.warning("默认分销等级不能删除");
return;
}
const name = row.gradeName || `等级值${row.sortOrder || ""}`;
if (!row.id) {
this.$Modal.confirm({
title: "确认删除",
content: `您确认要删除 ${name} 吗?`,
onOk: () => {
this.data.splice(index, 1);
},
});
return;
}
this.$Modal.confirm({
title: "确认删除",
content: `您确认要删除 ${name} 吗?`,
loading: true,
onOk: () => {
deleteDistributionGrade(row.id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("删除成功");
this.getDataList();
}
});
},
});
},
},
};
</script>
<style scoped lang="scss">
.grade-footer {
margin-top: 16px;
padding-top: 8px;
}
.limit-tip {
margin-left: 12px;
color: #909399;
font-size: 12px;
}
.grade-table :deep(.el-table__cell) {
vertical-align: top;
}
.grade-table :deep(.el-table__cell .cell) {
white-space: normal;
line-height: 1.6;
}
.grade-edit-lines {
display: flex;
flex-direction: column;
gap: 8px;
}
.grade-edit-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.line-label {
min-width: 70px;
color: #606266;
font-size: 13px;
}
.line-unit {
color: #606266;
font-size: 13px;
}
.inline-number {
width: 120px;
}
.no-condition-tip {
color: #909399;
font-size: 13px;
}
.disabled-text {
color: #c0c4cc;
}
.link-text {
color: #2d8cf0;
cursor: pointer;
user-select: none;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div class="search">
<el-card>
<el-form
ref="searchForm"
:model="searchForm"
inline
label-width="110px"
class="search-form"
@keyup.enter="handleSearch"
>
<el-form-item label="分销员昵称" prop="memberName">
<el-input
v-model="searchForm.memberName"
placeholder="请输入分销员昵称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="分销员手机号" prop="distributionMobile">
<el-input
v-model="searchForm.distributionMobile"
placeholder="请输入分销员手机号"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="变更后等级" prop="gradeId">
<el-select v-model="searchForm.gradeId" clearable placeholder="全部" style="width: 200px">
<el-option
v-for="item in gradeOptions"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item label="升级时间">
<el-date-picker
v-model="upgradeTimeRange"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
placeholder="选择时间"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table-column prop="distributionId" label="分销员ID" min-width="200" class-name="id-column" show-overflow-tooltip />
<el-table-column prop="memberName" label="分销员昵称" min-width="120" show-overflow-tooltip />
<el-table-column prop="memberMobile" label="分销员手机号" min-width="130" />
<el-table-column label="变更前等级" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row">{{ formatGrade(row.previousGradeName, row.previousGradeId) }}</span>
</template>
</el-table-column>
<el-table-column label="变更后等级" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row">{{ formatGrade(row.gradeName, row.gradeId) }}</span>
</template>
</el-table-column>
<el-table-column label="商品佣金比例" width="130" align="right">
<template #default="{ row }">
<span v-if="row">{{ formatRate(row.goodsCommissionRate) }}</span>
</template>
</el-table-column>
<el-table-column label="邀请佣金比例" width="130" align="right">
<template #default="{ row }">
<span v-if="row">{{ formatRate(row.inviteCommissionRate) }}</span>
</template>
</el-table-column>
<el-table-column prop="upgradeTime" label="升级时间" min-width="170" />
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getDistributionGradeHistoryPage, getDistributionGrades } from "@/api/distribution";
export default {
name: "distributionGradeHistory",
data() {
return {
loading: false,
searchForm: {
pageNumber: 1,
pageSize: 20,
memberName: "",
distributionMobile: "",
gradeId: "",
upgradeTimeStart: "",
upgradeTimeEnd: "",
},
upgradeTimeRange: [],
gradeOptions: [],
data: [],
total: 0,
};
},
mounted() {
this.loadGradeOptions();
this.getDataList();
},
methods: {
loadGradeOptions() {
getDistributionGrades()
.then((res) => {
if (!res.success) {
return;
}
this.gradeOptions = (res.result || [])
.slice()
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
.map((item) => ({
value: String(item.sortOrder),
label: item.gradeName || `等级值${item.sortOrder}`,
}));
})
.catch(() => {});
},
formatGrade(name, gradeId) {
if (name) {
return name;
}
if (gradeId) {
return `等级值${gradeId}`;
}
return "-";
},
formatRate(value) {
const num = Number(value);
if (!Number.isFinite(num)) {
return "-";
}
return `${num.toFixed(2).replace(/\.?0+$/, "") || "0"}%`;
},
applyUpgradeTimeRange() {
if (this.upgradeTimeRange && this.upgradeTimeRange.length === 2) {
this.searchForm.upgradeTimeStart = `${this.upgradeTimeRange[0]} 00:00:00`;
this.searchForm.upgradeTimeEnd = `${this.upgradeTimeRange[1]} 23:59:59`;
} else {
this.searchForm.upgradeTimeStart = "";
this.searchForm.upgradeTimeEnd = "";
}
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
memberName: "",
distributionMobile: "",
gradeId: "",
upgradeTimeStart: "",
upgradeTimeEnd: "",
};
this.upgradeTimeRange = [];
this.getDataList();
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.applyUpgradeTimeRange();
this.loading = true;
getDistributionGradeHistoryPage(this.searchForm)
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.catch(() => {
this.loading = false;
});
},
},
};
</script>
<style scoped>
.search-form {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<div class="search">
<el-card>
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table-column prop="id" label="ID" min-width="200" class-name="id-column" show-overflow-tooltip />
<el-table-column prop="totalCount" label="导入总数" width="110" />
<el-table-column prop="successCount" label="成功数" width="100" />
<el-table-column prop="failCount" label="失败数" width="100" />
<el-table-column label="导入状态" width="120">
<template #default="{ row }">
<span v-if="row">{{ statusLabel(row.status) }}</span>
</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" min-width="120" show-overflow-tooltip />
<el-table-column prop="createTime" label="导入时间" min-width="170" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row">
<a
v-if="row.failCount > 0"
class="link-text"
@click="downloadFailList(row)"
>下载失败列表</a>
<span v-else>-</span>
</template>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</el-card>
</div>
</template>
<script>
import {
downloadDistributionImportFailItems,
getDistributionImportBatches,
} from "@/api/distribution";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "distributionImportRecord",
data() {
return {
loading: false,
searchForm: {
pageNumber: 1,
pageSize: 20,
},
data: [],
total: 0,
};
},
mounted() {
this.getDataList();
},
methods: {
statusLabel(status) {
const map = {
PROCESSING: "处理中",
DONE: "已完成",
FAILED: "失败",
};
return map[status] || status || "-";
},
changePage() {
this.getDataList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getDistributionImportBatches(this.searchForm)
.then((res) => {
this.loading = false;
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.catch(() => {
this.loading = false;
});
},
downloadFailList(row) {
if (!row || !row.failCount) return;
downloadDistributionImportFailItems(row.id)
.then((blob) => {
if (blob && blob.type && blob.type.includes("application/json")) {
const reader = new FileReader();
reader.onload = () => {
try {
const result = JSON.parse(reader.result);
this.$Message.error(result.message || "下载失败");
} catch (e) {
this.$Message.error("下载失败");
}
};
reader.readAsText(blob);
return;
}
downloadBlob(blob, `分销员导入失败列表_${row.id}.xlsx`);
this.$Message.success("下载成功");
})
.catch(() => {
this.$Message.error("下载失败");
});
},
},
};
</script>
<style scoped lang="scss">
.link-text {
color: #2d8cf0;
cursor: pointer;
text-decoration: none;
}
.mt_10 {
margin-top: 10px;
}
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="search">
<el-card> <el-card>
<el-form <el-form
ref="searchForm" ref="searchForm"
@@ -17,10 +17,10 @@
style="width: 240px" style="width: 240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="分销" prop="distributionName"> <el-form-item label="分销" prop="distributionName">
<el-input <el-input
v-model="searchForm.distributionName" v-model="searchForm.distributionName"
placeholder="请输入分销名称" placeholder="请输入分销名称"
clearable clearable
style="width: 240px" style="width: 240px"
/> />
@@ -60,7 +60,13 @@
</el-form> </el-form>
</el-card> </el-card>
<el-card> <el-card class="list-card">
<el-tabs v-model="currentStatus" class="status-tabs" @tab-change="onStatusTabChange">
<el-tab-pane label="全部" name="ALL" />
<el-tab-pane label="未完成" name="NO_COMPLETED" />
<el-tab-pane label="完成" name="COMPLETE" />
<el-tab-pane label="退款" name="REFUND" />
</el-tabs>
<el-table <el-table
ref="table" ref="table"
v-loading="loading" v-loading="loading"
@@ -76,38 +82,42 @@
fixed="left" fixed="left"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column label="客户信息" min-width="180">
<template #default="{ row }">
<div v-if="row" class="member-cell">
<div class="member-name">{{ row.memberName || "-" }}</div>
<div class="member-id">客户ID{{ row.memberId || "-" }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="商品信息" min-width="200"> <el-table-column label="商品信息" min-width="200">
<template #default="{ row }"> <template #default="{ row }">
<div v-if="row" class="goods-msg"> <div v-if="row" class="goods-msg">
<img :src="row.image" width="60" height="60" alt="" /> <img v-if="row.image" :src="row.image" class="goods-thumb" alt="" />
<div> <span v-else class="goods-thumb-empty">无图</span>
<div class="div-zoom"> <div class="goods-info">
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a> <div class="goods-name">{{ row.goodsName }}</div>
</div> <div class="goods-num">数量x{{ row.num }}</div>
<div style="color: #999; font-size: 10px">数量x{{ row.num }}</div>
<el-popover trigger="hover" title="扫码在手机中查看" placement="top" width="180">
<template #reference>
<img
src="../../assets/qrcode.svg"
class="hover-pointer"
width="20"
height="20"
alt="qrcode"
/>
</template>
<vue-qr
:text="wapLinkTo(row.goodsId, row.skuId)"
:margin="0"
color-dark="#000"
color-light="#fff"
:size="150"
/>
</el-popover>
</div> </div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="distributionName" label="分销" min-width="100" show-overflow-tooltip /> <el-table-column label="一级分销" min-width="180">
<template #default="{ row }">
<div v-if="row" class="member-cell">
<div class="member-name">{{ row.distributionName || "-" }}</div>
<div class="member-id">分销员ID{{ row.distributionId || "-" }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="二级分销员" min-width="180">
<template #default="{ row }">
<div v-if="row" class="member-cell">
<div class="member-name">{{ row.inviteDistributionName || "-" }}</div>
<div class="member-id">分销员ID{{ row.inviteDistributionId || "-" }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="storeName" label="店铺名称" min-width="100" show-overflow-tooltip /> <el-table-column prop="storeName" label="店铺名称" min-width="100" show-overflow-tooltip />
<el-table-column label="状态" min-width="90"> <el-table-column label="状态" min-width="90">
<template #default="{ row }"> <template #default="{ row }">
@@ -116,10 +126,18 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="佣金金额" min-width="100"> <el-table-column label="一级佣金" min-width="100">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }"> <span v-if="row" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.rebate, "") }}</span> {{ formatCommission(row.directCommission) }}
</span>
</template>
</el-table-column>
<el-table-column label="二级佣金" min-width="100">
<template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">
{{ formatCommission(row.inviteCommission) }}
</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160" fixed="right" /> <el-table-column prop="createTime" label="创建时间" min-width="160" fixed="right" />
@@ -145,23 +163,23 @@
import { getDistributionOrder } from "@/api/distribution"; import { getDistributionOrder } from "@/api/distribution";
import { orderStatusList } from "./dataJson"; import { orderStatusList } from "./dataJson";
import { getShopListData } from "@/api/shops"; import { getShopListData } from "@/api/shops";
import vueQr from "vue-qr";
export default { export default {
name: "distributionOrder", name: "distributionOrder",
components: { vueQr },
data() { data() {
return { return {
timeRange: [], timeRange: [],
orderStatusList, orderStatusList,
shopList: [], shopList: [],
distributionId: this.$route.query.id, distributionId: this.$route.query.id,
currentStatus: "ALL",
loading: true, loading: true,
searchForm: { searchForm: {
pageNumber: 1, pageNumber: 1,
pageSize: 20, pageSize: 20,
sort: "create_time", sort: "create_time",
order: "desc", order: "desc",
distributionOrderStatus: "",
}, },
data: [], data: [],
total: 0, total: 0,
@@ -178,6 +196,12 @@ export default {
this.getDataList(); this.getDataList();
this.getShopList(); this.getShopList();
}, },
onStatusTabChange(name) {
this.currentStatus = name;
this.searchForm.distributionOrderStatus = name === "ALL" ? "" : name;
this.searchForm.pageNumber = 1;
this.getDataList();
},
changePage() { changePage() {
this.getDataList(); this.getDataList();
}, },
@@ -191,7 +215,11 @@ export default {
this.getDataList(); this.getDataList();
}, },
getDataList() { getDataList() {
if (this.distributionId) {
this.searchForm.distributionId = this.distributionId; this.searchForm.distributionId = this.distributionId;
} else {
delete this.searchForm.distributionId;
}
this.loading = true; this.loading = true;
if (this.timeRange && this.timeRange[0] && this.timeRange[1]) { if (this.timeRange && this.timeRange[0] && this.timeRange[1]) {
const startTime = new Date(this.timeRange[0]).getTime(); const startTime = new Date(this.timeRange[0]).getTime();
@@ -249,6 +277,12 @@ export default {
} }
return "warning"; return "warning";
}, },
formatCommission(amount) {
if (amount === null || amount === undefined || amount === "") {
return "-";
}
return this.$filters.unitPrice(amount, "¥");
},
}, },
mounted() { mounted() {
this.init(); this.init();
@@ -256,26 +290,72 @@ export default {
}; };
</script> </script>
<style lang="scss"> <style scoped lang="scss">
.member-cell {
line-height: 1.4;
}
.member-name {
color: #303133;
}
.member-id {
margin-top: 2px;
color: #909399;
font-size: 12px;
word-break: break-all;
}
.goods-msg { .goods-msg {
display: flex; display: flex;
align-items: center; align-items: center;
> div { gap: 10px;
margin-left: 10px; padding: 2px 0;
} }
.goods-thumb {
width: 50px;
height: 50px;
object-fit: cover;
flex-shrink: 0;
} }
.goods-thumb-empty {
.link-text { display: inline-flex;
color: #409eff; align-items: center;
cursor: pointer; justify-content: center;
text-decoration: none; width: 50px;
height: 50px;
flex-shrink: 0;
background: #f5f7fa;
color: #909399;
font-size: 12px;
} }
.goods-info {
.hover-pointer { min-width: 0;
cursor: pointer; }
.goods-name {
line-height: 1.4;
word-break: break-word;
}
.goods-num {
color: #999;
font-size: 12px;
margin-top: 2px;
} }
.mt_10 { .mt_10 {
margin-top: 10px; margin-top: 10px;
} }
.list-card {
margin-top: 16px;
}
.status-tabs {
margin-bottom: 4px;
:deep(.el-tabs__content) {
display: none;
}
:deep(.el-tabs__header) {
margin-bottom: 0;
}
}
</style> </style>

View File

@@ -0,0 +1,600 @@
<template>
<div class="distribution-recruit" style="background-color: #fff; padding: 10px">
<el-form
ref="planFormRef"
:model="planForm"
label-width="160px"
label-position="right"
label-suffix=""
>
<el-divider content-position="left">招募设置</el-divider>
<el-form-item label="分销员加入条件">
<div class="join-condition-wrap">
<el-radio-group v-model="planForm.joinConditionType">
<el-radio value="NONE">无条件</el-radio>
<el-radio value="CONDITIONAL">有条件</el-radio>
</el-radio-group>
<div v-if="planForm.joinConditionType === 'CONDITIONAL'" class="join-condition-block">
<div class="join-condition-item">
<el-checkbox v-model="planForm.requirePurchaseGoods">需购买指定商品</el-checkbox>
<div v-if="planForm.requirePurchaseGoods" class="join-condition-detail">
<div class="form-desc">
请确保以下商品已上架若配置前已购买任一指定商品的用户则已经满足条件无需再次购买
</div>
<div class="goods-toolbar">
<a class="link-btn" @click.prevent="openGoodsSelect">选择商品</a>
</div>
<el-table
v-if="recruitGoodsList.length"
border
:data="recruitGoodsList"
class="recruit-goods-table"
>
<el-table-column label="商品图" width="90" align="center">
<template #default="{ row }">
<img
v-if="row.thumbnail"
:src="row.thumbnail"
alt="商品图"
class="recruit-goods-thumb"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="商品名称" min-width="200">
<template #default="{ row }">
<div class="recruit-goods-name">{{ row.goodsName }}</div>
</template>
</el-table-column>
<el-table-column prop="sn" label="商品编码" min-width="120" show-overflow-tooltip />
<el-table-column label="SKU信息" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
{{ row.simpleSpecs ? row.simpleSpecs.trim() : "-" }}
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ $index }">
<a class="link-btn delete-btn" @click.prevent="removeRecruitGoods($index)">删除</a>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="join-condition-item">
<el-checkbox v-model="planForm.requireSelfPurchaseAmount">
自购金额满
<el-input-number
v-model="planForm.minSelfPurchaseAmount"
:min="0.01"
:precision="2"
:disabled="!planForm.requireSelfPurchaseAmount"
controls-position="right"
class="inline-number"
/>
</el-checkbox>
<div class="form-desc join-condition-desc">
客户支付后即计入自购金额自购金额最低设置为0.01
</div>
</div>
<div class="join-condition-item">
<el-checkbox v-model="planForm.requireConsumeCount">
消费笔数满
<el-input-number
v-model="planForm.minConsumeCount"
:min="1"
:precision="0"
:disabled="!planForm.requireConsumeCount"
controls-position="right"
class="inline-number"
/>
</el-checkbox>
<div class="form-desc join-condition-desc">
客户支付后即计入消费笔数消费笔数最低设置为1笔
</div>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="分销员申请方式" required>
<div class="apply-mode-block">
<el-radio-group v-model="planForm.applyMode">
<el-radio value="MANUAL">手动申请</el-radio>
<el-radio value="AUTO">满足条件自动申请</el-radio>
</el-radio-group>
<div v-if="planForm.applyMode === 'MANUAL'" class="manual-apply-extra">
<el-checkbox v-model="planForm.requireApplyInfo">需要填写申请信息</el-checkbox>
<div class="form-desc">申请人需填写对应字段方便审核分销员信息</div>
<template v-if="planForm.requireApplyInfo">
<div class="recruit-field-toolbar">
<a class="link-btn" @click.prevent="addRecruitField">+ 添加字段</a>
</div>
<div
v-for="(field, index) in recruitFields"
:key="field._key"
class="recruit-field-row"
>
<div class="recruit-field-row-main">
<span class="field-label">字段{{ index + 1 }}</span>
<el-input
v-model="field.fieldName"
placeholder="如:姓名"
maxlength="64"
style="width: 200px"
/>
<el-select v-model="field.fieldType" style="width: 140px; margin-left: 8px">
<el-option
v-for="item in fieldTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<a class="link-btn delete-btn" @click.prevent="removeRecruitField(index)">删除字段</a>
</div>
<div class="recruit-field-row-required">
<el-radio-group v-model="field.required">
<el-radio :value="true">必填</el-radio>
<el-radio :value="false">选填</el-radio>
</el-radio-group>
</div>
</div>
</template>
</div>
</div>
</el-form-item>
<el-form-item label="审核方式">
<el-radio-group v-model="planForm.auditMode">
<el-radio value="MANUAL">人工审核</el-radio>
<el-radio value="AUTO_PASS">无需审核满足条件自动通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="展示线上招募入口">
<el-radio-group v-model="planForm.onlineEntryVisible">
<el-radio :value="true">展示</el-radio>
<el-radio :value="false">不展示</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="招募海报">
<div class="recruit-poster-block">
<div v-if="planForm.posterBackground" class="poster-preview-wrap">
<img :src="planForm.posterBackground" alt="招募海报" class="poster-thumb" />
</div>
<upload-pic-input v-model="planForm.posterBackground" :show-input="false" />
</div>
</el-form-item>
<el-form-item label="分销推广服务协议">
<editor v-if="editorReady" ref="agreementEditor" open-xss v-model="planForm.agreementContent" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="planSaveLoading" @click="savePlan">保存</el-button>
</el-form-item>
</el-form>
<sku-select ref="skuSelect" @selectedGoodsData="selectedGoodsData" />
</div>
</template>
<script>
import {
getCurrentRecruitPlan,
saveRecruitPlan,
} from "@/api/distribution";
import uploadPicInput from "@/components/lili/upload-pic-input";
import tinymec from "@/components/editor/index.vue";
import skuSelect from "@/components/lili-dialog";
const MAX_RECRUIT_FIELDS = 5;
const FIELD_TYPE_OPTIONS = [
{ value: "NAME", label: "姓名格式" },
{ value: "MOBILE", label: "手机号格式" },
{ value: "IMAGE", label: "图片格式" },
{ value: "GENDER", label: "性别格式" },
];
const buildDefaultPlan = () => ({
id: "",
planVersion: null,
publishStatus: false,
joinConditionType: "NONE",
applyMode: "MANUAL",
auditMode: "MANUAL",
onlineEntryVisible: true,
requireApplyInfo: false,
agreementContent: "",
requirePurchaseGoods: false,
requireSelfPurchaseAmount: false,
requireConsumeCount: false,
minSelfPurchaseAmount: 0.01,
minConsumeCount: 1,
goodsIds: [],
posterEnabled: false,
posterTitle: "",
posterBenefitText: "",
posterRuleText: "",
posterContact: "",
posterBackground: "",
});
const buildDefaultField = () => ({
_key: `field_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
fieldName: "",
fieldType: "NAME",
required: true,
placeholder: "",
});
export default {
name: "distributionRecruit",
components: { uploadPicInput, editor: tinymec, skuSelect },
data() {
return {
planSaveLoading: false,
editorReady: true,
planForm: buildDefaultPlan(),
recruitFields: [],
recruitGoodsList: [],
fieldTypeOptions: FIELD_TYPE_OPTIONS,
};
},
mounted() {
this.loadPlan();
},
watch: {
"planForm.applyMode"(value) {
if (value !== "MANUAL") {
this.planForm.requireApplyInfo = false;
this.recruitFields = [];
}
},
"planForm.requireApplyInfo"(value) {
if (!value) {
this.recruitFields = [];
}
},
"planForm.joinConditionType"(value) {
if (value !== "CONDITIONAL") {
this.planForm.requirePurchaseGoods = false;
this.planForm.requireSelfPurchaseAmount = false;
this.planForm.requireConsumeCount = false;
this.recruitGoodsList = [];
}
},
"planForm.requirePurchaseGoods"(value) {
if (!value) {
this.recruitGoodsList = [];
}
},
"planForm.requireSelfPurchaseAmount"(value) {
if (value && (!this.planForm.minSelfPurchaseAmount || this.planForm.minSelfPurchaseAmount < 0.01)) {
this.planForm.minSelfPurchaseAmount = 0.01;
}
},
"planForm.requireConsumeCount"(value) {
if (value && (!this.planForm.minConsumeCount || this.planForm.minConsumeCount < 1)) {
this.planForm.minConsumeCount = 1;
}
},
},
methods: {
normalizeFields(fields) {
if (!fields || !fields.length) {
return [];
}
return fields.map((item, index) => ({
...buildDefaultField(),
...item,
_key: item._key || item.id || `field_${index}_${Date.now()}`,
required: item.required !== false,
}));
},
addRecruitField() {
if (this.recruitFields.length >= MAX_RECRUIT_FIELDS) {
this.$Message.warning("最多添加5个字段");
return;
}
this.recruitFields.push(buildDefaultField());
},
removeRecruitField(index) {
this.recruitFields.splice(index, 1);
},
validateRecruitFields() {
if (!this.planForm.requireApplyInfo) {
return true;
}
if (!this.recruitFields.length) {
this.$Message.warning("请至少添加一个申请字段");
return false;
}
for (let i = 0; i < this.recruitFields.length; i++) {
const field = this.recruitFields[i];
if (!field.fieldName || !field.fieldName.trim()) {
this.$Message.warning(`请填写字段${i + 1}名称`);
return false;
}
}
return true;
},
validateJoinConditions() {
if (this.planForm.joinConditionType !== "CONDITIONAL") {
return true;
}
const {
requirePurchaseGoods,
requireSelfPurchaseAmount,
requireConsumeCount,
} = this.planForm;
if (!requirePurchaseGoods && !requireSelfPurchaseAmount && !requireConsumeCount) {
this.$Message.warning("请至少选择一个加入条件");
return false;
}
if (requirePurchaseGoods && !this.recruitGoodsList.length) {
this.$Message.warning("请选择指定商品");
return false;
}
if (requireSelfPurchaseAmount && this.planForm.minSelfPurchaseAmount < 0.01) {
this.$Message.warning("自购金额最低设置为0.01元");
return false;
}
if (requireConsumeCount && this.planForm.minConsumeCount < 1) {
this.$Message.warning("消费笔数最低设置为1笔");
return false;
}
return true;
},
openGoodsSelect() {
this.$refs.skuSelect.open("goods");
const data = this.recruitGoodsList.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
sn: item.sn,
simpleSpecs: item.simpleSpecs,
price: item.price,
quantity: item.quantity,
small: item.thumbnail,
}));
this.$refs.skuSelect.goodsData = data;
},
selectedGoodsData(items) {
this.recruitGoodsList = (items || []).map((item) => ({
skuId: item.id,
goodsId: item.goodsId,
goodsName: item.goodsName,
sn: item.sn || "",
simpleSpecs: item.simpleSpecs || "",
price: item.price,
quantity: item.quantity,
thumbnail: item.small || item.thumbnail || "",
}));
},
removeRecruitGoods(index) {
this.recruitGoodsList.splice(index, 1);
},
buildSavePayload() {
const fields = this.planForm.requireApplyInfo
? this.recruitFields.map((field, index) => ({
id: field.id,
fieldName: field.fieldName.trim(),
fieldType: field.fieldType,
required: field.required !== false,
placeholder: field.placeholder || "",
sortOrder: index,
}))
: [];
return {
...this.planForm,
id: this.planForm.id || undefined,
publishStatus: true,
posterEnabled: !!(this.planForm.posterBackground && this.planForm.posterBackground.trim()),
recruitGoods: this.planForm.requirePurchaseGoods
? this.recruitGoodsList.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
sn: item.sn,
simpleSpecs: item.simpleSpecs,
thumbnail: item.thumbnail,
price: item.price,
}))
: [],
goodsIds: this.planForm.requirePurchaseGoods
? this.recruitGoodsList.map((item) => item.goodsId)
: [],
fields,
};
},
applyPlanResult(result) {
this.planForm = { ...buildDefaultPlan(), ...result };
this.recruitFields = this.normalizeFields(result.fields);
this.recruitGoodsList = (result.recruitGoods || []).map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
sn: item.sn || "",
simpleSpecs: item.simpleSpecs || "",
thumbnail: item.thumbnail,
price: item.price,
}));
},
loadPlan() {
getCurrentRecruitPlan().then((res) => {
if (res.success && res.result) {
this.applyPlanResult(res.result);
}
});
},
savePlan() {
if (!this.validateRecruitFields() || !this.validateJoinConditions()) {
return;
}
this.planSaveLoading = true;
saveRecruitPlan(this.buildSavePayload())
.then((res) => {
if (res.success) {
this.$Message.success("保存成功");
if (res.result) {
this.applyPlanResult(res.result);
} else {
this.loadPlan();
}
}
})
.finally(() => {
this.planSaveLoading = false;
});
},
},
};
</script>
<style scoped lang="scss">
.apply-mode-block {
width: 100%;
}
.manual-apply-extra {
margin-top: 12px;
.form-desc {
margin-top: 8px;
margin-left: 24px;
}
}
.recruit-field-toolbar {
margin: 12px 0 8px 24px;
}
.link-btn {
color: #2d8cf0;
cursor: pointer;
user-select: none;
}
.recruit-field-row {
margin: 12px 0 0 24px;
padding-bottom: 12px;
border-bottom: 1px dashed #e8eaec;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
.recruit-field-row-main {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.field-label {
min-width: 56px;
color: #606266;
}
.delete-btn {
margin-left: 8px;
}
.recruit-field-row-required {
margin-top: 10px;
padding-left: 56px;
}
.form-desc {
margin-top: 8px;
color: #909399;
font-size: 12px;
line-height: 1.6;
}
.join-condition-wrap {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.join-condition-block {
margin-top: 12px;
padding: 16px;
background: #f8f8f9;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
}
.join-condition-item + .join-condition-item {
margin-top: 16px;
}
.join-condition-detail {
margin-top: 8px;
margin-left: 24px;
}
.join-condition-desc {
margin-top: 8px;
margin-left: 24px;
}
.goods-toolbar {
margin: 8px 0;
}
.recruit-goods-table {
margin-top: 8px;
:deep(.el-table__cell) {
vertical-align: top;
}
:deep(.el-table__cell .cell) {
white-space: normal;
line-height: 1.5;
}
}
.recruit-goods-name {
white-space: normal;
word-break: break-all;
line-height: 1.5;
}
.recruit-goods-thumb {
display: block;
width: 56px;
height: 56px;
margin: 4px auto;
object-fit: contain;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.inline-number {
width: 120px;
margin: 0 6px;
vertical-align: middle;
}
.recruit-poster-block {
width: 100%;
max-width: 520px;
}
.poster-preview-wrap {
margin-bottom: 12px;
}
.poster-thumb {
display: block;
max-width: 240px;
max-height: 240px;
border: 1px solid #ebeef5;
border-radius: 4px;
}
</style>

View File

@@ -1,62 +1,976 @@
<template> <template>
<div style="background-color: #fff"> <div class="distribution-setting">
<el-form ref="form" :model="form" :rules="formRule" label-width="140px" style="padding: 10px"> <el-card>
<el-divider content-position="left">分销设置</el-divider> <el-tabs v-model="activeTab" class="setting-tabs">
<el-tab-pane label="基础设置" name="base">
<el-form
ref="form"
:model="form"
:rules="formRule"
label-width="180px"
label-position="right"
label-suffix=""
class="setting-form"
>
<el-form-item label="是否开启分销" prop="isOpen"> <el-form-item label="是否开启分销" prop="isOpen">
<el-switch v-model="form.isOpen" active-text="开启" inactive-text="关闭" /> <el-switch v-model="form.isOpen" active-text="开启" inactive-text="关闭" />
</el-form-item> </el-form-item>
<el-form-item label="分销关系绑定天数" prop="distributionDay"> <el-form-item label="分销员名称" prop="distributorName">
<el-input-number v-model="form.distributionDay" :min="1" :max="365" style="width: 120px" /> <div class="option-with-desc">
<el-input
v-model="form.distributorName"
maxlength="20"
show-word-limit
placeholder="如:推广大使"
style="width: 350px"
/>
<div class="form-desc">可自定义对平台分销员的称呼推广大使</div>
</div>
</el-form-item>
<el-form-item label="升级条件是否显示">
<div class="option-with-desc">
<el-radio-group v-model="form.upgradeConditionVisible">
<el-radio :value="true">显示</el-radio>
<el-radio :value="false">不显示</el-radio>
</el-radio-group>
<div class="form-desc">
选择显示后分销员可以看到不同等级分销员的权益佣金比例与邀请奖励
</div>
</div>
</el-form-item>
<el-form-item label="是否展示上级分销员">
<div class="option-with-desc">
<el-radio-group v-model="form.parentDistributorVisible">
<el-radio :value="true">展示</el-radio>
<el-radio :value="false">不展示</el-radio>
</el-radio-group>
<div class="form-desc">
选择展示后分销员可在个人中心查看上级分销员信息
</div>
</div>
</el-form-item>
<el-form-item label="成为分销员后上级归属">
<el-radio-group v-model="form.parentAttributionMode" class="radio-option-list">
<div class="radio-option-item">
<el-radio value="RECRUIT_INVITER">邀请方</el-radio>
<div class="form-desc">举例分销员A绑定客户B分销员C邀请客户B成为分销员B的上级为C</div>
</div>
<div class="radio-option-item">
<el-radio value="BOUND_DISTRIBUTOR">之前绑定的分销员</el-radio>
<div class="form-desc">举例分销员A绑定客户B分销员C邀请客户B成为分销员B的上级为A</div>
</div>
</el-radio-group>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="submit">保存</el-button> <el-button type="primary" :loading="saving.base" @click="submitBase">保存</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-tab-pane>
<el-tab-pane label="客户关系" name="customer">
<el-form
ref="customerForm"
:model="customerForm"
label-width="180px"
label-position="right"
label-suffix=""
class="setting-form"
>
<el-form-item label="锁客模式">
<el-radio-group v-model="customerForm.bindMode">
<el-radio value="PERMANENT">永久锁客</el-radio>
<el-radio value="SHORT_TERM">短期锁客</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="customerForm.bindMode === 'PERMANENT'">
<el-form-item label="永久锁客生效方式">
<el-radio-group v-model="customerForm.permanentBindMode">
<el-radio value="SCAN_IMMEDIATE">扫码立即永久绑定</el-radio>
<el-radio value="PAID_WITHIN_DAYS">支付后转永久</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="customerForm.permanentBindMode === 'PAID_WITHIN_DAYS'"
label="永久锁客支付天数"
>
<el-input-number
v-model="customerForm.permanentPaidDays"
:min="1"
:max="365"
:step="1"
:precision="0"
style="width: 120px"
@change="onPermanentPaidDaysChange"
/>
<span class="form-tip">支付后转永久时客户需在指定天数内完成支付</span>
</el-form-item>
</template>
<el-form-item v-if="customerForm.bindMode === 'SHORT_TERM'" label="短期锁客有效天数">
<el-input-number v-model="customerForm.shortTermBindDays" :min="1" :max="365" style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving.customer" @click="submitCustomer">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="结算方案" name="settlement">
<el-form
ref="settlementForm"
:model="settlementForm"
label-width="180px"
label-position="right"
label-suffix=""
class="setting-form"
>
<el-form-item label="分销员自购直接佣金归属">
<el-radio-group v-model="settlementForm.selfPurchaseCommissionMode">
<el-radio value="SELF">佣金给分销员自己</el-radio>
<el-radio value="BOUND_DISTRIBUTOR">佣金给绑定分销员</el-radio>
<el-radio value="NONE">无直接佣金</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving.settlement" @click="submitSettlement">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="平台海报" name="poster">
<el-form
ref="posterFormRef"
:model="posterForm"
label-width="180px"
label-position="right"
label-suffix=""
class="setting-form poster-form"
>
<el-form-item label="头像昵称">
<div class="option-with-desc">
<el-radio-group v-model="posterForm.memberInfoVisible">
<el-radio :value="true">展示</el-radio>
<el-radio :value="false">不展示</el-radio>
</el-radio-group>
<div class="form-desc">选择展示后分销员平台海报将显示头像和昵称</div>
</div>
</el-form-item>
<el-form-item label="背景图片">
<div class="poster-upload-block">
<div v-if="posterForm.backgroundImage" class="poster-preview-wrap">
<img :src="posterForm.backgroundImage" alt="平台海报背景" class="poster-bg-thumb" />
</div>
<upload-pic-input v-model="posterForm.backgroundImage" :show-input="false" />
<div class="form-desc">建议上传竖版图片用于平台海报背景</div>
</div>
</el-form-item>
<el-form-item label="推荐语">
<el-input
v-model="posterForm.slogan"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
placeholder="请输入平台海报推荐语"
style="width: 420px"
/>
</el-form-item>
<el-form-item label="文字颜色">
<el-color-picker v-model="posterForm.textColor" />
<span class="form-tip">{{ posterForm.textColor }}</span>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving.poster" @click="submitPoster">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="邀请卡" name="inviteCard">
<el-form
ref="inviteCardFormRef"
:model="inviteCardForm"
label-width="180px"
label-position="right"
label-suffix=""
class="setting-form poster-form"
>
<el-form-item label="头像昵称">
<div class="option-with-desc">
<el-radio-group v-model="inviteCardForm.memberInfoVisible">
<el-radio :value="true">展示</el-radio>
<el-radio :value="false">不展示</el-radio>
</el-radio-group>
<div class="form-desc">选择展示后分销员邀请卡将显示头像和昵称</div>
</div>
</el-form-item>
<el-form-item label="背景图片">
<div class="poster-upload-block">
<div v-if="inviteCardForm.backgroundImage" class="poster-preview-wrap">
<img :src="inviteCardForm.backgroundImage" alt="邀请卡背景" class="poster-bg-thumb" />
</div>
<upload-pic-input v-model="inviteCardForm.backgroundImage" :show-input="false" />
<div class="form-desc">建议上传竖版图片用于邀请卡背景</div>
</div>
</el-form-item>
<el-form-item label="推荐语">
<el-input
v-model="inviteCardForm.slogan"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
placeholder="请输入邀请卡推荐语"
style="width: 420px"
/>
</el-form-item>
<el-form-item label="文字颜色">
<el-color-picker v-model="inviteCardForm.textColor" />
<span class="form-tip">{{ inviteCardForm.textColor }}</span>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving.inviteCard" @click="submitInviteCard">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="商品分销设置" name="featured">
<el-form
ref="featuredForm"
:model="featuredForm"
label-width="220px"
label-position="right"
label-suffix=""
class="setting-form featured-form"
>
<div class="setting-section">
<div class="section-title">基础设置</div>
<el-form-item label="商品默认排序">
<el-select v-model="featuredForm.defaultSort" style="width: 420px">
<el-option
v-for="item in defaultSortOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="商品默认分享文案">
<el-input
v-model="featuredForm.defaultShareText"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
placeholder="请输入分销员分享商品时的默认文案"
style="width: 420px"
/>
</el-form-item>
</div>
<div class="setting-section">
<div class="section-title">主推商品</div>
<el-form-item label="分销员个人中心展示主推商品">
<div class="option-with-desc">
<el-radio-group v-model="featuredForm.centerVisible">
<el-radio :value="true">展示</el-radio>
<el-radio :value="false">不展示</el-radio>
</el-radio-group>
<div class="form-desc">
选择展示后分销员个人中心将增加主推商品版块促进分销员卖货
</div>
</div>
</el-form-item>
<el-form-item label="商品销量">
<div class="option-with-desc">
<el-radio-group v-model="featuredForm.showSales">
<el-radio :value="true">展示</el-radio>
<el-radio :value="false">不展示</el-radio>
</el-radio-group>
<div class="form-desc">当销量为 0 时自动隐藏商品销量</div>
</div>
</el-form-item>
<el-form-item label="主推商品">
<el-radio-group v-model="featuredForm.sourceMode">
<el-radio value="AUTO">自动匹配主推商品</el-radio>
<el-radio value="CUSTOM">自定义主推商品</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="featuredForm.sourceMode === 'AUTO'" label="匹配规则">
<el-radio-group v-model="featuredForm.matchRule">
<el-radio value="HIGHEST_COMMISSION">展示佣金最高的推广商品</el-radio>
<el-radio value="HIGHEST_SALES">展示销量最高的推广商品</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="featuredForm.sourceMode === 'CUSTOM'" label="自定义商品">
<div class="custom-goods-block">
<div class="form-desc">
请从已审核通过的分销商品中选择最多可配置 10 按列表顺序展示
</div>
<div class="goods-toolbar">
<a class="link-btn" @click.prevent="openFeaturedGoodsPicker">选择分销商品</a>
</div>
<el-table
v-if="featuredCustomGoods.length"
border
:data="featuredCustomGoods"
class="featured-goods-table"
>
<el-table-column label="商品图" width="90" align="center">
<template #default="{ row }">
<img
v-if="row.thumbnail"
:src="row.thumbnail"
alt="商品图"
class="featured-goods-thumb"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="商品名称" min-width="180">
<template #default="{ row }">
<div class="featured-goods-name">{{ row.goodsName }}</div>
</template>
</el-table-column>
<el-table-column label="规格" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.specs || "-" }}</template>
</el-table-column>
<el-table-column label="佣金" width="100">
<template #default="{ row }">
{{ $filters.unitPrice(row.commission, "¥") }}
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ $index }">
<a class="link-btn delete-btn" @click.prevent="removeFeaturedGoods($index)">删除</a>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
<el-form-item label="主推商品数量">
<el-radio-group v-model="featuredForm.displayCount">
<el-radio :value="5">5</el-radio>
<el-radio :value="10">10</el-radio>
</el-radio-group>
</el-form-item>
</div>
<el-form-item>
<el-button type="primary" :loading="saving.featured" @click="submitFeatured">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-card>
<el-dialog
v-model="featuredPickerVisible"
title="选择分销商品"
width="960px"
:close-on-click-modal="false"
destroy-on-close
@open="loadFeaturedPickerGoods"
>
<el-form inline class="featured-picker-search" @keyup.enter.prevent="searchFeaturedPickerGoods">
<el-form-item label="商品名称">
<el-input
v-model="featuredPickerSearch.goodsName"
clearable
placeholder="请输入商品名称"
style="width: 220px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchFeaturedPickerGoods">搜索</el-button>
</el-form-item>
</el-form>
<el-table
ref="featuredPickerTable"
v-loading="featuredPickerLoading"
border
:data="featuredPickerList"
max-height="420"
@selection-change="onFeaturedPickerSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="商品名称" min-width="200" show-overflow-tooltip prop="goodsName" />
<el-table-column label="规格" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.specs || "-" }}</template>
</el-table-column>
<el-table-column label="佣金" width="100">
<template #default="{ row }">{{ $filters.unitPrice(row.commission, "¥") }}</template>
</el-table-column>
<el-table-column label="店铺" min-width="120" show-overflow-tooltip prop="storeName" />
</el-table>
<div class="featured-picker-pagination">
<el-pagination
v-model:current-page="featuredPickerSearch.pageNumber"
v-model:page-size="featuredPickerSearch.pageSize"
:total="featuredPickerTotal"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next"
size="small"
@current-change="loadFeaturedPickerGoods"
@size-change="onFeaturedPickerPageSizeChange"
/>
</div>
<template #footer>
<el-button @click="featuredPickerVisible = false">取消</el-button>
<el-button type="primary" @click="confirmFeaturedPicker">确定</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { setSetting, getSetting } from "@/api/index"; import { setSetting, getSetting } from "@/api/index";
import {
getDistributionCustomerSetting,
putDistributionCustomerSetting,
getDistributionSettlementSetting,
putDistributionSettlementSetting,
getDistributionFeaturedGoodsSetting,
putDistributionFeaturedGoodsSetting,
getDistributionPromotionPosterSetting,
putDistributionPromotionPosterSetting,
getDistributionInviteCardSetting,
putDistributionInviteCardSetting,
getDistributionGoods,
} from "@/api/distribution";
import uploadPicInput from "@/components/lili/upload-pic-input";
import { regular } from "@/utils"; import { regular } from "@/utils";
const DEFAULT_FORM = {
isOpen: true,
distributorName: "分销员",
upgradeConditionVisible: true,
parentDistributorVisible: true,
parentAttributionMode: "RECRUIT_INVITER",
};
const DEFAULT_POSTER_FORM = {
memberInfoVisible: true,
backgroundImage: "",
slogan: "发现好物,邀请你一起逛逛",
textColor: "#1f2a44",
};
const DEFAULT_INVITE_CARD_FORM = {
memberInfoVisible: true,
backgroundImage: "",
slogan: "邀请您成为我的下级分销员",
textColor: "#1f2a44",
};
const DEFAULT_FEATURED_FORM = {
defaultSort: "HIGHEST_COMMISSION",
defaultShareText: "",
centerVisible: false,
showSales: false,
sourceMode: "AUTO",
matchRule: "HIGHEST_COMMISSION",
displayCount: 5,
};
const DEFAULT_SORT_OPTIONS = [
{ value: "HIGHEST_COMMISSION", label: "商品佣金越高越靠前" },
{ value: "HIGHEST_PRICE", label: "商品价格越高越靠前" },
{ value: "HIGHEST_SALES", label: "商品销量越高越靠前" },
{ value: "LATEST_LISTING", label: "商品上架越晚越靠前" },
];
export default { export default {
name: "distributionSetting", name: "distributionSetting",
components: {
uploadPicInput,
},
data() { data() {
return { return {
form: { activeTab: "base",
isOpen: true, saving: {
distributionDay: 1, base: false,
customer: false,
settlement: false,
featured: false,
poster: false,
inviteCard: false,
}, },
form: { ...DEFAULT_FORM },
formRule: { formRule: {
isOpen: [regular.REQUIRED], isOpen: [regular.REQUIRED],
distributionDay: [regular.REQUIRED], distributorName: [regular.REQUIRED, regular.VARCHAR20],
},
customerForm: {
bindMode: "SHORT_TERM",
permanentBindMode: "PAID_WITHIN_DAYS",
permanentPaidDays: 1,
shortTermBindDays: 7,
},
settlementForm: {
selfPurchaseCommissionMode: "SELF",
},
posterForm: { ...DEFAULT_POSTER_FORM },
inviteCardForm: { ...DEFAULT_INVITE_CARD_FORM },
featuredForm: { ...DEFAULT_FEATURED_FORM },
defaultSortOptions: DEFAULT_SORT_OPTIONS,
featuredCustomGoods: [],
featuredPickerVisible: false,
featuredPickerLoading: false,
featuredPickerList: [],
featuredPickerTotal: 0,
featuredPickerSelection: [],
featuredPickerSearch: {
goodsName: "",
authStatus: "PASS",
pageNumber: 1,
pageSize: 20,
}, },
}; };
}, },
mounted() { mounted() {
this.init(); this.init();
}, },
watch: {
"customerForm.permanentBindMode"(value) {
if (value === "PAID_WITHIN_DAYS") {
this.normalizePermanentPaidDays();
}
},
},
methods: { methods: {
init() { init() {
this.getDataList(); this.loadBaseSetting();
this.loadFeaturedSetting();
this.loadPosterSetting();
this.loadInviteCardSetting();
this.loadCustomerSetting();
this.loadSettlementSetting();
}, },
getDataList() { loadBaseSetting() {
getSetting("DISTRIBUTION_SETTING").then((res) => { getSetting("DISTRIBUTION_SETTING").then((res) => {
if (res.success) { if (!res.success || !res.result) {
this.form = res.result; return;
} }
const result = res.result;
this.form = {
...DEFAULT_FORM,
...result,
distributorName: result.distributorName || "分销员",
parentAttributionMode: result.parentAttributionMode || DEFAULT_FORM.parentAttributionMode,
};
}); });
}, },
submit() { loadPosterSetting() {
this.$refs.form.validate((valid) => { getDistributionPromotionPosterSetting().then((res) => {
if (!valid) return; if (!res.success || !res.result) {
setSetting("DISTRIBUTION_SETTING", this.form).then((res) => { return;
}
const result = res.result;
this.posterForm = {
...DEFAULT_POSTER_FORM,
memberInfoVisible: result.memberInfoVisible !== false,
backgroundImage: result.backgroundImage || "",
slogan: result.slogan || DEFAULT_POSTER_FORM.slogan,
textColor: result.textColor || DEFAULT_POSTER_FORM.textColor,
};
});
},
submitPoster() {
const slogan = (this.posterForm.slogan || "").trim();
if (!slogan) {
this.$Message.warning("请填写推荐语");
return;
}
this.saving.poster = true;
putDistributionPromotionPosterSetting({
memberInfoVisible: this.posterForm.memberInfoVisible,
backgroundImage: (this.posterForm.backgroundImage || "").trim(),
slogan,
textColor: this.posterForm.textColor || DEFAULT_POSTER_FORM.textColor,
})
.then((res) => {
if (res.success) { if (res.success) {
this.$Message.success("操作成功"); this.$Message.success("操作成功");
this.getDataList(); this.loadPosterSetting();
}
})
.finally(() => {
this.saving.poster = false;
});
},
loadInviteCardSetting() {
getDistributionInviteCardSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
this.inviteCardForm = {
...DEFAULT_INVITE_CARD_FORM,
memberInfoVisible: result.memberInfoVisible !== false,
backgroundImage: result.backgroundImage || "",
slogan: result.slogan || DEFAULT_INVITE_CARD_FORM.slogan,
textColor: result.textColor || DEFAULT_INVITE_CARD_FORM.textColor,
};
});
},
submitInviteCard() {
const slogan = (this.inviteCardForm.slogan || "").trim();
if (!slogan) {
this.$Message.warning("请填写推荐语");
return;
}
this.saving.inviteCard = true;
putDistributionInviteCardSetting({
memberInfoVisible: this.inviteCardForm.memberInfoVisible,
backgroundImage: (this.inviteCardForm.backgroundImage || "").trim(),
slogan,
textColor: this.inviteCardForm.textColor || DEFAULT_INVITE_CARD_FORM.textColor,
})
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadInviteCardSetting();
}
})
.finally(() => {
this.saving.inviteCard = false;
});
},
loadFeaturedSetting() {
getDistributionFeaturedGoodsSetting().then((res) => {
if (!res.success || !res.result) {
return;
}
const result = res.result;
this.featuredForm = {
...DEFAULT_FEATURED_FORM,
defaultSort: result.defaultSort || DEFAULT_FEATURED_FORM.defaultSort,
defaultShareText: result.defaultShareText || "",
centerVisible: !!result.centerVisible,
showSales: !!result.showSales,
sourceMode: result.sourceMode || DEFAULT_FEATURED_FORM.sourceMode,
matchRule: result.matchRule || DEFAULT_FEATURED_FORM.matchRule,
displayCount: result.displayCount === 10 ? 10 : 5,
};
this.featuredCustomGoods = Array.isArray(result.customGoods) ? [...result.customGoods] : [];
});
},
loadCustomerSetting() {
getDistributionCustomerSetting().then((res) => {
if (res.success && res.result) {
this.customerForm = { ...this.customerForm, ...res.result };
this.normalizePermanentPaidDays();
} }
}); });
},
normalizePermanentPaidDays() {
const days = Number(this.customerForm.permanentPaidDays);
if (!Number.isFinite(days) || days < 1) {
this.customerForm.permanentPaidDays = 1;
} else {
this.customerForm.permanentPaidDays = Math.floor(days);
}
},
onPermanentPaidDaysChange(value) {
if (value == null || value < 1) {
this.customerForm.permanentPaidDays = 1;
}
},
loadSettlementSetting() {
getDistributionSettlementSetting().then((res) => {
if (res.success && res.result) {
this.settlementForm = { ...this.settlementForm, ...res.result };
}
});
},
openFeaturedGoodsPicker() {
this.featuredPickerVisible = true;
},
loadFeaturedPickerGoods() {
this.featuredPickerLoading = true;
getDistributionGoods({ ...this.featuredPickerSearch })
.then((res) => {
if (res.success) {
this.featuredPickerList = res.result?.records || [];
this.featuredPickerTotal = res.result?.total || 0;
}
})
.finally(() => {
this.featuredPickerLoading = false;
});
},
searchFeaturedPickerGoods() {
this.featuredPickerSearch.pageNumber = 1;
this.loadFeaturedPickerGoods();
},
onFeaturedPickerPageSizeChange() {
this.featuredPickerSearch.pageNumber = 1;
this.loadFeaturedPickerGoods();
},
onFeaturedPickerSelectionChange(selection) {
this.featuredPickerSelection = selection || [];
},
confirmFeaturedPicker() {
if (!this.featuredPickerSelection.length) {
this.$Message.warning("请至少选择一个分销商品");
return;
}
const existingSkuIds = new Set(this.featuredCustomGoods.map((item) => item.skuId));
const merged = [...this.featuredCustomGoods];
this.featuredPickerSelection.forEach((item) => {
if (!item.skuId || existingSkuIds.has(item.skuId)) {
return;
}
if (merged.length >= 10) {
return;
}
existingSkuIds.add(item.skuId);
merged.push({
id: item.id,
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
specs: item.specs,
thumbnail: item.thumbnail,
price: item.price,
commission: item.commission,
});
});
if (merged.length > 10) {
this.$Message.warning("自定义主推商品最多配置 10 个");
this.featuredCustomGoods = merged.slice(0, 10);
} else {
this.featuredCustomGoods = merged;
}
this.featuredPickerVisible = false;
this.featuredPickerSelection = [];
},
removeFeaturedGoods(index) {
this.featuredCustomGoods.splice(index, 1);
},
buildFeaturedPayload() {
return {
defaultSort: this.featuredForm.defaultSort,
defaultShareText: (this.featuredForm.defaultShareText || "").trim(),
centerVisible: this.featuredForm.centerVisible,
showSales: this.featuredForm.showSales,
sourceMode: this.featuredForm.sourceMode,
matchRule: this.featuredForm.matchRule,
displayCount: this.featuredForm.displayCount === 10 ? 10 : 5,
skuIds:
this.featuredForm.sourceMode === "CUSTOM"
? this.featuredCustomGoods.map((item) => item.skuId).filter(Boolean)
: undefined,
};
},
submitFeatured() {
if (this.featuredForm.sourceMode === "CUSTOM" && !this.featuredCustomGoods.length) {
this.$Message.warning("请至少选择一个自定义主推商品");
return;
}
this.saving.featured = true;
putDistributionFeaturedGoodsSetting(this.buildFeaturedPayload())
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadFeaturedSetting();
}
})
.finally(() => {
this.saving.featured = false;
});
},
buildBasePayload() {
return {
isOpen: this.form.isOpen,
distributorName: (this.form.distributorName || "分销员").trim(),
upgradeConditionVisible: this.form.upgradeConditionVisible,
parentDistributorVisible: this.form.parentDistributorVisible,
parentAttributionMode: this.form.parentAttributionMode,
};
},
submitBase() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.saving.base = true;
setSetting("DISTRIBUTION_SETTING", this.buildBasePayload())
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadBaseSetting();
}
})
.finally(() => {
this.saving.base = false;
});
});
},
submitCustomer() {
this.normalizePermanentPaidDays();
if (
this.customerForm.bindMode === "PERMANENT" &&
this.customerForm.permanentBindMode === "PAID_WITHIN_DAYS" &&
this.customerForm.permanentPaidDays < 1
) {
this.$Message.warning("永久锁客支付天数不能低于1天");
return;
}
this.saving.customer = true;
putDistributionCustomerSetting(this.customerForm)
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadCustomerSetting();
}
})
.finally(() => {
this.saving.customer = false;
});
},
submitSettlement() {
this.saving.settlement = true;
putDistributionSettlementSetting(this.settlementForm)
.then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.loadSettlementSetting();
}
})
.finally(() => {
this.saving.settlement = false;
}); });
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss">
.distribution-setting {
.setting-tabs {
overflow: visible;
}
.setting-form {
max-width: 760px;
padding: 16px 8px 8px;
}
.form-tip {
margin-left: 12px;
color: #909399;
font-size: 12px;
}
.option-with-desc {
width: 100%;
}
.radio-option-list {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
.radio-option-item + .radio-option-item {
margin-top: 12px;
}
.radio-option-item .form-desc {
margin-top: 4px;
margin-left: 24px;
}
}
.form-desc {
margin-top: 8px;
color: #909399;
font-size: 12px;
line-height: 1.6;
}
.featured-form {
max-width: 900px;
}
.poster-form {
max-width: 760px;
}
.poster-upload-block {
width: 100%;
}
.poster-preview-wrap {
margin-bottom: 12px;
}
.poster-bg-thumb {
width: 180px;
height: 260px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ebeef5;
}
.setting-section {
margin-bottom: 28px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.setting-section:last-of-type {
border-bottom: none;
margin-bottom: 12px;
}
.section-title {
margin-bottom: 20px;
color: #303133;
font-size: 16px;
font-weight: 600;
line-height: 1.4;
}
.custom-goods-block {
width: 100%;
}
.goods-toolbar {
margin: 8px 0 12px;
}
.link-btn {
color: var(--el-color-primary);
cursor: pointer;
}
.delete-btn {
color: #f56c6c;
}
.featured-goods-table {
width: 100%;
}
.featured-goods-thumb {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 4px;
}
.featured-goods-name {
line-height: 1.5;
word-break: break-all;
}
.featured-picker-search {
margin-bottom: 12px;
}
.featured-picker-pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
}
</style>

View File

@@ -17,10 +17,10 @@
style="width: 240px" style="width: 240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="商品编号" prop="id"> <el-form-item label="商品ID" prop="id">
<el-input <el-input
v-model="searchForm.id" v-model="searchForm.id"
placeholder="请输入商品编号" placeholder="请输入商品ID"
clearable clearable
style="width: 240px" style="width: 240px"
/> />

View File

@@ -33,20 +33,16 @@
<el-card> <el-card>
<el-table v-loading="loading" :data="data" ref="table" class="mt_10" style="width: 100%"> <el-table v-loading="loading" :data="data" ref="table" class="mt_10" style="width: 100%">
<el-table-column label="商品名称" min-width="200" show-overflow-tooltip> <el-table-column label="商品名称" min-width="220" class-name="goods-name-column">
<template #default="{ row }"> <template #default="{ row }">
<div v-if="row" style="margin-top: 5px; height: 80px; display: flex; align-items: flex-start"> <div v-if="row" class="goods-name-cell">
<img <img
v-if="row.original" v-if="row.original"
:src="row.original" :src="row.original"
style="height: 60px; width: 60px; object-fit: cover; margin-top: 3px" class="goods-name-img"
alt="" alt=""
/> />
<div style="margin-left: 13px"> <a class="link-text goods-name-text">{{ row.goodsName }}</a>
<div class="div-zoom">
<a class="link-text">{{ row.goodsName }}</a>
</div>
</div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -201,9 +197,25 @@ export default {
margin: 0 8px; margin: 0 8px;
color: #dcdee2; color: #dcdee2;
} }
.div-zoom { .goods-name-cell {
overflow: hidden; display: flex;
text-overflow: ellipsis; align-items: flex-start;
white-space: nowrap; gap: 12px;
padding: 4px 0;
}
.goods-name-img {
width: 60px;
height: 60px;
object-fit: cover;
flex-shrink: 0;
}
.goods-name-text {
white-space: normal;
word-break: break-word;
line-height: 1.5;
}
:deep(.goods-name-column .cell) {
white-space: normal;
line-height: 1.5;
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="search">
<el-card> <el-card>
<el-form <el-form
ref="searchForm" ref="searchForm"
@@ -9,10 +9,10 @@
class="search-form" class="search-form"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<el-form-item label="会员名称" prop="memberName"> <el-form-item label="客户名称" prop="memberName">
<el-input <el-input
v-model="searchForm.memberName" v-model="searchForm.memberName"
placeholder="请输入会员名称" placeholder="请输入客户名称"
clearable clearable
style="width: 240px" style="width: 240px"
/> />
@@ -34,7 +34,7 @@
<el-card> <el-card>
<el-table ref="table" v-loading="loading" border :data="data" class="mt_10" style="width: 100%"> <el-table ref="table" v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="memberName" label="会员名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="content" label="评论内容" min-width="220" show-overflow-tooltip /> <el-table-column prop="content" label="评论内容" min-width="220" show-overflow-tooltip />
<el-table-column label="是否置顶" width="100" align="center"> <el-table-column label="是否置顶" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
@@ -297,7 +297,7 @@ export default {
this.infoData = {}; this.infoData = {};
}, },
remove(v) { remove(v) {
ElMessageBox.confirm("您确认要删除会员" + v.memberName + "的评论?", "确认删除", { type: "warning" }).then(() => { ElMessageBox.confirm("您确认要删除客户" + v.memberName + "的评论?", "确认删除", { type: "warning" }).then(() => {
return API_Member.delMemberReview(v.id).then(() => { return API_Member.delMemberReview(v.id).then(() => {
ElMessage.success("修改成功"); ElMessage.success("修改成功");
this.init(); this.init();

View File

@@ -14,7 +14,7 @@
<el-badge :value="res.complain">待处理投诉审核</el-badge> <el-badge :value="res.complain">待处理投诉审核</el-badge>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item v-if="res.distributionCash" command="distributionCash"> <el-dropdown-item v-if="res.distributionCash" command="distributionCash">
<el-badge :value="res.distributionCash">待处理分销提现申请</el-badge> <el-badge :value="res.distributionCash">待处理分销提现申请</el-badge>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item v-if="res.goods" command="applyGoods"> <el-dropdown-item v-if="res.goods" command="applyGoods">
<el-badge :value="res.goods">待处理商品审核</el-badge> <el-badge :value="res.goods">待处理商品审核</el-badge>

View File

@@ -9,10 +9,10 @@
class="search-form" class="search-form"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<el-form-item label="会员名称" prop="memberName"> <el-form-item label="客户名称" prop="memberName">
<el-input <el-input
v-model="searchForm.memberName" v-model="searchForm.memberName"
placeholder="请输入会员名称" placeholder="请输入客户名称"
clearable clearable
style="width: 240px" style="width: 240px"
/> />
@@ -52,7 +52,7 @@
class="mt_10" class="mt_10"
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="memberName" label="会员名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="rechargeSn" label="订单号" min-width="180" show-overflow-tooltip /> <el-table-column prop="rechargeSn" label="订单号" min-width="180" show-overflow-tooltip />
<el-table-column label="充值金额" width="160" sortable> <el-table-column label="充值金额" width="160" sortable>
<template #default="{ row }"> <template #default="{ row }">

View File

@@ -9,10 +9,10 @@
class="search-form" class="search-form"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<el-form-item label="会员名称" prop="memberName"> <el-form-item label="客户名称" prop="memberName">
<el-input <el-input
v-model="searchForm.memberName" v-model="searchForm.memberName"
placeholder="请输入会员名称" placeholder="请输入客户名称"
clearable clearable
style="width: 240px" style="width: 240px"
/> />
@@ -44,7 +44,7 @@
class="mt_10" class="mt_10"
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="memberName" label="会员名称" min-width="100" /> <el-table-column prop="memberName" label="客户名称" min-width="100" />
<el-table-column label="变动金额" width="150"> <el-table-column label="变动金额" width="150">
<template #default="{ row }"> <template #default="{ row }">
<template v-if="row"> <template v-if="row">

View File

@@ -9,10 +9,10 @@
class="search-form" class="search-form"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<el-form-item label="会员名称" prop="memberName"> <el-form-item label="客户名称" prop="memberName">
<el-input <el-input
v-model="searchForm.memberName" v-model="searchForm.memberName"
placeholder="请输入会员名称" placeholder="请输入客户名称"
clearable clearable
style="width: 240px" style="width: 240px"
/> />
@@ -57,7 +57,7 @@
@selection-change="changeSelect" @selection-change="changeSelect"
> >
<el-table-column prop="sn" label="申请编号" min-width="160" show-overflow-tooltip /> <el-table-column prop="sn" label="申请编号" min-width="160" show-overflow-tooltip />
<el-table-column prop="memberName" label="户名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="memberName" label="户名称" min-width="120" show-overflow-tooltip />
<el-table-column label="申请金额" width="120"> <el-table-column label="申请金额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<priceColorScheme v-if="row" :value="row.applyMoney" :color="$mainColor" /> <priceColorScheme v-if="row" :value="row.applyMoney" :color="$mainColor" />

View File

@@ -9,11 +9,11 @@
class="search-form" class="search-form"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<el-form-item label="会员ID" prop="id"> <el-form-item label="客户ID" prop="id">
<el-input v-model="searchForm.id" placeholder="请输入会员ID" clearable style="width: 240px" /> <el-input v-model="searchForm.id" placeholder="请输入客户ID" clearable style="width: 240px" />
</el-form-item> </el-form-item>
<el-form-item label="会员名称" prop="username"> <el-form-item label="客户名称" prop="username">
<el-input v-model="searchForm.username" placeholder="请输入会员名称" clearable style="width: 240px" /> <el-input v-model="searchForm.username" placeholder="请输入客户名称" clearable style="width: 240px" />
</el-form-item> </el-form-item>
<el-form-item label="会员昵称" prop="nickName"> <el-form-item label="会员昵称" prop="nickName">
<el-input v-model="searchForm.nickName" placeholder="请输入会员昵称" clearable style="width: 240px" /> <el-input v-model="searchForm.nickName" placeholder="请输入会员昵称" clearable style="width: 240px" />
@@ -75,7 +75,7 @@
align="center" align="center"
:reserve-selection="checkboxSelect" :reserve-selection="checkboxSelect"
/> />
<el-table-column prop="id" label="会员ID" min-width="120" show-overflow-tooltip /> <el-table-column prop="id" label="客户ID" min-width="120" show-overflow-tooltip />
<el-table-column label="头像" min-width="80" align="center"> <el-table-column label="头像" min-width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<img <img
@@ -86,7 +86,7 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="username" label="会员名称" min-width="150" show-overflow-tooltip /> <el-table-column prop="username" label="客户名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="nickName" label="会员昵称" min-width="120" show-overflow-tooltip /> <el-table-column prop="nickName" label="会员昵称" min-width="120" show-overflow-tooltip />
<el-table-column label="联系方式" min-width="130"> <el-table-column label="联系方式" min-width="130">
<template #default="{ row }"> <template #default="{ row }">
@@ -182,11 +182,11 @@
autocomplete="off" autocomplete="off"
/> />
</el-form-item> </el-form-item>
<el-form-item label="会员名称" prop="username" style="width: 90%"> <el-form-item label="客户名称" prop="username" style="width: 90%">
<el-input <el-input
v-model="addMemberForm.username" v-model="addMemberForm.username"
maxlength="15" maxlength="15"
placeholder="请输入会员名称" placeholder="请输入客户名称"
autocomplete="off" autocomplete="off"
name="member-username" name="member-username"
/> />
@@ -421,7 +421,7 @@ export default {
{ required: true, message: "请输入手机号码" }, { required: true, message: "请输入手机号码" },
{ pattern: RegExp.mobile, message: "请输入正确的手机号" }, { pattern: RegExp.mobile, message: "请输入正确的手机号" },
], ],
username: [{ required: true, message: "请输入会员名称" }], username: [{ required: true, message: "请输入客户名称" }],
password: [{ required: true, message: "请输入密码" }], password: [{ required: true, message: "请输入密码" }],
}, },
ruleValidate: {}, ruleValidate: {},

View File

@@ -9,10 +9,18 @@
class="search-form" class="search-form"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<el-form-item label="会员名称" prop="username"> <el-form-item label="客户ID" prop="id">
<el-input
v-model="searchForm.id"
placeholder="请输入客户ID"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item label="客户名称" prop="username">
<el-input <el-input
v-model="searchForm.username" v-model="searchForm.username"
placeholder="请输入会员名称" placeholder="请输入客户名称"
clearable clearable
style="width: 240px" style="width: 240px"
/> />
@@ -40,7 +48,8 @@
class="mt_10" class="mt_10"
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="username" label="会员名称" min-width="150" align="left" show-overflow-tooltip /> <el-table-column prop="id" label="客户ID" min-width="120" show-overflow-tooltip />
<el-table-column prop="username" label="客户名称" min-width="150" align="left" show-overflow-tooltip />
<el-table-column prop="nickName" label="昵称" min-width="120" align="left" show-overflow-tooltip /> <el-table-column prop="nickName" label="昵称" min-width="120" align="left" show-overflow-tooltip />
<el-table-column label="联系方式" min-width="130"> <el-table-column label="联系方式" min-width="130">
<template #default="{ row }"> <template #default="{ row }">
@@ -98,7 +107,7 @@
>修改</el-button> >修改</el-button>
<input type="file" style="display: none" id="file" /> <input type="file" style="display: none" id="file" />
</el-form-item> </el-form-item>
<el-form-item label="会员名称" prop="name"> <el-form-item label="客户名称" prop="name">
<el-input v-model="formValidate.username" style="width: 200px" disabled /> <el-input v-model="formValidate.username" style="width: 200px" disabled />
</el-form-item> </el-form-item>
<el-form-item label="用户昵称" prop="name"> <el-form-item label="用户昵称" prop="name">
@@ -168,6 +177,7 @@ export default {
pageNumber: 1, pageNumber: 1,
pageSize: 20, pageSize: 20,
order: "desc", order: "desc",
id: "",
username: "", username: "",
mobile: "", mobile: "",
disabled: "CLOSE", disabled: "CLOSE",

View File

@@ -7,6 +7,8 @@
</el-affix> </el-affix>
<el-card class="card"> <el-card class="card">
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="概览" name="overview">
<h4>分销概况</h4> <h4>分销概况</h4>
<div class="metric-grid"> <div class="metric-grid">
<div class="metric-item" v-for="item in metricDefs" :key="item.key"> <div class="metric-item" v-for="item in metricDefs" :key="item.key">
@@ -26,10 +28,38 @@
</div> </div>
</div> </div>
</div> </div>
</el-card>
<el-card class="card"> <h4 class="section-title">今日数据</h4>
<h4>分销排行</h4> <div class="metric-grid">
<div class="metric-item" v-for="item in todayMetricDefs" :key="item.key">
<div class="metric-label">{{ item.label }}</div>
<div class="metric-value">
<template v-if="item.isMoney">
<priceColorScheme :value="overviewV2[item.key]" :color="$mainColor" />
</template>
<template v-else>
{{ formatNumber(overviewV2[item.key]) }}
</template>
</div>
</div>
</div>
<h4 class="section-title">累计数据</h4>
<div class="metric-grid">
<div class="metric-item" v-for="item in totalMetricDefs" :key="item.key">
<div class="metric-label">{{ item.label }}</div>
<div class="metric-value">
<template v-if="item.isMoney">
<priceColorScheme :value="overviewV2[item.key]" :color="$mainColor" />
</template>
<template v-else>
{{ formatNumber(overviewV2[item.key]) }}
</template>
</div>
</div>
</div>
<h4 class="section-title">分销排行</h4>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :xs="24" :md="12"> <el-col :xs="24" :md="12">
<div class="rank-title">TOP分销员</div> <div class="rank-title">TOP分销员</div>
@@ -58,12 +88,88 @@
</el-table> </el-table>
</el-col> </el-col>
</el-row> </el-row>
</el-tab-pane>
<el-tab-pane label="行为数据" name="behavior">
<el-table v-loading="behaviorLoading" :data="behaviorData" stripe border style="width: 100%">
<el-table-column prop="nickname" label="昵称" min-width="120" show-overflow-tooltip />
<el-table-column prop="mobile" label="手机号" min-width="120" />
<el-table-column prop="gradeName" label="等级" width="100" />
<el-table-column label="新增销售额" min-width="120">
<template #default="{ row }">
<priceColorScheme v-if="row" :value="row.newSalesAmount" :color="$mainColor" />
</template>
</el-table-column>
<el-table-column prop="newCustomerCount" label="新增客户数" width="110" />
<el-table-column prop="paidCustomerCount" label="支付客户数" width="110" />
<el-table-column prop="newInviteCount" label="新增邀请数" width="110" />
<el-table-column prop="orderInviteCount" label="开单邀请数" width="110" />
<el-table-column prop="shareCount" label="商品分享数" width="110" />
<el-table-column prop="sharedGoodsCount" label="分享商品数" width="110" />
<el-table-column prop="shareVisitorCount" label="分享引流" width="100" />
<el-table-column prop="shareVisitCount" label="分享浏览量" width="110" />
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="behaviorPage.pageNumber"
v-model:page-size="behaviorPage.pageSize"
:page-sizes="[20, 50, 100]"
:total="behaviorTotal"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="loadBehavior"
@size-change="onBehaviorSizeChange"
/>
</div>
</el-tab-pane>
<el-tab-pane label="商品数据" name="goods">
<el-table v-loading="goodsLoading" :data="goodsData" stripe border style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="shareDistributorCount" label="分享人数" width="100" />
<el-table-column prop="shareCount" label="分享次数" width="100" />
<el-table-column prop="visitVisitorCount" label="访问人数" width="100" />
<el-table-column label="成交金额" min-width="120">
<template #default="{ row }">
<priceColorScheme v-if="row" :value="row.dealAmount" :color="$mainColor" />
</template>
</el-table-column>
<el-table-column label="支出佣金" min-width="120">
<template #default="{ row }">
<priceColorScheme v-if="row" :value="row.commissionAmount" :color="$mainColor" />
</template>
</el-table-column>
<el-table-column label="访问转化率" width="110">
<template #default="{ row }">
<span v-if="row">{{ formatRate(row.visitConversionRate) }}</span>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="goodsPage.pageNumber"
v-model:page-size="goodsPage.pageSize"
:page-sizes="[20, 50, 100]"
:total="goodsTotal"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="loadGoods"
@size-change="onGoodsSizeChange"
/>
</div>
</el-tab-pane>
</el-tabs>
</el-card> </el-card>
</div> </div>
</template> </template>
<script> <script>
import * as API_Statistics from "@/api/statistics"; import * as API_Statistics from "@/api/statistics";
import {
getDistributionOverviewV2,
getDistributionBehaviorV2,
getDistributionGoodsV2,
} from "@/api/distribution";
import affixTime from "@/components/affix-time"; import affixTime from "@/components/affix-time";
import { QuestionFilled } from "@element-plus/icons-vue"; import { QuestionFilled } from "@element-plus/icons-vue";
@@ -71,6 +177,7 @@ export default {
components: { affixTime, QuestionFilled }, components: { affixTime, QuestionFilled },
data() { data() {
return { return {
activeTab: "overview",
params: { params: {
searchType: "LAST_SEVEN", searchType: "LAST_SEVEN",
year: "", year: "",
@@ -78,43 +185,52 @@ export default {
storeId: "", storeId: "",
}, },
overview: {}, overview: {},
overviewV2: {},
distributorRank: [], distributorRank: [],
goodsRank: [], goodsRank: [],
behaviorData: [],
behaviorTotal: 0,
behaviorLoading: false,
behaviorPage: { pageNumber: 1, pageSize: 20 },
goodsData: [],
goodsTotal: 0,
goodsLoading: false,
goodsPage: { pageNumber: 1, pageSize: 20 },
metricDefs: [ metricDefs: [
{ key: "distributorNum", label: "分销员数", tip: "已通过审核的分销员数量" }, { key: "distributorNum", label: "分销员数", tip: "已通过审核的分销员数量" },
{ key: "applyNum", label: "待审核分销员", tip: "申请中待审核的分销员数量" }, { key: "applyNum", label: "待审核分销员", tip: "申请中待审核的分销员数量" },
{ key: "distributionOrderNum", label: "分销订单数", tip: "周期内分销订单笔数" }, { key: "distributionOrderNum", label: "分销订单数", tip: "周期内分销订单笔数" },
{ { key: "distributionOrderAmount", label: "分销订单金额", isMoney: true, tip: "周期内分销订单佣金合计" },
key: "distributionOrderAmount", { key: "settledCommission", label: "已结算佣金", isMoney: true, tip: "周期内已完成结算的佣金" },
label: "分销订单金额", { key: "pendingCommission", label: "待结算佣金", isMoney: true, tip: "周期内待结算的佣金" },
isMoney: true, { key: "cashAmount", label: "提现金额", isMoney: true, tip: "周期内分销员提现金额" },
tip: "周期内分销订单佣金合计", ],
}, todayMetricDefs: [
{ { key: "todayNewDistributorCount", label: "今日新增分销员" },
key: "settledCommission", { key: "todayNewSubDistributorCount", label: "今日新增下级分销员" },
label: "已结算佣金", { key: "todaySalesAmount", label: "今日分销员销售额", isMoney: true },
isMoney: true, { key: "todayPaidCustomerCount", label: "今日成交客户数" },
tip: "周期内已完成结算的佣金", { key: "todayCommissionAmount", label: "今日支出佣金", isMoney: true },
}, ],
{ totalMetricDefs: [
key: "pendingCommission", { key: "totalDistributorCount", label: "累计分销员数" },
label: "待结算佣金", { key: "totalSubDistributorCount", label: "累计下级分销员数" },
isMoney: true, { key: "totalSalesAmount", label: "累计分销员销售额", isMoney: true },
tip: "周期内待结算的佣金", { key: "totalPaidCustomerCount", label: "累计成交客户数" },
}, { key: "totalShareVisitorCount", label: "商品分享引流" },
{ { key: "totalCommissionAmount", label: "累计支出佣金", isMoney: true },
key: "cashAmount",
label: "提现金额",
isMoney: true,
tip: "周期内分销员提现金额",
},
], ],
}; };
}, },
watch: { watch: {
params: { params: {
handler() { handler() {
this.loadData(); this.loadOverviewData();
if (this.activeTab === "behavior") {
this.loadBehavior();
} else if (this.activeTab === "goods") {
this.loadGoods();
}
}, },
deep: true, deep: true,
immediate: true, immediate: true,
@@ -131,17 +247,36 @@ export default {
endTime: item.endTime || "", endTime: item.endTime || "",
}; };
}, },
onTabChange(name) {
if (name === "behavior") {
this.loadBehavior();
} else if (name === "goods") {
this.loadGoods();
}
},
buildQueryParams() {
return {
startTime: this.params.startTime || "",
endTime: this.params.endTime || "",
};
},
formatNumber(val) { formatNumber(val) {
if (val == null || val === "") return 0; if (val == null || val === "") return 0;
const num = Number(val); const num = Number(val);
return Number.isNaN(num) ? 0 : Math.round(num); return Number.isNaN(num) ? 0 : Math.round(num);
}, },
async loadData() { formatRate(val) {
if (val == null || val === "") return "-";
return `${(Number(val) * 100).toFixed(2)}%`;
},
async loadOverviewData() {
const query = { ...this.params }; const query = { ...this.params };
const [overviewRes, distributorRes, goodsRes] = await Promise.all([ const v2Query = this.buildQueryParams();
const [overviewRes, distributorRes, goodsRes, overviewV2Res] = await Promise.all([
API_Statistics.getDistributionOverview(query), API_Statistics.getDistributionOverview(query),
API_Statistics.getDistributionRankDistributor(query), API_Statistics.getDistributionRankDistributor(query),
API_Statistics.getDistributionRankGoods(query), API_Statistics.getDistributionRankGoods(query),
getDistributionOverviewV2(v2Query),
]); ]);
if (overviewRes && overviewRes.success) { if (overviewRes && overviewRes.success) {
this.overview = overviewRes.result || {}; this.overview = overviewRes.result || {};
@@ -152,6 +287,45 @@ export default {
if (goodsRes && goodsRes.success) { if (goodsRes && goodsRes.success) {
this.goodsRank = goodsRes.result || []; this.goodsRank = goodsRes.result || [];
} }
if (overviewV2Res && overviewV2Res.success) {
this.overviewV2 = overviewV2Res.result || {};
}
},
loadBehavior() {
this.behaviorLoading = true;
getDistributionBehaviorV2({
...this.buildQueryParams(),
pageNumber: this.behaviorPage.pageNumber,
pageSize: this.behaviorPage.pageSize,
}).then((res) => {
this.behaviorLoading = false;
if (res.success) {
this.behaviorData = res.result.records || [];
this.behaviorTotal = res.result.total || 0;
}
});
},
onBehaviorSizeChange() {
this.behaviorPage.pageNumber = 1;
this.loadBehavior();
},
loadGoods() {
this.goodsLoading = true;
getDistributionGoodsV2({
...this.buildQueryParams(),
pageNumber: this.goodsPage.pageNumber,
pageSize: this.goodsPage.pageSize,
}).then((res) => {
this.goodsLoading = false;
if (res.success) {
this.goodsData = res.result.records || [];
this.goodsTotal = res.result.total || 0;
}
});
},
onGoodsSizeChange() {
this.goodsPage.pageNumber = 1;
this.loadGoods();
}, },
}, },
}; };
@@ -167,6 +341,9 @@ export default {
h4 { h4 {
margin: 0 0 20px 0; margin: 0 0 20px 0;
} }
.section-title {
margin-top: 24px;
}
.metric-grid { .metric-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);

View File

@@ -3,7 +3,7 @@ var BASE = {
* @description api请求基础路径 * @description api请求基础路径
*/ */
API_DEV: { API_DEV: {
seller: "https://admin-api.pickmall.cn", seller: "http://127.0.0.1:8887",
}, },
API_PROD: { API_PROD: {
seller: "https://admin-api.pickmall.cn", seller: "https://admin-api.pickmall.cn",

View File

@@ -1,6 +1,6 @@
// 统一请求路径前缀在libs/axios.js中修改 // 统一请求路径前缀在libs/axios.js中修改
import {getRequest, postRequest, putRequest, deleteRequest, importRequest, getRequestWithNoToken} from '@/libs/axios'; import {getRequest, postRequest, postRequestWithNoForm, putRequest, deleteRequest, importRequest, getRequestWithNoToken} from '@/libs/axios';
//获取分销商品列表 //获取分销商品列表
@@ -18,22 +18,32 @@ export const distributionGoodsCheck = (id,param) => {
return putRequest(`/distribution/goods/checked/${id}`,param) return putRequest(`/distribution/goods/checked/${id}`,param)
} }
// 批量选择分销商品
export const distributionGoodsBatchCheck = (items) => {
return postRequestWithNoForm('/distribution/goods/checked/batch', { items })
}
//查询分销商 // 修改分销商品佣金
export const updateDistributionGoodsCommission = (id, params) => {
return putRequest(`/distribution/goods/${id}/commission`, params)
}
//查询分销员
export const getDistributionListData = (params) => { export const getDistributionListData = (params) => {
return getRequest('/distribution/getByPage', params) return getRequest('/distribution/getByPage', params)
} }
//审核分销 //审核分销
export const auditDistribution = (id, params) => { export const auditDistribution = (id, params) => {
return putRequest(`/distribution/audit/${id}`, params) return putRequest(`/distribution/audit/${id}`, params)
} }
// 清退分销 // 清退分销
export const retreatDistribution = (id, params) => { export const retreatDistribution = (id, params) => {
return putRequest(`/distribution/retreat/${id}`, params) return putRequest(`/distribution/retreat/${id}`, params)
} }
// 恢复分销 // 恢复分销
export const resumeDistribution = (id, params) => { export const resumeDistribution = (id, params) => {
return putRequest(`/distribution/resume/${id}`, params) return putRequest(`/distribution/resume/${id}`, params)
} }

View File

@@ -1,4 +1,4 @@
// 分销状态列表 // 分销状态列表
export const distributionStatusList= [ export const distributionStatusList= [
{ {
value:'APPLY', value:'APPLY',

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="search">
<el-card> <el-card>
<el-form <el-form
ref="searchForm" ref="searchForm"
@@ -17,13 +17,59 @@
style="width: 240px" style="width: 240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="商品品牌" prop="brandId">
<el-select
v-model="searchForm.brandId"
placeholder="请选择商品品牌"
clearable
filterable
style="width: 240px"
>
<el-option
v-for="item in brandList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="店铺分类" prop="storeCategoryPath">
<el-cascader
v-model="searchForm.storeCategoryPath"
:options="shopCategoryList"
:props="shopCategoryProps"
clearable
filterable
placeholder="请选择店铺分类"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="商品类型" prop="goodsType">
<el-select
v-model="searchForm.goodsType"
placeholder="请选择商品类型"
clearable
style="width: 240px"
>
<el-option label="实物商品" value="PHYSICAL_GOODS" />
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
<el-option label="电子卡券" value="E_COUPON" />
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button> <el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<el-card> <el-card class="list-card">
<el-tabs v-model="currentAuthStatus" class="auth-tabs" @tab-change="onAuthStatusChange">
<el-tab-pane label="全部" name="ALL" />
<el-tab-pane label="待审核" name="TOBEAUDITED" />
<el-tab-pane label="已审核" name="PASS" />
<el-tab-pane label="审核未通过" name="REFUSE" />
</el-tabs>
<div class="operation"> <div class="operation">
<el-button type="primary" @click="add">添加</el-button> <el-button type="primary" @click="add">添加</el-button>
</div> </div>
@@ -36,43 +82,33 @@
class="mt_10" class="mt_10"
style="width: 100%" style="width: 100%"
> >
<el-table-column label="商品名称" min-width="250"> <el-table-column prop="skuId" label="ID" min-width="180" show-overflow-tooltip />
<el-table-column label="商品图片" width="90" align="center">
<template #default="{ row }"> <template #default="{ row }">
<div v-if="row" class="goods-msg"> <img v-if="row?.thumbnail" :src="row.thumbnail" class="goods-thumb" alt="" />
<img :src="row.thumbnail" width="60" height="60" alt="" /> <span v-else>-</span>
<div>
<div class="div-zoom">
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
</div>
<el-popover trigger="hover" title="扫码在手机中查看" placement="top" width="180">
<template #reference>
<img
src="../../assets/qrcode.svg"
class="hover-pointer"
width="20"
height="20"
alt="qrcode"
/>
</template>
<vue-qr
:text="wapLinkTo(row.goodsId, row.skuId)"
:margin="0"
color-dark="#000"
color-light="#fff"
:size="150"
/>
</el-popover>
</div>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品价格" min-width="110"> <el-table-column label="商品名称" min-width="180" class-name="goods-name-column">
<template #default="{ row }">
<span v-if="row" class="goods-name-text">{{ row.goodsName }}</span>
</template>
</el-table-column>
<el-table-column label="规格" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
{{ row?.specs || "-" }}
</template>
</el-table-column>
<el-table-column label="商品类型" min-width="110">
<template #default="{ row }">
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
</template>
</el-table-column>
<el-table-column label="商品金额" min-width="110">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, "") }}</span> <span v-if="row" :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, "") }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="quantity" label="库存" min-width="80" />
<el-table-column prop="createTime" label="添加时间" min-width="160" />
<el-table-column label="佣金金额" min-width="110"> <el-table-column label="佣金金额" min-width="110">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }"> <span v-if="row" :style="{ color: $mainColor }">
@@ -80,9 +116,20 @@
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-width="100" align="center" fixed="right"> <el-table-column prop="quantity" label="库存" min-width="80" align="center" />
<el-table-column label="审核状态" min-width="110" align="center">
<template #default="{ row }"> <template #default="{ row }">
<a v-if="row" class="link-text" @click="remove(row)">删除</a> <span v-if="row">{{ authStatusText(row.authStatus) }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="添加时间" min-width="160" />
<el-table-column label="操作" min-width="120" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row">
<a class="link-text" @click="edit(row)">编辑</a>
<span class="op-split">|</span>
<a class="link-text" @click="remove(row)">删除</a>
</template>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -91,7 +138,7 @@
<el-pagination <el-pagination
v-model:current-page="searchForm.pageNumber" v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize" v-model:page-size="searchForm.pageSize"
:page-sizes="[10, 20, 50]" :page-sizes="[20, 50, 100]"
:total="total" :total="total"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
size="small" size="small"
@@ -106,20 +153,88 @@
<el-dialog <el-dialog
v-model="modalVisible" v-model="modalVisible"
:title="modalTitle" :title="modalTitle"
width="500px" width="900px"
:close-on-click-modal="false" :close-on-click-modal="false"
append-to-body append-to-body
class="add-distribution-goods-dialog"
> >
<el-form ref="form" :model="form" label-width="100px" :rules="formValidate"> <el-table :data="selectedGoodsList" border max-height="420">
<el-form-item label="分销佣金" prop="commission"> <el-table-column label="商品图片" width="90" align="center">
<el-input v-model="form.commission" clearable placeholder="请输入分销佣金" /> <template #default="{ row }">
</el-form-item> <img v-if="row?.thumbnail" :src="row.thumbnail" class="goods-thumb" alt="" />
</el-form> <span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="商品名称" min-width="160" class-name="goods-name-column">
<template #default="{ row }">
<span class="goods-name-text">{{ row.goodsName }}</span>
</template>
</el-table-column>
<el-table-column label="规格" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.specs || "-" }}
</template>
</el-table-column>
<el-table-column label="商品价格" width="110" align="right">
<template #default="{ row }">
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, "¥") }}</span>
</template>
</el-table-column>
<el-table-column label="分销佣金" width="160" align="center">
<template #default="{ row, $index }">
<el-input
v-model="row.commission"
clearable
placeholder="请输入佣金"
:class="{ 'commission-input-error': row.commissionError }"
@blur="validateRowCommission(row, $index)"
@input="row.commissionError = false"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<a
v-if="selectedGoodsList.length > 1"
class="link-text"
@click="removeSelectedGoods($index)"
>移除</a>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<template #footer> <template #footer>
<el-button @click="modalVisible = false">取消</el-button> <el-button @click="modalVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button> <el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog
v-model="editVisible"
title="编辑分销商品"
width="480px"
:close-on-click-modal="false"
append-to-body
>
<el-form ref="editFormRef" :model="editForm" label-width="100px">
<el-form-item label="商品名称">
<span>{{ editForm.goodsName }}</span>
</el-form-item>
<el-form-item label="分销佣金" prop="commission">
<el-input
v-model="editForm.commission"
clearable
placeholder="请输入分销佣金"
:class="{ 'commission-input-error': editForm.commissionError }"
@input="editForm.commissionError = false"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" :loading="editLoading" @click="handleEditSubmit">保存</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -127,75 +242,166 @@
import { import {
getDistributionGoods, getDistributionGoods,
distributionGoodsCancel, distributionGoodsCancel,
distributionGoodsCheck, distributionGoodsBatchCheck,
updateDistributionGoodsCommission,
} from "@/api/distribution"; } from "@/api/distribution";
import { getBrandListData, getShopGoodsLabelListSeller } from "@/api/goods";
import liliDialog from "@/views/lili-dialog"; import liliDialog from "@/views/lili-dialog";
import vueQr from "vue-qr";
export default { export default {
name: "distributionGoods", name: "distributionGoods",
components: { liliDialog, vueQr }, components: { liliDialog },
data() { data() {
return { return {
modalVisible: false, modalVisible: false,
modalTitle: "添加分销商品", modalTitle: "添加分销商品",
editVisible: false,
editLoading: false,
submitLoading: false, submitLoading: false,
loading: true, loading: true,
currentAuthStatus: "ALL",
searchForm: { searchForm: {
pageNumber: 1, pageNumber: 1,
pageSize: 10, pageSize: 20,
sort: "createTime", sort: "createTime",
order: "desc", order: "desc",
goodsName: "",
brandId: "",
storeCategoryPath: "",
goodsType: "",
authStatus: "",
}, },
form: { brandList: [],
commission: 1, shopCategoryList: [],
shopCategoryProps: {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
}, },
skuId: 0, selectedGoodsList: [],
formValidate: { editForm: {
commission: [ id: "",
{ required: true, message: "请输入大于1小于9999的合法佣金金额", trigger: "blur" }, goodsName: "",
{ commission: "",
pattern: /^[1-9]\d{0,3}(\.\d{1,2})?$/, commissionError: false,
message: "请输入大于1小于9999的合法佣金金额",
trigger: "blur",
},
],
}, },
commissionPattern: /^[1-9]\d{0,3}(\.\d{1,2})?$/,
data: [], data: [],
total: 0, total: 0,
}; };
}, },
methods: { methods: {
init() { init() {
this.loadFilterOptions();
this.getDataList();
},
loadFilterOptions() {
getBrandListData().then((res) => {
this.brandList = Array.isArray(res) ? res : res?.result || [];
});
getShopGoodsLabelListSeller().then((res) => {
if (res?.success) {
this.shopCategoryList = this.formatShopCategory(res.result || []);
}
});
},
formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length
? this.formatShopCategory(item.children)
: undefined,
}));
},
goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
},
authStatusText(value) {
if (value === "TOBEAUDITED") return "待审核";
if (value === "REFUSE") return "审核未通过";
return "已审核";
},
onAuthStatusChange(name) {
this.searchForm.authStatus = name === "ALL" ? "" : name;
this.searchForm.pageNumber = 1;
this.getDataList(); this.getDataList();
}, },
add() { add() {
this.$refs.liliDialog.goodsData = []; this.$refs.liliDialog.goodsData = [];
this.$refs.liliDialog.open("goods", "single"); this.$refs.liliDialog.open("goods");
}, },
selectedGoodsData(selected) { selectedGoodsData(selected) {
if (!selected?.length) return; if (!selected?.length) return;
this.selectedGoodsList = selected.map((item) => ({
skuId: item.id,
goodsId: item.goodsId,
thumbnail: item.thumbnail,
goodsName: item.goodsName,
specs: item.simpleSpecs || "-",
price: item.price,
commission: "1",
commissionError: false,
}));
this.modalVisible = true; this.modalVisible = true;
this.form.commission = 1; this.modalTitle = `添加分销商品(已选${this.selectedGoodsList.length}项)`;
this.modalTitle = "添加分销商品"; },
this.skuId = selected[0].id; validateRowCommission(row) {
const valid = this.commissionPattern.test(String(row.commission || "").trim());
row.commissionError = !valid;
return valid;
},
validateAllCommission() {
let valid = true;
this.selectedGoodsList.forEach((row) => {
if (!this.validateRowCommission(row)) {
valid = false;
}
});
return valid;
},
removeSelectedGoods(index) {
this.selectedGoodsList.splice(index, 1);
if (!this.selectedGoodsList.length) {
this.modalVisible = false;
return;
}
this.modalTitle = `添加分销商品(已选${this.selectedGoodsList.length}项)`;
}, },
handleSubmit() { handleSubmit() {
this.$refs.form.validate((valid) => { if (!this.selectedGoodsList.length) return;
if (!valid) return; if (!this.validateAllCommission()) {
this.$Message.error("请为所有商品填写大于1且小于9999的合法佣金金额");
return;
}
this.submitLoading = true; this.submitLoading = true;
distributionGoodsCheck(this.skuId, this.form) const items = this.selectedGoodsList.map((item) => ({
skuId: item.skuId,
commission: Number(item.commission),
}));
distributionGoodsBatchCheck(items)
.then((res) => { .then((res) => {
if (res?.success || res?.message === "success") { if (res?.success || res?.message === "success") {
this.$Message.success("添加成功"); const addedCount = Array.isArray(res?.result) ? res.result.length : 0;
if (addedCount > 0) {
this.$Message.success(`成功添加${addedCount}个分销商品`);
} else {
this.$Message.info("所选商品均已是分销商品,已跳过");
}
} }
this.modalVisible = false; this.modalVisible = false;
this.selectedGoodsList = [];
this.getDataList(); this.getDataList();
}) })
.finally(() => { .finally(() => {
this.submitLoading = false; this.submitLoading = false;
}); });
});
}, },
changePage(v) { changePage(v) {
this.searchForm.pageNumber = v; this.searchForm.pageNumber = v;
@@ -243,6 +449,37 @@ export default {
}, },
}); });
}, },
edit(row) {
this.editForm = {
id: row.id,
goodsName: row.goodsName,
commission: row.commission != null ? String(row.commission) : "1",
commissionError: false,
};
this.editVisible = true;
},
handleEditSubmit() {
const valid = this.commissionPattern.test(String(this.editForm.commission || "").trim());
this.editForm.commissionError = !valid;
if (!valid) {
this.$Message.error("请输入大于1且小于9999的合法佣金金额");
return;
}
this.editLoading = true;
updateDistributionGoodsCommission(this.editForm.id, {
commission: Number(this.editForm.commission),
})
.then((res) => {
if (res?.success || res?.message === "success") {
this.$Message.success("修改成功");
this.editVisible = false;
this.getDataList();
}
})
.finally(() => {
this.editLoading = false;
});
},
}, },
mounted() { mounted() {
this.init(); this.init();
@@ -255,15 +492,11 @@ export default {
margin: 10px 0; margin: 10px 0;
} }
.goods-msg { .goods-thumb {
display: flex; width: 48px;
align-items: flex-start; height: 48px;
gap: 13px;
padding: 5px 0;
img {
object-fit: contain; object-fit: contain;
} vertical-align: middle;
} }
.link-text { .link-text {
@@ -272,19 +505,46 @@ export default {
text-decoration: none; text-decoration: none;
} }
.div-zoom { .op-split {
overflow: hidden; margin: 0 6px;
text-overflow: ellipsis; color: #dcdfe6;
white-space: nowrap;
margin-bottom: 4px;
} }
.hover-pointer { .goods-name-text {
cursor: pointer; white-space: normal;
vertical-align: middle; word-break: break-word;
line-height: 1.5;
}
:deep(.goods-name-column .cell) {
white-space: normal;
word-break: break-word;
line-height: 1.5;
} }
.mt_10 { .mt_10 {
margin-top: 10px; margin-top: 10px;
} }
.list-card {
margin-top: 16px;
}
.auth-tabs {
margin-bottom: 4px;
}
.commission-input-error {
:deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px #f56c6c inset;
}
}
.add-distribution-goods-dialog {
:deep(.goods-name-column .cell) {
white-space: normal;
word-break: break-word;
line-height: 1.5;
}
}
</style> </style>

View File

@@ -17,10 +17,10 @@
style="width: 240px" style="width: 240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="分销" prop="distributionName"> <el-form-item label="分销" prop="distributionName">
<el-input <el-input
v-model="searchForm.distributionName" v-model="searchForm.distributionName"
placeholder="请输入分销名称" placeholder="请输入分销名称"
clearable clearable
style="width: 240px" style="width: 240px"
/> />
@@ -89,7 +89,7 @@
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="distributionName" label="分销" min-width="100" show-overflow-tooltip /> <el-table-column prop="distributionName" label="分销" min-width="100" show-overflow-tooltip />
<el-table-column label="状态" min-width="90"> <el-table-column label="状态" min-width="90">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row" :type="filterStatusTagType(row.distributionOrderStatus)"> <el-tag v-if="row" :type="filterStatusTagType(row.distributionOrderStatus)">
@@ -100,7 +100,7 @@
<el-table-column label="佣金金额" min-width="100"> <el-table-column label="佣金金额" min-width="100">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }"> <span v-if="row" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.rebate, "") }}</span> {{ $filters.unitPrice(row.directCommission ?? row.rebate, "") }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160" fixed="right" /> <el-table-column prop="createTime" label="创建时间" min-width="160" fixed="right" />

View File

@@ -111,18 +111,22 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品名称" min-width="200" show-overflow-tooltip> <el-table-column label="商品名称" min-width="220" class-name="goods-name-column">
<template #default="{ row }"> <template #default="{ row }">
<a class="link-text" @click="linkTo(row.id, row.skuId)">{{ row.goodsName }}</a> <a
v-if="row"
class="link-text goods-name-text"
@click="linkTo(row.id, row.skuId)"
>{{ row.goodsName }}</a>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="价格" width="200"> <el-table-column label="价格" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span> <span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="buyCount" label="销量" width="150" /> <el-table-column prop="buyCount" label="销量" width="90" />
<el-table-column prop="quantity" label="库存" width="150" /> <el-table-column prop="quantity" label="库存" width="90" />
<el-table-column label="销售模式" width="150"> <el-table-column label="销售模式" width="150">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row">{{ salesModelText(row.salesModel) }}</span> <span v-if="row">{{ salesModelText(row.salesModel) }}</span>
@@ -465,4 +469,19 @@ export default {
font-size: 14px; font-size: 14px;
} }
} }
.goods-name-text {
white-space: normal;
word-break: break-word;
line-height: 1.5;
display: inline-block;
}
:deep(.goods-name-column .cell) {
white-space: normal;
line-height: 1.5;
}
.link-text {
color: #409eff;
cursor: pointer;
text-decoration: none;
}
</style> </style>

View File

@@ -1,63 +1,187 @@
<template> <template>
<div class="wrapper"> <div class="wrapper">
<div class="wap-content"> <div class="wap-content">
<div class="query-wrapper"> <div class="goods-filter-panel">
<div class="query-item"> <el-form :model="filterForm" inline label-width="72px" class="goods-filter-form" @keyup.enter="onSearchGoods">
<div>搜索范围</div> <el-form-item label-width="0" class="filter-row-item">
<div class="filter-row">
<div class="filter-row-field">
<span class="filter-row-label">商品名称</span>
<el-input <el-input
v-model="goodsParams.goodsName" v-model="filterForm.goodsName"
placeholder="商品名称" placeholder="请输入商品名称"
clearable clearable
style="width: 150px" class="filter-control"
@clear="onSearchGoods"
@keyup.enter="onSearchGoods"
/> />
</div> </div>
<div class="query-item"> <div class="filter-row-field">
<span class="filter-row-label">商品ID</span>
<el-input
v-model="filterForm.goodsId"
placeholder="请输入商品ID"
clearable
class="filter-control"
/>
</div>
<div class="filter-row-field">
<span class="filter-row-label">商品类型</span>
<el-select v-model="filterForm.goodsType" placeholder="请选择" clearable class="filter-control">
<el-option label="实物商品" value="PHYSICAL_GOODS" />
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
<el-option label="电子卡券" value="E_COUPON" />
</el-select>
</div>
</div>
</el-form-item>
<el-form-item label-width="0" class="filter-row-item">
<div class="filter-row">
<div class="filter-row-field">
<span class="filter-row-label">品牌</span>
<el-select
v-model="filterForm.brandId"
placeholder="请选择品牌"
clearable
filterable
class="filter-control"
>
<el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<div class="filter-row-field">
<span class="filter-row-label">店铺分类</span>
<el-cascader <el-cascader
v-model="category" v-model="filterForm.storeCategoryPath"
:options="skuList" :options="shopCategoryList"
placeholder="请选择商品分类" :props="shopCategoryProps"
popper-class="goods-dialog-cascader-popper" placeholder="请选择店铺分类"
style="width: 250px"
clearable clearable
filterable
class="filter-control"
/> />
</div> </div>
<div class="query-item"> <div class="filter-row-field">
<span class="filter-row-label">平台类目</span>
<el-cascader
v-model="filterForm.categoryPath"
:options="platformCategoryList"
:props="platformCategoryProps"
placeholder="请选择平台类目"
popper-class="goods-dialog-cascader-popper"
clearable
filterable
class="filter-control"
/>
</div>
</div>
</el-form-item>
<el-form-item label-width="0" class="range-form-item range-filter-row-item">
<div class="range-filter-row">
<div class="range-group">
<span class="filter-row-label">价格</span>
<div class="range-group-inputs">
<el-input v-model="filterForm.priceMin" placeholder="≥" clearable />
<span class="range-sep">~</span>
<el-input v-model="filterForm.priceMax" placeholder="≤" clearable />
</div>
</div>
<div class="range-group">
<span class="filter-row-label">销量</span>
<div class="range-group-inputs">
<el-input v-model="filterForm.salesMin" placeholder="≥" clearable />
<span class="range-sep">~</span>
<el-input v-model="filterForm.salesMax" placeholder="≤" clearable />
</div>
</div>
<div class="range-group">
<span class="filter-row-label">库存</span>
<div class="range-group-inputs">
<el-input v-model="filterForm.stockMin" placeholder="≥" clearable />
<span class="range-sep">~</span>
<el-input v-model="filterForm.stockMax" placeholder="≤" clearable />
</div>
</div>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearchGoods">搜索</el-button> <el-button type="primary" @click="onSearchGoods">搜索</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</div> </div>
</div>
<div> <div ref="tableWrap" class="goods-table-wrap">
<div class="wap-content-list"> <el-table
<div ref="goodsTable"
class="wap-content-item" v-loading="loading"
:class="{ active: item.selected }" :data="goodsData"
@click="checkedGoods(item, index)" border
v-for="(item, index) in goodsData" :height="tableHeight"
:key="index" :highlight-current-row="type !== 'multiple'"
class="goods-sku-table"
row-key="id"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
> >
<div> <el-table-column
<img :src="item.thumbnail" alt="" /> v-if="type === 'multiple'"
</div> type="selection"
<div class="wap-content-desc"> width="48"
<div class="wap-content-desc-title">{{ item.goodsName }}</div> align="center"
<div class="wap-sku"> :reserve-selection="true"
{{ item.goodsUnit }} />
<el-tag <el-table-column prop="id" label="ID" min-width="150" show-overflow-tooltip />
style="margin-left: 10px" <el-table-column label="商品图片" width="90" align="center">
:type="item.salesModel === 'RETAIL' ? 'info' : 'primary'" <template #default="{ row }">
> <img v-if="row" :src="row.thumbnail" class="goods-thumb" alt="" />
{{ item.salesModel === "RETAIL" ? "零售型" : "批发型" }} </template>
</el-tag> </el-table-column>
</div> <el-table-column label="商品名称" min-width="160" class-name="goods-name-column">
<div class="wap-content-desc-bottom"> <template #default="{ row }">
<div>{{ $filters.unitPrice(item.price) }}</div> <span v-if="row" class="goods-name-text">{{ row.goodsName }}</span>
</div> </template>
</div> </el-table-column>
</div> <el-table-column label="规格" min-width="120" show-overflow-tooltip>
<div v-if="loading" v-loading="loading" class="loading-mask" /> <template #default="{ row }">
<div v-if="empty" class="empty">暂无商品信息</div> {{ row?.simpleSpecs || "-" }}
</template>
</el-table-column>
<el-table-column label="商品类型" width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ goodsTypeText(row?.goodsType) }}
</template>
</el-table-column>
<el-table-column label="店铺分类" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ storeCategoryText(row?.storeCategoryPath) }}
</template>
</el-table-column>
<el-table-column label="平台类目" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ platformCategoryText(row?.categoryPath) }}
</template>
</el-table-column>
<el-table-column label="品牌" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ brandText(row?.brandId) }}
</template>
</el-table-column>
<el-table-column label="价格" width="100" align="right">
<template #default="{ row }">
<span v-if="row" class="goods-price">{{ $filters.unitPrice(row.price, "") }}</span>
</template>
</el-table-column>
<el-table-column label="销量" width="80" align="center">
<template #default="{ row }">
{{ row?.buyCount ?? 0 }}
</template>
</el-table-column>
<el-table-column prop="quantity" label="库存" width="80" align="center" />
<template #empty>
<div class="empty">{{ loading ? "" : "暂无商品信息" }}</div>
</template>
</el-table>
</div> </div>
<el-pagination <el-pagination
v-model:current-page="goodsParams.pageNumber" v-model:current-page="goodsParams.pageNumber"
class="pageration" class="pageration"
@@ -69,10 +193,25 @@
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import * as API_Goods from "@/api/goods"; import * as API_Goods from "@/api/goods";
const defaultFilterForm = () => ({
goodsName: "",
goodsId: "",
storeCategoryPath: "",
categoryPath: "",
goodsType: "",
brandId: "",
priceMin: "",
priceMax: "",
salesMin: "",
salesMax: "",
stockMin: "",
stockMax: "",
});
export default { export default {
props: { props: {
selectedWay: { selectedWay: {
@@ -84,29 +223,39 @@ export default {
return { return {
type: "multiple", type: "multiple",
selectedList: [], selectedList: [],
skuList: [], platformCategoryList: [],
shopCategoryList: [],
brandList: [],
platformCategoryProps: {
value: "id",
label: "name",
children: "children",
checkStrictly: true,
emitPath: false,
},
shopCategoryProps: {
value: "value",
label: "label",
children: "children",
checkStrictly: true,
emitPath: false,
},
filterForm: defaultFilterForm(),
total: 0, total: 0,
goodsParams: { goodsParams: {
pageNumber: 1, pageNumber: 1,
pageSize: 15, pageSize: 20,
order: "desc", order: "desc",
goodsName: "",
sn: "",
categoryPath: "",
marketEnable: "UPPER", marketEnable: "UPPER",
authFlag: "PASS", authFlag: "PASS",
sort: "createTime", sort: "createTime",
}, },
category: [],
goodsData: [], goodsData: [],
empty: false,
loading: false, loading: false,
tableHeight: 400,
}; };
}, },
watch: { watch: {
category(val) {
this.goodsParams.categoryPath = val && val.length ? val.join(",") : "";
},
selectedWay: { selectedWay: {
handler(val) { handler(val) {
this.selectedList = Array.isArray(val) ? val.slice() : []; this.selectedList = Array.isArray(val) ? val.slice() : [];
@@ -114,41 +263,82 @@ export default {
deep: true, deep: true,
immediate: true, immediate: true,
}, },
"goodsParams.categoryPath": {
handler() {
this.goodsData = [];
this.goodsParams.pageNumber = 1;
this.getQueryGoodsList();
},
deep: true,
},
}, },
mounted() { mounted() {
this.init(); this.init();
this.$nextTick(this.calcTableHeight);
window.addEventListener("resize", this.calcTableHeight);
},
beforeUnmount() {
window.removeEventListener("resize", this.calcTableHeight);
}, },
methods: { methods: {
calcTableHeight() {
this.$nextTick(() => {
const wrap = this.$refs.tableWrap;
if (wrap) {
this.tableHeight = Math.max(wrap.clientHeight, 320);
}
});
},
onSearchGoods() { onSearchGoods() {
this.goodsData = [];
this.goodsParams.pageNumber = 1; this.goodsParams.pageNumber = 1;
this.getQueryGoodsList(); this.getQueryGoodsList();
}, },
resetFilter() {
this.filterForm = defaultFilterForm();
this.onSearchGoods();
},
changePageSize(v) { changePageSize(v) {
this.goodsParams.pageNumber = v; this.goodsParams.pageNumber = v;
this.getQueryGoodsList(); this.getQueryGoodsList();
}, },
buildQueryParams() {
const params = {
...this.goodsParams,
goodsName: this.filterForm.goodsName || undefined,
goodsId: this.filterForm.goodsId || undefined,
goodsType: this.filterForm.goodsType || undefined,
brandId: this.filterForm.brandId || undefined,
storeCategoryPath: this.filterForm.storeCategoryPath || undefined,
categoryPath: this.filterForm.categoryPath || undefined,
geQuantity: undefined,
leQuantity: undefined,
geBuyCount: undefined,
leBuyCount: undefined,
price: undefined,
};
const { priceMin, priceMax, salesMin, salesMax, stockMin, stockMax } = this.filterForm;
if (priceMin !== "" && priceMax !== "") {
params.price = `${priceMin}_${priceMax}`;
} else if (priceMin !== "") {
params.price = String(priceMin);
} else if (priceMax !== "") {
params.price = `0_${priceMax}`;
}
if (stockMin !== "" && stockMin != null) params.geQuantity = Number(stockMin);
if (stockMax !== "" && stockMax != null) params.leQuantity = Number(stockMax);
if (salesMin !== "" && salesMin != null) params.geBuyCount = Number(salesMin);
if (salesMax !== "" && salesMax != null) params.leBuyCount = Number(salesMax);
return params;
},
getQueryGoodsList() { getQueryGoodsList() {
this.loading = true; this.loading = true;
API_Goods.getGoodsSkuData(this.goodsParams) API_Goods.getGoodsSkuData(this.buildQueryParams())
.then((res) => { .then((res) => {
this.initGoods(res); this.initGoods(res);
}) })
.finally(() => { .finally(() => {
this.loading = false; this.loading = false;
this.calcTableHeight();
}); });
}, },
initGoods(res) { initGoods(res) {
const records = res?.result?.records || []; const records = res?.result?.records || [];
if (records.length) { this.total = res?.result?.total || 0;
records.forEach((item) => { records.forEach((item) => {
item.selected = false; item.selected = false;
item.___type = "goods"; item.___type = "goods";
@@ -158,113 +348,281 @@ export default {
} }
}); });
}); });
this.total = res.result.total || 0;
this.goodsData = records; this.goodsData = records;
this.empty = false; this.syncTableSelection();
} else { },
this.goodsData = []; syncTableSelection() {
this.empty = true; this.$nextTick(() => {
const table = this.$refs.goodsTable;
if (!table) return;
if (this.type === "multiple") {
table.clearSelection();
this.goodsData.forEach((row) => {
if (row.selected) {
table.toggleRowSelection(row, true);
} }
});
return;
}
const selected = this.goodsData.find((item) => item.selected);
table.setCurrentRow(selected || null);
});
}, },
emitSelected(list) { emitSelected(list) {
this.selectedList = list; this.selectedList = list;
this.$emit("selected", this.selectedList); this.$emit("selected", this.selectedList);
}, },
init() { handleRowClick(row) {
API_Goods.getGoodsSkuData(this.goodsParams).then((res) => { if (this.type === "multiple") return;
this.initGoods(res);
});
API_Goods.getGoodsCategoryAll(0).then((res) => {
if (res.result) {
this.deepGroup(res.result);
}
});
},
deepGroup(val) {
val.forEach((item) => {
let childWay = [];
if (item.children) {
item.children.forEach((child) => {
if (child.children) {
child.children.forEach((grandson, index, arr) => {
arr[index] = {
value: grandson.id,
label: grandson.name,
};
});
}
childWay.push({
value: child.id,
label: child.name,
children: child.children,
});
});
}
this.skuList.push({
value: item.id,
label: item.name,
children: childWay,
});
});
},
checkedGoods(val) {
if (this.type !== "multiple") {
this.goodsData.forEach((item) => { this.goodsData.forEach((item) => {
item.selected = false; item.selected = false;
}); });
val.selected = true; row.selected = true;
this.emitSelected([val]); this.emitSelected([row]);
return; this.$refs.goodsTable?.setCurrentRow(row);
},
handleSelectionChange(selection) {
if (this.type !== "multiple") return;
const selectedIds = new Set(selection.map((item) => item.id));
this.goodsData.forEach((item) => {
item.selected = selectedIds.has(item.id);
});
const currentPageIds = new Set(this.goodsData.map((item) => item.id));
const otherPages = this.selectedList.filter((item) => !currentPageIds.has(item.id));
this.emitSelected([...otherPages, ...selection]);
},
formatShopCategory(list) {
return (list || []).map((item) => ({
value: item.id,
label: item.labelName,
children:
item.children && item.children.length ? this.formatShopCategory(item.children) : undefined,
}));
},
goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
},
brandText(brandId) {
if (!brandId) return "-";
const brand = this.brandList.find((item) => String(item.id) === String(brandId));
return brand?.name || "-";
},
findTreeNodeLabel(tree, targetId, valueKey, labelKey) {
for (const node of tree || []) {
if (String(node[valueKey]) === String(targetId)) {
return node[labelKey];
} }
if (!val.selected) { const childLabel = this.findTreeNodeLabel(node.children, targetId, valueKey, labelKey);
val.selected = true; if (childLabel) return childLabel;
this.emitSelected([...this.selectedList, val]);
} else {
val.selected = false;
this.emitSelected(this.selectedList.filter((item) => item.id !== val.id));
} }
return "";
},
categoryPathText(path, tree, valueKey, labelKey) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "-";
const labels = ids
.map((id) => this.findTreeNodeLabel(tree, id, valueKey, labelKey))
.filter(Boolean);
return labels.length ? labels.join(" / ") : "-";
},
storeCategoryText(path) {
return this.categoryPathText(path, this.shopCategoryList, "value", "label");
},
platformCategoryText(path) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "-";
const targetId = ids.length >= 3 ? ids[2] : ids[ids.length - 1];
const label = this.findTreeNodeLabel(this.platformCategoryList, targetId, "id", "name");
return label || "-";
},
init() {
this.getQueryGoodsList();
API_Goods.getCategoryTree().then((res) => {
if (res?.success) {
this.platformCategoryList = res.result || [];
}
});
API_Goods.getShopGoodsLabelListSeller().then((res) => {
if (res?.success) {
this.shopCategoryList = this.formatShopCategory(res.result || []);
}
});
API_Goods.getBrandListData().then((res) => {
this.brandList = Array.isArray(res) ? res : res?.result || [];
});
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "./style.scss"; @import "./style.scss";
.wrapper {
height: 100%;
flex-direction: column;
}
.wap-content { .wap-content {
width: 100%; width: 100%;
flex: 1;
min-height: 0;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
} }
.goods-table-wrap {
flex: 1;
min-height: 0;
}
.goods-filter-panel {
background: #f5f7fa;
padding: 12px 16px 4px;
border-radius: 4px;
margin-bottom: 12px;
}
.goods-filter-form {
margin-bottom: 0;
:deep(.el-form-item) {
margin-right: 8px;
margin-bottom: 8px;
}
}
.range-form-item {
:deep(.el-form-item__content) {
margin-left: 0 !important;
}
}
.range-filter-row-item {
display: block;
width: 100%;
:deep(.el-form-item__content) {
width: 100%;
}
}
.filter-row-item {
display: block;
width: 100%;
:deep(.el-form-item__content) {
width: 100%;
margin-left: 0 !important;
}
}
.filter-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 16px;
}
.filter-row-field {
display: inline-flex;
align-items: center;
flex: 0 0 244px;
min-width: 244px;
}
.filter-control {
width: 160px;
}
.filter-row-label {
display: inline-flex;
align-items: center;
justify-content: flex-end;
flex: 0 0 72px;
padding-right: 12px;
color: #606266;
font-size: 14px;
white-space: nowrap;
box-sizing: border-box;
}
.range-filter-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 16px;
}
.range-group {
display: inline-flex;
align-items: center;
flex: 0 0 244px;
min-width: 244px;
}
.range-group-inputs {
display: inline-flex;
align-items: center;
:deep(.el-input) {
width: 72px;
}
}
.range-sep {
margin: 0 4px;
color: #909399;
user-select: none;
}
.goods-sku-table {
width: 100%;
}
.goods-name-text {
white-space: normal;
word-break: break-word;
line-height: 1.5;
}
:deep(.goods-name-column .cell) {
white-space: normal;
word-break: break-word;
line-height: 1.5;
}
.goods-thumb {
width: 48px;
height: 48px;
object-fit: contain;
vertical-align: middle;
}
.goods-price {
color: $theme_color;
}
.empty { .empty {
text-align: center; text-align: center;
padding: 8px 0; padding: 24px 0;
width: 100%; color: #909399;
}
.wap-content {
flex: 1;
padding: 0;
}
.wap-content-list {
display: flex;
position: relative;
flex-wrap: wrap;
height: 340px;
}
.wap-content-item {
width: 210px;
margin: 10px 7px;
padding: 6px 0;
}
.active {
background: url("../../assets/selected.png") no-repeat;
background-position: right;
background-size: 10%;
}
.loading-mask {
position: absolute;
inset: 0;
} }
.pageration { .pageration {
margin-top: 12px; margin-top: 12px;
justify-content: flex-end; justify-content: flex-end;
} }
:deep(.el-table__body tr) {
cursor: pointer;
}
</style> </style>
<style lang="scss"> <style lang="scss">
.goods-dialog-cascader-popper { .goods-dialog-cascader-popper {

View File

@@ -1,6 +1,33 @@
<template> <template>
<el-drawer
v-model="goodsVisible"
title="选择商品"
direction="rtl"
size="85%"
:z-index="10000"
:close-on-click-modal="false"
append-to-body
destroy-on-close
class="lili-goods-drawer"
>
<goodsDialog
v-if="goodsVisible"
ref="goodsDialog"
:selectedWay="goodsData"
@selected="
(val) => {
goodsData = val;
}
"
/>
<template #footer>
<el-button @click="clickClose">取消</el-button>
<el-button type="primary" @click="clickOK">确定</el-button>
</template>
</el-drawer>
<el-dialog <el-dialog
v-model="flag" v-model="linkVisible"
width="1160px" width="1160px"
top="120px" top="120px"
:z-index="10000" :z-index="10000"
@@ -9,27 +36,15 @@
destroy-on-close destroy-on-close
@close="clickClose" @close="clickClose"
> >
<template v-if="flag">
<goodsDialog
@selected="
(val) => {
goodsData = val;
}
"
v-if="goodsFlag"
ref="goodsDialog"
:selectedWay="goodsData"
/>
<linkDialog <linkDialog
v-if="linkVisible"
class="linkDialog"
@selectedLink=" @selectedLink="
(val) => { (val) => {
linkData = val; linkData = val;
} }
" "
v-else
class="linkDialog"
/> />
</template>
<template #footer> <template #footer>
<el-button @click="clickClose">取消</el-button> <el-button @click="clickClose">取消</el-button>
<el-button type="primary" @click="clickOK">确定</el-button> <el-button type="primary" @click="clickOK">确定</el-button>
@@ -46,23 +61,44 @@ export default {
}, },
data() { data() {
return { return {
goodsVisible: false,
linkVisible: false,
goodsFlag: false, goodsFlag: false,
goodsData: [], goodsData: [],
linkData: "", linkData: "",
flag: false,
}; };
}, },
computed: {
flag: {
get() {
return this.goodsVisible || this.linkVisible;
},
set(val) {
if (val) {
if (this.goodsFlag) {
this.goodsVisible = true;
} else {
this.linkVisible = true;
}
} else {
this.goodsVisible = false;
this.linkVisible = false;
}
},
},
},
methods: { methods: {
clearGoodsSelected() { clearGoodsSelected() {
this.goodsData = []; this.goodsData = [];
}, },
clickClose() { clickClose() {
this.flag = false; this.goodsVisible = false;
this.linkVisible = false;
this.goodsFlag = false; this.goodsFlag = false;
this.$emit("closeFlag", false); this.$emit("closeFlag", false);
}, },
singleGoods() { singleGoods() {
var timer = setInterval(() => { const timer = setInterval(() => {
if (this.$refs.goodsDialog) { if (this.$refs.goodsDialog) {
this.$refs.goodsDialog.type = "single"; this.$refs.goodsDialog.type = "single";
clearInterval(timer); clearInterval(timer);
@@ -70,34 +106,50 @@ export default {
}, 100); }, 100);
}, },
clickOK() { clickOK() {
if (this.goodsFlag) { if (this.goodsVisible) {
this.$emit("selectedGoodsData", this.goodsData); this.$emit("selectedGoodsData", this.goodsData);
} else { } else if (this.linkVisible) {
this.$emit("selectedLink", this.linkData); this.$emit("selectedLink", this.linkData);
} }
this.clickClose(); this.clickClose();
}, },
open(type, mutiple) { open(type, mutiple) {
this.flag = true;
if (type == "goods") { if (type == "goods") {
this.goodsFlag = true; this.goodsFlag = true;
this.goodsVisible = true;
if (mutiple) { if (mutiple) {
this.singleGoods(); this.singleGoods();
} }
} else { } else {
this.goodsFlag = false; this.goodsFlag = false;
this.linkVisible = true;
} }
}, },
close() { close() {
this.flag = false; this.clickClose();
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.lili-goods-drawer {
:deep(.el-drawer__body) {
display: flex;
flex-direction: column;
padding: 12px 20px;
overflow: hidden;
}
:deep(.el-drawer__footer) {
border-top: 1px solid #ebeef5;
padding: 12px 20px;
text-align: right;
}
}
:deep(.el-dialog__body) { :deep(.el-dialog__body) {
width: 100%; width: 100%;
height: 500px; height: 640px;
overflow: hidden; overflow: hidden;
} }
</style> </style>