feat(会员管理): 添加第三方账户绑定和会员等级功能

- 在登录API中新增获取第三方账户绑定列表、绑定和解绑功能
- 在会员API中新增获取当前会员等级、等级规则和等级列表的接口
- 在会员中心页面中添加第三方账户绑定的UI和逻辑
- 新增会员等级页面,展示当前等级、经验值和经验值记录
This commit is contained in:
pikachu1995@126.com
2026-05-13 18:53:33 +08:00
parent fe4e0ce75c
commit aa4bc81da4
22 changed files with 3262 additions and 386 deletions

View File

@@ -0,0 +1,197 @@
<template>
<div class="search">
<Card>
<Form
ref="searchForm"
:model="searchForm"
inline
:label-width="90"
@keydown.enter.native="handleSearch"
@submit.native.prevent
class="search-form"
>
<FormItem label="客户手机号" prop="mobile">
<Input
v-model="searchForm.memberMobile"
clearable
placeholder="请输入客户手机号"
style="width: 220px"
/>
</FormItem>
<FormItem label="规则" prop="ruleKey">
<Select v-model="searchForm.ruleKey" clearable filterable style="width: 220px">
<Option v-for="item in ruleOptions" :key="item.value" :value="item.value">
{{ item.label }}
</Option>
</Select>
</FormItem>
<Button type="primary" icon="ios-search" @click="handleSearch">搜索</Button>
</Form>
</Card>
<Card>
<Table
:loading="loading"
border
:columns="columns"
:data="data"
class="mt_10"
></Table>
<Row type="flex" justify="end" class="mt_10">
<Page
:current="searchForm.pageNumber"
:total="total"
:page-size="searchForm.pageSize"
@on-change="changePage"
@on-page-size-change="changePageSize"
:page-size-opts="[20, 50, 100]"
size="small"
show-total
show-elevator
show-sizer
></Page>
</Row>
</Card>
</div>
</template>
<script>
import * as API_Member from "@/api/member";
const RULE_OPTIONS = [
{ value: "CONSUME", label: "消费" },
{ value: "REGISTER", label: "注册" },
{ value: "SIGN_IN", label: "签到" },
{ value: "COMMENT", label: "评价" },
{ value: "SHARE", label: "分享商城" },
{ value: "PROFILE", label: "完善信息" },
{ value: "FOLLOW_STORE", label: "关注店铺" },
{ value: "BIND_WECHAT", label: "绑定微信" },
{ value: "ADD_ADDRESS", label: "添加收货地址" },
{ value: "SHARE_REGISTER", label: "分享注册" },
{ value: "SHARE_BUY", label: "分享购买" },
];
export default {
name: "memberGradeExperienceLog",
data() {
return {
loading: false,
total: 0,
data: [],
ruleOptions: RULE_OPTIONS,
searchForm: {
pageNumber: 1,
pageSize: 20,
memberMobile: "",
ruleKey: "",
},
columns: [
{
title: "客户手机号",
key: "memberMobile",
width: 140,
tooltip: true,
render: (h, params) => {
const v = params.row.memberMobile || params.row.mobile || "-";
return h("span", v);
},
},
{
title: "规则名称",
key: "ruleName",
width: 150,
tooltip: true,
render: (h, params) => {
const text = params.row.ruleName || this.findRuleName(params.row.ruleKey) || "-";
return h("span", text);
},
},
{
title: "变化经验值",
key: "value",
width: 120,
render: (h, params) => {
const v =
params.row.value ??
params.row.variableExperience ??
params.row.experience ??
params.row.variableValue;
return h("span", v == null ? "-" : v);
},
},
{
title: "经验值限额",
key: "maxValue",
width: 120,
render: (h, params) => {
const v = params.row.maxValue ?? params.row.maxExperience ?? params.row.limitValue;
return h("span", v == null ? "-" : v);
},
},
{
title: "操作说明",
key: "content",
minWidth: 200,
tooltip: true,
render: (h, params) => {
const text = params.row.content || params.row.remark || params.row.description || "-";
return h("span", text);
},
},
{ title: "创建时间", key: "createTime", width: 170 },
],
};
},
mounted() {
this.init();
},
methods: {
findRuleName(ruleKey) {
const hit = this.ruleOptions.find((item) => item.value === ruleKey);
return hit ? hit.label : "";
},
init() {
this.getData();
},
buildParams() {
const params = {
pageNumber: this.searchForm.pageNumber,
pageSize: this.searchForm.pageSize,
};
if (this.searchForm.memberMobile) params.mobile = this.searchForm.memberMobile;
if (this.searchForm.ruleKey) params.ruleKey = this.searchForm.ruleKey;
return params;
},
getData() {
this.loading = true;
API_Member.getMemberExperienceByPage(this.buildParams())
.then((res) => {
if (res && res.success && res.result) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
} else {
this.data = [];
this.total = 0;
}
})
.finally(() => {
this.loading = false;
});
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getData();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getData();
},
changePageSize(v) {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = v;
this.getData();
},
},
};
</script>

View File

@@ -0,0 +1,556 @@
<template>
<div class="experience-setting">
<Card>
<Form :label-width="120" label-position="right">
<Table :loading="loading" :columns="columns" :data="form.items" class="mt_10 experience-table"></Table>
<FormItem label="经验值说明" style="margin-top: 16px" class="desc-item">
<Input
v-model="form.description"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="请输入经验值说明"
/>
</FormItem>
<FormItem>
<Button type="primary" :loading="submitLoading" @click="submit">保存</Button>
</FormItem>
</Form>
</Card>
</div>
</template>
<script>
import { getSetting, setSetting } from "@/api/index";
const RULE_OPTIONS = [
{ ruleKey: "CONSUME", ruleName: "消费" },
{ ruleKey: "REGISTER", ruleName: "注册" },
{ ruleKey: "SIGN_IN", ruleName: "签到" },
{ ruleKey: "COMMENT", ruleName: "评价" },
{ ruleKey: "SHARE", ruleName: "分享商城" },
{ ruleKey: "PROFILE", ruleName: "完善信息" },
{ ruleKey: "FOLLOW_STORE", ruleName: "关注店铺" },
{ ruleKey: "BIND_WECHAT", ruleName: "绑定微信" },
{ ruleKey: "ADD_ADDRESS", ruleName: "添加收货地址" },
{ ruleKey: "SHARE_REGISTER", ruleName: "分享注册" },
{ ruleKey: "SHARE_BUY", ruleName: "分享购买" },
];
const defaultRuleItem = (rule) => ({
ruleKey: rule.ruleKey,
ruleName: rule.ruleName,
enabled: false,
value: 1,
maxValue: null,
});
const defaultForm = () => ({
items: RULE_OPTIONS.map((item) => defaultRuleItem(item)),
description: "",
});
export default {
name: "memberGradeExperienceSetting",
data() {
return {
loading: false,
submitLoading: false,
form: defaultForm(),
columns: [
{
title: "是否开启",
key: "enabled",
width: 90,
align: "center",
render: (h, params) => {
return h("Checkbox", {
props: { value: !!this.form.items[params.index].enabled },
on: {
"on-change": (checked) => {
this.updateRuleEnabled(params.index, checked);
},
},
});
},
},
{
title: "类型",
key: "ruleName",
width: 160,
},
{
title: "经验值(1-100)",
key: "value",
minWidth: 700,
render: (h, params) => {
const row = this.form.items[params.index] || {};
const inputNode = h("Input", {
style: { width: "120px" },
props: {
value: row.value == null ? "" : String(row.value),
number: true,
},
on: {
input: (val) => {
this.updateRuleValue(params.index, val);
},
"on-change": (v) => {
this.updateRuleValue(params.index, v);
},
"on-blur": () => {
this.commitRuleValue(params.index);
},
},
});
const maxInputNode = h("Input", {
style: { width: "120px" },
props: {
value: row.maxValue == null ? "" : String(row.maxValue),
number: true,
},
on: {
input: (val) => {
this.updateRuleMaxValue(params.index, val);
},
"on-change": (v) => {
this.updateRuleMaxValue(params.index, v);
},
"on-blur": () => {
this.commitRuleMaxValue(params.index);
},
},
});
if (params.row.ruleKey === "REGISTER") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center" } },
[h("span", { style: { marginRight: "8px" } }, "获得经验值:"), inputNode]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"会员注册成功后可获得经验值"
),
]
);
}
if (params.row.ruleKey === "SHARE") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", marginRight: "16px" } },
[h("span", { style: { marginRight: "8px" } }, "分享商品详情页获得经验值:"), inputNode]
),
h(
"div",
{ style: { display: "flex", alignItems: "center" } },
[h("span", { style: { marginRight: "8px" } }, "可获得经验值限额:"), maxInputNode]
),
]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"会员分享商城页面可获得的经验值"
),
]
);
}
if (params.row.ruleKey === "COMMENT") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[h("span", { style: { marginRight: "8px" } }, "对已购买商品完成提交评论获得经验值:"), inputNode]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"仅针对评论字数大于30字的评论进行发放"
),
]
);
}
if (params.row.ruleKey === "FOLLOW_STORE") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", marginRight: "16px" } },
[h("span", { style: { marginRight: "8px" } }, "获得经验值:"), inputNode]
),
h(
"div",
{ style: { display: "flex", alignItems: "center" } },
[h("span", { style: { marginRight: "8px" } }, "可获得经验值限额:"), maxInputNode]
),
]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"关注店铺可获得经验值每个客户D相同店铺仅第一次关注可进行获得"
),
]
);
}
if (params.row.ruleKey === "PROFILE") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[h("span", { style: { marginRight: "8px" } }, "获得经验值:"), inputNode]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"完善个人基本信息可获得经验值,每个会员仅可获得一次"
),
]
);
}
if (params.row.ruleKey === "BIND_WECHAT") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[h("span", { style: { marginRight: "8px" } }, "获取经验值:"), inputNode]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"绑定微信成功获得经验值,每个会员仅可获得一次"
),
]
);
}
if (params.row.ruleKey === "ADD_ADDRESS") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[h("span", { style: { marginRight: "8px" } }, "获取经验值:"), inputNode]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"添加收货地址后获得经验值,每个会员仅可获得一次"
),
]
);
}
if (params.row.ruleKey === "SHARE_REGISTER") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", marginRight: "16px" } },
[h("span", { style: { marginRight: "8px" } }, "获取的经验值:"), inputNode]
),
h(
"div",
{ style: { display: "flex", alignItems: "center" } },
[h("span", { style: { marginRight: "8px" } }, "可获得经验值限额:"), maxInputNode]
),
]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"仅被注册成功后才可获得相应奖励经验值"
),
]
);
}
if (params.row.ruleKey === "SHARE_BUY") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", marginRight: "16px" } },
[h("span", { style: { marginRight: "8px" } }, "获取的经验值:"), inputNode]
),
h(
"div",
{ style: { display: "flex", alignItems: "center" } },
[h("span", { style: { marginRight: "8px" } }, "可获得经验值限额:"), maxInputNode]
),
]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"仅被购买成功后才可获得相应奖励经验值"
),
]
);
}
if (params.row.ruleKey === "SIGN_IN") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[h("span", { style: { marginRight: "8px" } }, "获取的经验值:"), inputNode]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"客户每日签到后可获的经验值"
),
]
);
}
if (params.row.ruleKey === "CONSUME") {
return h(
"div",
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-start" } },
[
h(
"div",
{ style: { display: "flex", alignItems: "center", flexWrap: "wrap" } },
[h("span", { style: { marginRight: "8px" } }, "1元获取经验值"), inputNode]
),
h(
"div",
{ style: { marginTop: "6px", color: "#808695", fontSize: "12px" } },
"客户消费1元可获取经验值向下取整"
),
]
);
}
return inputNode;
},
},
],
};
},
mounted() {
this.loadData();
},
methods: {
getRawInputValue(v) {
return v && v.target ? v.target.value : v;
},
updateRuleEnabled(index, enabled) {
const item = this.form.items[index] || {};
this.$set(this.form.items, index, {
...item,
enabled: !!enabled,
});
},
updateRuleValue(index, v) {
const raw = this.getRawInputValue(v);
const next = Number(raw);
const item = this.form.items[index] || {};
this.$set(this.form.items, index, {
...item,
value: Number.isFinite(next) ? next : null,
});
},
commitRuleValue(index) {
const item = this.form.items[index] || {};
const next = Number(item.value);
let value = 1;
if (Number.isFinite(next)) {
if (next < 1) value = 1;
else if (next > 100) value = 100;
else value = Math.floor(next);
}
this.$set(this.form.items, index, {
...item,
value,
});
},
updateRuleMaxValue(index, v) {
const raw = this.getRawInputValue(v);
const item = this.form.items[index] || {};
if (raw == null || raw === "") {
this.$set(this.form.items, index, {
...item,
maxValue: null,
});
return;
}
const next = Number(raw);
this.$set(this.form.items, index, {
...item,
maxValue: Number.isFinite(next) ? next : null,
});
},
commitRuleMaxValue(index) {
const item = this.form.items[index] || {};
if (item.maxValue == null || item.maxValue === "") {
this.$set(this.form.items, index, {
...item,
maxValue: null,
});
return;
}
const next = Number(item.maxValue);
this.$set(this.form.items, index, {
...item,
maxValue: Number.isFinite(next) && next >= 1 ? Math.floor(next) : null,
});
},
normalizeConfig(val) {
if (!val) return {};
if (typeof val === "string") {
try {
return JSON.parse(val);
} catch (e) {
return {};
}
}
if (typeof val === "object") return val;
return {};
},
normalizeItems(items) {
const map = {};
if (Array.isArray(items)) {
items.forEach((item) => {
if (!item || !item.ruleKey) return;
map[item.ruleKey] = item;
});
}
return RULE_OPTIONS.map((rule) => {
const hit = map[rule.ruleKey] || {};
return {
ruleKey: rule.ruleKey,
ruleName: hit.ruleName || rule.ruleName,
enabled: !!hit.enabled,
value: Number(hit.value) > 0 ? Number(hit.value) : 1,
maxValue: hit.maxValue == null || hit.maxValue === "" ? null : Number(hit.maxValue),
};
});
},
loadData() {
this.loading = true;
getSetting("EXPERIENCE_SETTING")
.then((res) => {
if (res && res.success) {
const cfg = this.normalizeConfig(res.result);
this.form = {
items: this.normalizeItems(cfg.items),
description: cfg.description || "",
};
} else {
this.form = defaultForm();
}
})
.finally(() => {
this.loading = false;
});
},
validateForm() {
const invalid = this.form.items.find((item) => {
const v = Number(item.value);
if (!Number.isInteger(v) || v < 1 || v > 100) return true;
if (item.maxValue != null && item.maxValue !== "") {
const m = Number(item.maxValue);
if (!Number.isInteger(m) || m < 1) return true;
}
return false;
});
if (invalid) {
this.$Message.error("请检查经验值配置经验值范围为1-100限额需为正整数");
return false;
}
return true;
},
submit() {
if (!this.validateForm()) return;
const payload = {
items: this.form.items.map((item) => ({
ruleKey: item.ruleKey,
ruleName: item.ruleName,
enabled: !!item.enabled,
value: Number(item.value),
maxValue: item.maxValue == null || item.maxValue === "" ? null : Number(item.maxValue),
})),
description: this.form.description || "",
};
this.submitLoading = true;
setSetting("EXPERIENCE_SETTING", payload)
.then((res) => {
if (res && res.success) {
this.$Message.success("保存成功");
}
})
.finally(() => {
this.submitLoading = false;
});
},
},
};
</script>
<style scoped lang="scss">
.experience-setting {
padding: 2px 0;
}
::v-deep .experience-table .ivu-table th {
background: #fafbfc;
}
::v-deep .experience-table .ivu-table td {
padding-top: 12px;
padding-bottom: 12px;
}
::v-deep .experience-table .ivu-table-cell {
font-size: 13px;
line-height: 1.7;
}
::v-deep .desc-item .ivu-form-item-label {
font-size: 16px;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,547 @@
<template>
<div class="search">
<Card>
<Row class="operation padding-row">
<Button type="primary" @click="openAdd">添加客户等级</Button>
</Row>
<Table
:loading="loading"
border
:columns="columns"
:data="data"
class="mt_10"
></Table>
</Card>
<Modal v-model="addFlag" title="添加客户等级" width="720" :z-index="950" :mask-closable="false">
<Form ref="addForm" :model="formAdd" :rules="rules" :label-width="110">
<FormItem label="等级名称" prop="gradeName">
<Input v-model="formAdd.gradeName" maxlength="50" placeholder="请输入等级名称" />
</FormItem>
<FormItem label="是否默认" prop="isDefault">
<RadioGroup v-model="formAdd.isDefault">
<Radio :label="true"></Radio>
<Radio :label="false"></Radio>
</RadioGroup>
</FormItem>
<FormItem label="等级图标" prop="gradeImage">
<upload-pic-input v-model="formAdd.gradeImage"></upload-pic-input>
</FormItem>
<FormItem label="等级背景图" prop="gradeBackground">
<upload-pic-input v-model="formAdd.gradeBackground"></upload-pic-input>
</FormItem>
<FormItem label="字体颜色" prop="gradeFontColor">
<Input v-model="formAdd.gradeFontColor" maxlength="20" placeholder="如:#333333" />
</FormItem>
<FormItem label="所需经验值" prop="requiredExperience">
<InputNumber v-model="formAdd.requiredExperience" :min="1" :precision="0" style="width: 220px"></InputNumber>
</FormItem>
<FormItem label="等级排序" prop="gradeSort">
<InputNumber v-model="formAdd.gradeSort" :min="1" :max="9999" :precision="0" style="width: 220px"></InputNumber>
</FormItem>
<FormItem label="等级开关" prop="gradeState">
<RadioGroup v-model="formAdd.gradeState">
<Radio label="OPEN">开启</Radio>
<Radio label="CLOSE">关闭</Radio>
</RadioGroup>
</FormItem>
<FormItem label="关联权益" prop="benefitIds">
<Select
:value="addBenefitOrder"
multiple
filterable
placeholder="请选择客户权益"
style="width: 100%"
@on-change="onAddBenefitIdsChange"
>
<Option v-for="b in benefitOptions" :key="b.id" :value="String(b.id)">{{ benefitOptionLabel(b) }}</Option>
</Select>
</FormItem>
</Form>
<div slot="footer">
<Button @click="addFlag = false">取消</Button>
<Button type="primary" :loading="submitAddLoading" @click="submitAdd">确定</Button>
</div>
</Modal>
<Modal v-model="editFlag" title="编辑客户等级" width="720" :z-index="950" :mask-closable="false">
<Form ref="editForm" :model="formEdit" :rules="rules" :label-width="110">
<Input v-model="formEdit.id" v-show="false" />
<FormItem label="等级名称" prop="gradeName">
<Input v-model="formEdit.gradeName" maxlength="50" placeholder="请输入等级名称" />
</FormItem>
<FormItem label="是否默认" prop="isDefault">
<RadioGroup v-model="formEdit.isDefault">
<Radio :label="true"></Radio>
<Radio :label="false"></Radio>
</RadioGroup>
</FormItem>
<FormItem label="等级图标" prop="gradeImage">
<upload-pic-input v-model="formEdit.gradeImage"></upload-pic-input>
</FormItem>
<FormItem label="等级背景图" prop="gradeBackground">
<upload-pic-input v-model="formEdit.gradeBackground"></upload-pic-input>
</FormItem>
<FormItem label="字体颜色" prop="gradeFontColor">
<Input v-model="formEdit.gradeFontColor" maxlength="20" placeholder="如:#333333" />
</FormItem>
<FormItem label="所需经验值" prop="requiredExperience">
<InputNumber v-model="formEdit.requiredExperience" :min="1" :precision="0" style="width: 220px"></InputNumber>
</FormItem>
<FormItem label="等级排序" prop="gradeSort">
<InputNumber v-model="formEdit.gradeSort" :min="1" :max="9999" :precision="0" style="width: 220px"></InputNumber>
</FormItem>
<FormItem label="等级开关" prop="gradeState">
<RadioGroup v-model="formEdit.gradeState">
<Radio label="OPEN">开启</Radio>
<Radio label="CLOSE">关闭</Radio>
</RadioGroup>
</FormItem>
<FormItem label="关联权益" prop="benefitIds">
<Select
:value="editBenefitOrder"
multiple
filterable
placeholder="请选择客户权益"
style="width: 100%"
@on-change="onEditBenefitIdsChange"
>
<Option v-for="b in benefitOptions" :key="b.id" :value="String(b.id)">{{ benefitOptionLabel(b) }}</Option>
</Select>
</FormItem>
</Form>
<div slot="footer">
<Button @click="editFlag = false">取消</Button>
<Button type="primary" :loading="submitEditLoading" @click="submitEdit">确定</Button>
</div>
</Modal>
</div>
</template>
<script>
import * as API_Member from "@/api/member.js";
import uploadPicInput from "@/components/lili/upload-pic-input";
const buildDefaultForm = () => ({
id: "",
gradeName: "",
isDefault: false,
gradeImage: "",
gradeBackground: "",
gradeFontColor: "",
requiredExperience: 1,
gradeSort: 1,
gradeState: "OPEN",
benefitIds: "",
});
/** 权益多选:新勾选追加到末尾,取消勾选移除,保持已有顺序 */
function syncOrderedBenefitIds(prevOrder, selected) {
const sel = Array.isArray(selected) ? selected.map((id) => String(id)) : [];
const out = [];
(prevOrder || []).forEach((id) => {
const s = String(id);
if (sel.includes(s)) out.push(s);
});
sel.forEach((s) => {
if (!out.includes(s)) out.push(s);
});
return out;
}
export default {
name: "memberGrade",
components: {
uploadPicInput,
},
data() {
return {
loading: true,
columns: [
{
title: "等级名称",
key: "gradeName",
width: 130,
tooltip: true,
},
{
title: "默认等级",
key: "isDefault",
width: 95,
render: (h, params) => {
const yes = params.row.isDefault === true;
return h(
"Tag",
{
props: {
color: yes ? "success" : "default",
},
},
yes ? "是" : "否"
);
},
},
{
title: "等级图标",
key: "gradeImage",
width: 100,
render: (h, params) => {
if (!params.row.gradeImage) return h("span", "-");
return h("img", {
attrs: {
src: params.row.gradeImage,
alt: "等级图标",
},
style: {
width: "48px",
height: "48px",
objectFit: "contain",
border: "1px solid #dcdee2",
borderRadius: "4px",
background: "#fff",
},
});
},
},
{
title: "等级背景图",
key: "gradeBackground",
width: 120,
render: (h, params) => {
if (!params.row.gradeBackground) return h("span", "-");
return h("img", {
attrs: {
src: params.row.gradeBackground,
alt: "等级背景图",
},
style: {
width: "64px",
height: "40px",
objectFit: "cover",
border: "1px solid #dcdee2",
borderRadius: "4px",
background: "#fff",
},
});
},
},
{
title: "所需经验值",
key: "requiredExperience",
width: 110,
},
{
title: "等级排序",
key: "gradeSort",
width: 95,
},
{
title: "状态",
key: "gradeState",
width: 118,
align: "center",
render: (h, params) => {
const row = params.row;
return h("i-switch", {
props: {
value: row.gradeState === "OPEN",
size: "large",
loading: !!row._gradeStateLoading,
},
on: {
"on-change": (checked) => {
this.onGradeStateSwitch(row, checked);
},
},
}, [
h("span", { slot: "open" }, "开启"),
h("span", { slot: "close" }, "关闭"),
]);
},
},
{
title: "操作",
key: "action",
align: "center",
width: 130,
render: (h, params) => {
const linkStyle = {
color: "#2d8cf0",
cursor: "pointer",
textDecoration: "none",
};
const sep = h(
"span",
{ style: { margin: "0 8px", color: "#dcdee2" } },
"|"
);
return h(
"div",
{ class: "ops", style: { display: "flex", justifyContent: "center" } },
[
h(
"a",
{ style: linkStyle, on: { click: () => this.openEdit(params.row) } },
"编辑"
),
sep,
h(
"a",
{ style: linkStyle, on: { click: () => this.remove(params.row) } },
"删除"
),
]
);
},
},
],
data: [],
addFlag: false,
editFlag: false,
submitAddLoading: false,
submitEditLoading: false,
formAdd: buildDefaultForm(),
formEdit: buildDefaultForm(),
/** 关联权益 id 顺序(与 benefitIds 一致) */
addBenefitOrder: [],
editBenefitOrder: [],
benefitOptions: [],
benefitTypeOptions: [],
benefitOptionsLoading: false,
editDetailLoading: false,
rules: {
gradeName: [{ required: true, message: "请输入等级名称", trigger: "blur" }],
gradeImage: [{ required: true, message: "请上传等级图标", trigger: "change" }],
requiredExperience: [{ required: true, type: "number", message: "请输入所需经验值", trigger: "change" }],
gradeSort: [{ required: true, type: "number", message: "请输入等级排序", trigger: "change" }],
gradeState: [{ required: true, message: "请选择等级开关", trigger: "change" }],
},
};
},
methods: {
benefitOptionLabel(b) {
if (!b) return "";
const name = b.benefitName || String(b.id);
const opt = this.benefitTypeOptions.find((o) => o.value === b.benefitType);
const typeText = opt ? opt.description : b.benefitType || "";
return typeText ? `${name}${typeText}` : name;
},
onAddBenefitIdsChange(val) {
this.addBenefitOrder = syncOrderedBenefitIds(this.addBenefitOrder, val);
},
onEditBenefitIdsChange(val) {
this.editBenefitOrder = syncOrderedBenefitIds(this.editBenefitOrder, val);
},
loadBenefitTypes() {
API_Member.getMemberBenefitTypes().then((res) => {
if (res && res.success && Array.isArray(res.result)) {
this.benefitTypeOptions = res.result;
} else {
this.benefitTypeOptions = [];
}
});
},
loadBenefitOptions() {
this.benefitOptionsLoading = true;
const pageSize = 500;
const fetchPage = (pageNumber) =>
API_Member.getMemberBenefitByPage({ pageNumber, pageSize, sort: "benefitSort", order: "asc" });
return fetchPage(1)
.then((res) => {
if (!(res && res.success && res.result)) {
this.benefitOptions = [];
return;
}
const records = Array.isArray(res.result.records) ? res.result.records : [];
const total = Number(res.result.total) || records.length;
let all = records.slice();
if (total > pageSize) {
const pages = Math.ceil(total / pageSize);
const rest = [];
for (let p = 2; p <= pages; p++) {
rest.push(fetchPage(p));
}
return Promise.all(rest).then((results) => {
results.forEach((r) => {
if (r && r.success && r.result && Array.isArray(r.result.records)) {
all = all.concat(r.result.records);
}
});
this.benefitOptions = all;
});
}
this.benefitOptions = all;
})
.catch(() => {
this.benefitOptions = [];
})
.finally(() => {
this.benefitOptionsLoading = false;
});
},
parseBenefitIdsFromString(str) {
return String(str || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
},
fillEditFormFromGrade(grade, benefitsOrderedIds) {
this.formEdit = {
id: grade.id || "",
gradeName: grade.gradeName || "",
isDefault: grade.isDefault === true,
gradeImage: grade.gradeImage || "",
gradeBackground: grade.gradeBackground || "",
gradeFontColor: grade.gradeFontColor || "",
requiredExperience: Number(grade.requiredExperience) > 0 ? Number(grade.requiredExperience) : 1,
gradeSort: Number(grade.gradeSort) > 0 ? Number(grade.gradeSort) : 1,
gradeState: grade.gradeState || "OPEN",
benefitIds: grade.benefitIds || "",
};
this.editBenefitOrder = (benefitsOrderedIds || []).map((id) => String(id));
},
init() {
this.getData();
this.loadBenefitTypes();
this.loadBenefitOptions();
},
getData() {
this.loading = true;
API_Member.getMemberGradeByPage().then((res) => {
this.loading = false;
if (res && res.success) {
this.data = Array.isArray(res.result) ? res.result : [];
}
});
},
openAdd() {
this.addFlag = true;
this.submitAddLoading = false;
if (!this.benefitOptions.length && !this.benefitOptionsLoading) {
this.loadBenefitOptions();
}
this.$nextTick(() => {
if (this.$refs.addForm) this.$refs.addForm.resetFields();
this.formAdd = buildDefaultForm();
this.addBenefitOrder = [];
});
},
submitAdd() {
this.$refs.addForm.validate((valid) => {
if (!valid) return;
this.submitAddLoading = true;
const { benefitIds: _omit, ...rest } = this.formAdd;
const payload = {
...rest,
benefitIds: (this.addBenefitOrder || []).join(","),
};
API_Member.addMemberGrade(payload).then((res) => {
this.submitAddLoading = false;
if (res && res.success) {
this.$Message.success("添加成功");
this.addFlag = false;
this.getData();
}
});
});
},
openEdit(row) {
this.editFlag = true;
this.submitEditLoading = false;
this.editDetailLoading = true;
if (!this.benefitOptions.length && !this.benefitOptionsLoading) {
this.loadBenefitOptions();
}
this.$nextTick(() => {
if (this.$refs.editForm) this.$refs.editForm.resetFields();
});
API_Member.getMemberGrade(row.id)
.then((res) => {
this.editDetailLoading = false;
if (res && res.success && res.result) {
const raw = res.result;
const grade = raw.grade != null ? raw.grade : raw;
const benefits = Array.isArray(raw.benefits) ? raw.benefits : [];
const orderedIds = benefits.length
? benefits.map((b) => b.id).filter((id) => id != null && id !== "")
: String(grade.benefitIds || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
this.fillEditFormFromGrade(grade, orderedIds);
} else {
this.fillEditFormFromGrade(row, this.parseBenefitIdsFromString(row.benefitIds));
}
})
.catch(() => {
this.editDetailLoading = false;
this.fillEditFormFromGrade(row, this.parseBenefitIdsFromString(row.benefitIds));
});
},
submitEdit() {
this.$refs.editForm.validate((valid) => {
if (!valid) return;
this.submitEditLoading = true;
const { id, benefitIds: _omit, ...rest } = this.formEdit;
const payload = {
...rest,
benefitIds: (this.editBenefitOrder || []).join(","),
};
API_Member.updateMemberGrade(id, payload).then((res) => {
this.submitEditLoading = false;
if (res && res.success) {
this.$Message.success("修改成功");
this.editFlag = false;
this.getData();
}
});
});
},
onGradeStateSwitch(row, checked) {
const nextState = checked ? "OPEN" : "CLOSE";
const prevState = row.gradeState;
if (nextState === prevState) return;
const text = checked ? "开启" : "关闭";
this.$Modal.confirm({
title: "提示",
content: `<p>确定${text}该客户等级?</p>`,
onOk: () => {
this.$set(row, "_gradeStateLoading", true);
return API_Member.updateMemberGradeState(row.id, nextState)
.then((res) => {
this.$set(row, "_gradeStateLoading", false);
if (res && res.success) {
this.$Message.success(`${text}成功`);
this.$set(row, "gradeState", nextState);
} else {
this.getData();
}
})
.catch(() => {
this.$set(row, "_gradeStateLoading", false);
});
},
});
},
remove(row) {
this.$Modal.confirm({
title: "提示",
content: "<p>确定删除该客户等级?</p>",
onOk: () => {
API_Member.deleteMemberGrade(row.id).then((res) => {
if (res && res.success) {
this.$Message.success("删除成功");
this.getData();
} else if (res && res.message) {
this.$Message.error(res.message);
}
});
},
});
},
},
mounted() {
this.init();
},
};
</script>