mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-06-21 17:40:25 +08:00
feat(会员管理): 添加第三方账户绑定和会员等级功能
- 在登录API中新增获取第三方账户绑定列表、绑定和解绑功能 - 在会员API中新增获取当前会员等级、等级规则和等级列表的接口 - 在会员中心页面中添加第三方账户绑定的UI和逻辑 - 新增会员等级页面,展示当前等级、经验值和经验值记录
This commit is contained in:
@@ -69,6 +69,32 @@ export function loginCallback (uuid) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getThirdAccountBindList () {
|
||||||
|
return request({
|
||||||
|
url: '/buyer/passport/connect/bind/list',
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindThirdAccount (data) {
|
||||||
|
return request({
|
||||||
|
url: '/buyer/passport/connect/bind',
|
||||||
|
method: Method.POST,
|
||||||
|
needToken: true,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unbindThirdAccount (data) {
|
||||||
|
return request({
|
||||||
|
url: '/buyer/passport/connect/bind/unbind',
|
||||||
|
method: Method.POST,
|
||||||
|
needToken: true,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 忘记密码 验证手机验证码
|
* 忘记密码 验证手机验证码
|
||||||
*/
|
*/
|
||||||
@@ -109,3 +135,12 @@ export function sCLogin(token,params) {
|
|||||||
params
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWechatH5QrCode (params) {
|
||||||
|
return request({
|
||||||
|
url: '/buyer/other/wechat/h5/qrcode',
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -489,6 +489,31 @@ export function memberPointHistory (params) {
|
|||||||
params
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function memberGradeCurrent (params) {
|
||||||
|
return request({
|
||||||
|
url: `/buyer/member/memberGrade/current`,
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function memberGradeRules () {
|
||||||
|
return request({
|
||||||
|
url: `/buyer/member/memberGrade/rules`,
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function memberGradeList () {
|
||||||
|
return request({
|
||||||
|
url: `/buyer/member/memberGrade/list`,
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true
|
||||||
|
})
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 分页获取会员站内信
|
* 分页获取会员站内信
|
||||||
* @param {Object} params 请求参数,包括pageNumber、pageSize、status
|
* @param {Object} params 请求参数,包括pageNumber、pageSize、status
|
||||||
|
|||||||
@@ -15,21 +15,112 @@
|
|||||||
<Button @click="modifyPwd">修改密码</Button>
|
<Button @click="modifyPwd">修改密码</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Row class="safeItem">
|
||||||
|
<Col :span="2">
|
||||||
|
<Icon size="40" type="md-link" />
|
||||||
|
</Col>
|
||||||
|
<Col :span="16">
|
||||||
|
<div class="setDivItem">第三方账户绑定</div>
|
||||||
|
<div class="setDivItem theme">绑定后可使用第三方账号快捷登录。</div>
|
||||||
|
<div class="connectList">
|
||||||
|
<div class="connectRow">
|
||||||
|
<div class="connectRowLeft">
|
||||||
|
<span class="connectName">{{ wechatConnect.label }}</span>
|
||||||
|
<Tag v-if="isBound(wechatConnect.type)" color="success">已绑定</Tag>
|
||||||
|
<Tag v-else>未绑定</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Spin size="large" fix v-if="connectLoading"></Spin>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="4">
|
||||||
|
<Button v-if="!isBound(wechatConnect.type)" type="primary" @click="openBindModal(wechatConnect)">绑定</Button>
|
||||||
|
<Button v-else @click="confirmUnbind(wechatConnect)">解绑</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
v-model="bindModalVisible"
|
||||||
|
title="绑定第三方账户"
|
||||||
|
:mask-closable="false"
|
||||||
|
:footer-hide="bindForm.type === 'WECHAT'"
|
||||||
|
:ok-text="bindModalOkText"
|
||||||
|
:loading="bindSubmitLoading"
|
||||||
|
@on-ok="submitBind"
|
||||||
|
@on-cancel="resetBindForm"
|
||||||
|
>
|
||||||
|
<Form :label-width="110">
|
||||||
|
<FormItem label="类型">
|
||||||
|
<Input v-model="bindForm.typeLabel" disabled />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem v-if="bindForm.type === 'WECHAT'" label="公众号二维码">
|
||||||
|
<div class="wechatQrBox">
|
||||||
|
<div class="wechatQrImgBox">
|
||||||
|
<Spin fix v-if="qrCodeLoading"></Spin>
|
||||||
|
<img v-if="qrCodeSrc" class="wechatQrImg" :src="qrCodeSrc" alt="微信公众号二维码" />
|
||||||
|
<div v-else class="wechatQrEmpty">二维码获取失败,请刷新重试</div>
|
||||||
|
</div>
|
||||||
|
<div class="wechatQrActions">
|
||||||
|
<Button @click="fetchWechatQrCode">刷新二维码</Button>
|
||||||
|
<Button type="primary" @click="submitBind">我已完成绑定</Button>
|
||||||
|
</div>
|
||||||
|
<div class="wechatQrTip">请使用微信扫码关注/授权,完成后点击“我已完成绑定”检测绑定状态</div>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem v-if="bindForm.type !== 'WECHAT'" label="OpenID/UnionID">
|
||||||
|
<Input v-model="bindForm.unionID" placeholder="请输入 OpenID/UnionID" />
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// import { getPwdStatus } from '@/api/account';
|
// import { getPwdStatus } from '@/api/account';
|
||||||
|
import storage from '@/plugins/storage'
|
||||||
|
import { bindThirdAccount, getThirdAccountBindList, getWechatH5QrCode, unbindThirdAccount } from '@/api/login.js'
|
||||||
export default {
|
export default {
|
||||||
name: 'AccountSafe',
|
name: 'AccountSafe',
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
pwdStatus: '' // 密码状态
|
pwdStatus: '',
|
||||||
|
connectLoading: false,
|
||||||
|
bindModalVisible: false,
|
||||||
|
bindSubmitLoading: false,
|
||||||
|
connectBoundTypes: [],
|
||||||
|
thirdAccountTypes: [
|
||||||
|
{ type: 'WECHAT', label: '微信' }
|
||||||
|
],
|
||||||
|
qrCodeLoading: false,
|
||||||
|
qrCodeBase64: '',
|
||||||
|
bindForm: {
|
||||||
|
type: '',
|
||||||
|
typeLabel: '',
|
||||||
|
unionID: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
wechatConnect () {
|
||||||
|
return this.thirdAccountTypes[0]
|
||||||
|
},
|
||||||
|
qrCodeSrc () {
|
||||||
|
if (!this.qrCodeBase64) return ''
|
||||||
|
if (this.qrCodeBase64.startsWith('data:image')) return this.qrCodeBase64
|
||||||
|
return `data:image/png;base64,${this.qrCodeBase64}`
|
||||||
|
},
|
||||||
|
bindModalOkText () {
|
||||||
|
if (this.bindForm.type === 'WECHAT') return '我已完成绑定'
|
||||||
|
return '确定'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.getPwdStatus()
|
this.getPwdStatus()
|
||||||
|
this.refreshConnectBinds()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 设置支付密码
|
// 设置支付密码
|
||||||
@@ -40,6 +131,7 @@ export default {
|
|||||||
this.$router.push({name: 'ModifyPwd', query: { status: 1 }})
|
this.$router.push({name: 'ModifyPwd', query: { status: 1 }})
|
||||||
},
|
},
|
||||||
// 获取密码状态
|
// 获取密码状态
|
||||||
|
getPwdStatus () {},
|
||||||
// getPwdStatus () {
|
// getPwdStatus () {
|
||||||
// getPwdStatus().then(res => {
|
// getPwdStatus().then(res => {
|
||||||
// if (res) {
|
// if (res) {
|
||||||
@@ -49,6 +141,124 @@ export default {
|
|||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
isBound (type) {
|
||||||
|
return this.connectBoundTypes.includes(type)
|
||||||
|
},
|
||||||
|
refreshConnectBinds () {
|
||||||
|
this.connectLoading = true
|
||||||
|
return getThirdAccountBindList()
|
||||||
|
.then(res => {
|
||||||
|
if (res && res.success) {
|
||||||
|
this.connectBoundTypes = Array.isArray(res.result) ? res.result : []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.connectLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openBindModal (item) {
|
||||||
|
this.bindForm.type = item.type
|
||||||
|
this.bindForm.typeLabel = item.label
|
||||||
|
this.bindForm.unionID = ''
|
||||||
|
this.qrCodeBase64 = ''
|
||||||
|
this.bindModalVisible = true
|
||||||
|
if (item.type === 'WECHAT') {
|
||||||
|
this.fetchWechatQrCode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetBindForm () {
|
||||||
|
this.bindForm.type = ''
|
||||||
|
this.bindForm.typeLabel = ''
|
||||||
|
this.bindForm.unionID = ''
|
||||||
|
this.qrCodeLoading = false
|
||||||
|
this.qrCodeBase64 = ''
|
||||||
|
this.bindSubmitLoading = false
|
||||||
|
},
|
||||||
|
fetchWechatQrCode () {
|
||||||
|
if (this.bindForm.type !== 'WECHAT') return
|
||||||
|
const rawUserInfo = storage.getItem('userInfo')
|
||||||
|
const userInfo = rawUserInfo ? JSON.parse(rawUserInfo) : null
|
||||||
|
const userId = userInfo && userInfo.id ? String(userInfo.id) : ''
|
||||||
|
if (!userId) {
|
||||||
|
this.qrCodeBase64 = ''
|
||||||
|
this.$Message.error('未获取到当前用户ID,请重新登录后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.qrCodeLoading = true
|
||||||
|
getWechatH5QrCode({ scene: userId })
|
||||||
|
.then(res => {
|
||||||
|
if (res && res.success) {
|
||||||
|
this.qrCodeBase64 = res.result || ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.qrCodeBase64 = ''
|
||||||
|
this.$Message.error((res && res.message) || '二维码获取失败')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.qrCodeBase64 = ''
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.qrCodeLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitBind () {
|
||||||
|
if (this.bindSubmitLoading) return
|
||||||
|
if (this.bindForm.type === 'WECHAT' && !this.bindForm.unionID) {
|
||||||
|
this.bindSubmitLoading = true
|
||||||
|
this.refreshConnectBinds()
|
||||||
|
.then(() => {
|
||||||
|
if (this.isBound(this.bindForm.type)) {
|
||||||
|
this.$Message.success('绑定成功')
|
||||||
|
this.bindModalVisible = false
|
||||||
|
this.resetBindForm()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$Message.warning('暂未检测到绑定,请扫码后重试')
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bindModalVisible = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.bindSubmitLoading = false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.bindForm.type) {
|
||||||
|
this.$Message.error('绑定类型不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.bindForm.unionID) {
|
||||||
|
this.$Message.error('OpenID/UnionID 不能为空')
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bindModalVisible = true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bindSubmitLoading = true
|
||||||
|
bindThirdAccount({ unionID: this.bindForm.unionID, type: this.bindForm.type })
|
||||||
|
.then(() => {
|
||||||
|
this.$Message.success('绑定成功')
|
||||||
|
this.bindModalVisible = false
|
||||||
|
this.resetBindForm()
|
||||||
|
this.refreshConnectBinds()
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.bindSubmitLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmUnbind (item) {
|
||||||
|
this.$Modal.confirm({
|
||||||
|
title: '解绑确认',
|
||||||
|
content: `<p>确定解绑 ${item.label} 吗?</p>`,
|
||||||
|
onOk: () => {
|
||||||
|
unbindThirdAccount({ type: item.type }).then(() => {
|
||||||
|
this.$Message.success('解绑成功')
|
||||||
|
this.refreshConnectBinds()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -76,4 +286,67 @@ export default {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connectList {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectRowLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectName {
|
||||||
|
color: $title_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechatQrBox {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechatQrImgBox {
|
||||||
|
position: relative;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border: 1px solid $border_color;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechatQrImg {
|
||||||
|
width: 168px;
|
||||||
|
height: 168px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechatQrEmpty {
|
||||||
|
color: $light_sub_color;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechatQrActions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechatQrTip {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: $light_sub_color;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
431
buyer/src/pages/home/memberCenter/MemberGrade.vue
Normal file
431
buyer/src/pages/home/memberCenter/MemberGrade.vue
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
<template>
|
||||||
|
<div class="memberGrade">
|
||||||
|
<card _Title="我的等级"/>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="gradeCard">
|
||||||
|
<div class="gradeLeft">
|
||||||
|
<img v-if="currentGrade.gradeImage" class="gradeIcon" :src="currentGrade.gradeImage" alt="等级图标" />
|
||||||
|
<div class="gradeInfo">
|
||||||
|
<div class="gradeNameRow">
|
||||||
|
<span class="gradeNameText">{{ currentGrade.gradeName || '暂无等级' }}</span>
|
||||||
|
<span v-if="currentGradeBenefits.length" class="benefitTagsInline">
|
||||||
|
<Tag v-for="b in currentGradeBenefits" :key="b.id" class="benefitTag" color="blue">{{ b.benefitName || '-' }}</Tag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="gradeExp">
|
||||||
|
当前经验值:<span class="num">{{ currentExperience || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="gradeNext" v-if="nextGrade">
|
||||||
|
距 {{ nextGrade.gradeName }} 还差 <span class="num">{{ diffExperience }}</span> 经验值
|
||||||
|
</div>
|
||||||
|
<div class="gradeNext" v-else>已达最高等级</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value="rules">
|
||||||
|
<TabPane label="获取经验值方式" name="rules">
|
||||||
|
<Table :columns="ruleColumns" :data="ruleList" :loading="rulesLoading">
|
||||||
|
<template slot-scope="{ row }" slot="ruleItem">
|
||||||
|
<div class="ruleItem">
|
||||||
|
<div class="ruleTop">
|
||||||
|
<div class="ruleName">
|
||||||
|
<span>{{ row.ruleName || row.ruleKey || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<Tag v-if="row.completed === true" color="success">已完成</Tag>
|
||||||
|
<Tag v-else color="default">未完成</Tag>
|
||||||
|
</div>
|
||||||
|
<div class="ruleDesc">{{ getRuleDesc(row.ruleKey) }}</div>
|
||||||
|
<div class="ruleMeta">
|
||||||
|
<span class="metaItem">单次获得:<b>+{{ row.value == null ? 0 : row.value }}</b></span>
|
||||||
|
<span v-if="row.maxValue != null && Number(row.maxValue) > 0" class="metaItem">限额:<b>{{ row.maxValue }}</b></span>
|
||||||
|
<span class="metaItem">已获得:<b>{{ row.gainedExperience == null ? 0 : row.gainedExperience }}</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<div class="rulesDesc" v-if="rulesDescription">{{ rulesDescription }}</div>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane label="经验值记录" name="logs">
|
||||||
|
<Table :columns="logColumns" :data="experienceLogs.records || []" :loading="logLoading">
|
||||||
|
<template slot-scope="{ row }" slot="rule">
|
||||||
|
<span>{{ getRuleName(row.ruleKey) }}</span>
|
||||||
|
</template>
|
||||||
|
<template slot-scope="{ row }" slot="delta">
|
||||||
|
<div :style="{color: getLogDeltaColor(row)}">
|
||||||
|
<span v-if="getLogDeltaSign(row) === '+'">+</span>{{ getLogDeltaAbsValue(row) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<Page
|
||||||
|
style="float:right;margin-top:10px"
|
||||||
|
:current="params.pageNumber"
|
||||||
|
:total="experienceLogs.total || 0"
|
||||||
|
:page-size="params.pageSize"
|
||||||
|
@on-change="changePage"
|
||||||
|
@on-page-size-change="changePageSize"
|
||||||
|
:page-size-opts="[10, 20, 50]"
|
||||||
|
size="small"
|
||||||
|
show-total
|
||||||
|
show-elevator
|
||||||
|
></Page>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { memberGradeCurrent, memberGradeList, memberGradeRules } from '@/api/member.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MemberGrade',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
ruleList: [],
|
||||||
|
rulesLoading: false,
|
||||||
|
rulesDescription: '',
|
||||||
|
/** 接口返回 List MemberGradeDetailVO,每项 { grade, benefits } */
|
||||||
|
gradeDetails: [],
|
||||||
|
gradeList: [],
|
||||||
|
currentGrade: {},
|
||||||
|
currentExperience: 0,
|
||||||
|
currentGradeId: '',
|
||||||
|
selectedRuleKey: '',
|
||||||
|
logLoading: false,
|
||||||
|
gradeLoading: false,
|
||||||
|
params: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
experienceLogs: {},
|
||||||
|
ruleColumns: [
|
||||||
|
{
|
||||||
|
title: '获取方式',
|
||||||
|
slot: 'ruleItem',
|
||||||
|
minWidth: 720
|
||||||
|
}
|
||||||
|
],
|
||||||
|
logColumns: [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
key: 'createTime',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '规则',
|
||||||
|
slot: 'rule',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '变动值',
|
||||||
|
slot: 'delta',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rulesMap () {
|
||||||
|
const map = {}
|
||||||
|
this.ruleList.forEach(item => {
|
||||||
|
map[item.ruleKey] = item.ruleName
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
},
|
||||||
|
sortedGradeList () {
|
||||||
|
const list = Array.isArray(this.gradeList) ? this.gradeList.slice() : []
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const sa = a && a.gradeSort != null ? Number(a.gradeSort) : 0
|
||||||
|
const sb = b && b.gradeSort != null ? Number(b.gradeSort) : 0
|
||||||
|
if (sa !== sb) return sa - sb
|
||||||
|
const ea = a && a.requiredExperience != null ? Number(a.requiredExperience) : 0
|
||||||
|
const eb = b && b.requiredExperience != null ? Number(b.requiredExperience) : 0
|
||||||
|
return ea - eb
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
},
|
||||||
|
currentGradeIndex () {
|
||||||
|
if (!this.currentGradeId) return -1
|
||||||
|
return this.sortedGradeList.findIndex(g => g && g.id === this.currentGradeId)
|
||||||
|
},
|
||||||
|
nextGrade () {
|
||||||
|
const idx = this.currentGradeIndex
|
||||||
|
if (idx < 0) return null
|
||||||
|
return this.sortedGradeList[idx + 1] || null
|
||||||
|
},
|
||||||
|
diffExperience () {
|
||||||
|
if (!this.nextGrade || this.nextGrade.requiredExperience == null) return 0
|
||||||
|
const diff = Number(this.nextGrade.requiredExperience) - Number(this.currentExperience || 0)
|
||||||
|
return diff > 0 ? diff : 0
|
||||||
|
},
|
||||||
|
/** 当前等级在列表详情中对应的启用权益(顺序与后台 benefits 一致) */
|
||||||
|
currentGradeBenefits () {
|
||||||
|
const id = this.currentGradeId
|
||||||
|
if (!id || !Array.isArray(this.gradeDetails) || !this.gradeDetails.length) return []
|
||||||
|
const detail = this.gradeDetails.find(
|
||||||
|
(d) => d && d.grade && String(d.grade.id) === String(id)
|
||||||
|
)
|
||||||
|
const list = detail && Array.isArray(detail.benefits) ? detail.benefits : []
|
||||||
|
return list.filter((b) => b && b.benefitState === 'OPEN')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init () {
|
||||||
|
this.loadRules()
|
||||||
|
this.loadGradeList()
|
||||||
|
this.loadCurrent()
|
||||||
|
},
|
||||||
|
loadRules () {
|
||||||
|
this.rulesLoading = true
|
||||||
|
memberGradeRules()
|
||||||
|
.then(res => {
|
||||||
|
if (res && res.success) {
|
||||||
|
const items = res.result && Array.isArray(res.result.items) ? res.result.items : []
|
||||||
|
this.rulesDescription = (res.result && res.result.description) || ''
|
||||||
|
this.ruleList = items
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.rulesLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadGradeList () {
|
||||||
|
this.gradeLoading = true
|
||||||
|
memberGradeList()
|
||||||
|
.then(res => {
|
||||||
|
if (res && res.success) {
|
||||||
|
const raw = Array.isArray(res.result) ? res.result : []
|
||||||
|
this.gradeDetails = raw
|
||||||
|
.map((item) => {
|
||||||
|
if (!item) return null
|
||||||
|
if (item.grade != null) {
|
||||||
|
return {
|
||||||
|
grade: item.grade,
|
||||||
|
benefits: Array.isArray(item.benefits) ? item.benefits : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { grade: item, benefits: [] }
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
this.gradeList = this.gradeDetails.map((d) => d.grade).filter(Boolean)
|
||||||
|
} else {
|
||||||
|
this.gradeDetails = []
|
||||||
|
this.gradeList = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.gradeLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadCurrent () {
|
||||||
|
this.logLoading = true
|
||||||
|
const params = {
|
||||||
|
pageNumber: this.params.pageNumber,
|
||||||
|
pageSize: this.params.pageSize
|
||||||
|
}
|
||||||
|
if (this.selectedRuleKey) params.ruleKey = this.selectedRuleKey
|
||||||
|
memberGradeCurrent(params)
|
||||||
|
.then(res => {
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
this.currentGrade = res.result.grade || {}
|
||||||
|
this.currentExperience = res.result.experience || 0
|
||||||
|
this.currentGradeId = res.result.gradeId || (this.currentGrade && this.currentGrade.id) || ''
|
||||||
|
this.experienceLogs = res.result.experienceLogs || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.logLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectRule (ruleKey) {
|
||||||
|
this.selectedRuleKey = ruleKey || ''
|
||||||
|
this.params.pageNumber = 1
|
||||||
|
this.loadCurrent()
|
||||||
|
},
|
||||||
|
changePage (val) {
|
||||||
|
this.params.pageNumber = val
|
||||||
|
this.loadCurrent()
|
||||||
|
},
|
||||||
|
changePageSize (val) {
|
||||||
|
this.params.pageSize = val
|
||||||
|
this.params.pageNumber = 1
|
||||||
|
this.loadCurrent()
|
||||||
|
},
|
||||||
|
getRuleName (ruleKey) {
|
||||||
|
if (!ruleKey) return '-'
|
||||||
|
return this.rulesMap[ruleKey] || ruleKey
|
||||||
|
},
|
||||||
|
getRuleDesc (ruleKey) {
|
||||||
|
const map = {
|
||||||
|
CONSUME: '客户消费1元可获取经验值,向下取整',
|
||||||
|
REGISTER: '会员注册成功后可获取经验值',
|
||||||
|
SIGN_IN: '客户每日签到后可获取的经验值',
|
||||||
|
COMMENT: '对已购买商品完成发表评价获取经验值;仅针对评论字数大于30字的评论进行发放',
|
||||||
|
SHARE: '会员分享商城页面可获取的经验值',
|
||||||
|
PROFILE: '完善个人基本信息可获得经验值,每个会员仅可获得一次',
|
||||||
|
FOLLOW_STORE: '关注店铺可获得经验值,每个客户ID同店铺仅第一次关注可进行获得',
|
||||||
|
BIND_WECHAT: '绑定微信成功后获得经验值,每个会员仅可获得一次',
|
||||||
|
ADD_ADDRESS: '添加收货地址可获得经验值,每个会员仅可获得一次',
|
||||||
|
SHARE_REGISTER: '仅被注册成功后可获得相应奖励经验值',
|
||||||
|
SHARE_BUY: '仅被购买成功后可获得相应奖励经验值'
|
||||||
|
}
|
||||||
|
if (!ruleKey) return '-'
|
||||||
|
return map[ruleKey] || '-'
|
||||||
|
},
|
||||||
|
getLogDeltaRaw (row) {
|
||||||
|
if (!row) return 0
|
||||||
|
const candidate = row.variableExperience != null ? row.variableExperience : (row.changeExperience != null ? row.changeExperience : (row.experience != null ? row.experience : row.value))
|
||||||
|
if (candidate == null) return 0
|
||||||
|
const num = Number(candidate)
|
||||||
|
if (Number.isNaN(num)) return 0
|
||||||
|
if (row.type === 'DECREASE') return -Math.abs(num)
|
||||||
|
if (row.type === 'INCREASE') return Math.abs(num)
|
||||||
|
return num
|
||||||
|
},
|
||||||
|
getLogDeltaSign (row) {
|
||||||
|
const num = this.getLogDeltaRaw(row)
|
||||||
|
return num >= 0 ? '+' : '-'
|
||||||
|
},
|
||||||
|
getLogDeltaAbsValue (row) {
|
||||||
|
const num = this.getLogDeltaRaw(row)
|
||||||
|
return Math.abs(num)
|
||||||
|
},
|
||||||
|
getLogDeltaColor (row) {
|
||||||
|
return this.getLogDeltaRaw(row) >= 0 ? 'green' : 'red'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 20px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rulesDesc {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid $border_color;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: $light_sub_color;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradeCard {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid $border_color;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradeLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradeIcon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradeNameRow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradeNameText {
|
||||||
|
font-size: 16px;
|
||||||
|
color: $light_title_color;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitTagsInline {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitTag {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradeExp,
|
||||||
|
.gradeNext {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num {
|
||||||
|
color: $theme_color;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruleItem {
|
||||||
|
padding: 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruleTop {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruleName {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: $light_title_color;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruleDesc {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: $light_sub_color;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruleMeta {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
color: $light_sub_color;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaItem b {
|
||||||
|
color: $theme_color;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -54,14 +54,40 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.formItem = JSON.parse(storage.getItem('userInfo'))
|
this.formItem = JSON.parse(storage.getItem('userInfo'))
|
||||||
|
this.formItem.birthday = this.normalizeBirthday(this.formItem.birthday)
|
||||||
this.accessToken.accessToken = storage.getItem('accessToken');
|
this.accessToken.accessToken = storage.getItem('accessToken');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
normalizeBirthday (value) {
|
||||||
|
if (!value) return ''
|
||||||
|
if (value instanceof Date) return value
|
||||||
|
if (typeof value === 'number') return new Date(value)
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const m = value.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||||
|
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
|
||||||
|
const d = new Date(value)
|
||||||
|
return isNaN(d.getTime()) ? '' : d
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
formatBirthday (value) {
|
||||||
|
if (!value) return null
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const m = value.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||||
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : null
|
||||||
|
}
|
||||||
|
const d = value instanceof Date ? value : new Date(value)
|
||||||
|
if (isNaN(d.getTime())) return null
|
||||||
|
const yyyy = d.getFullYear()
|
||||||
|
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${yyyy}-${mm}-${dd}`
|
||||||
|
},
|
||||||
save () { // 保存
|
save () { // 保存
|
||||||
this.$refs.form.validate(valid => {
|
this.$refs.form.validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
let params = {
|
let params = {
|
||||||
birthday: this.$options.filters.unixToDate(this.formItem.birthday / 1000, 'yyyy-MM-dd'),
|
birthday: this.formatBirthday(this.formItem.birthday),
|
||||||
face: this.formItem.face,
|
face: this.formItem.face,
|
||||||
nickName: this.formItem.nickName,
|
nickName: this.formItem.nickName,
|
||||||
sex: this.formItem.sex
|
sex: this.formItem.sex
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ const member = [{
|
|||||||
icon: '',
|
icon: '',
|
||||||
title: '我的积分',
|
title: '我的积分',
|
||||||
path: 'Point'
|
path: 'Point'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '',
|
||||||
|
title: '我的等级',
|
||||||
|
path: 'MemberGrade'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
display: true
|
display: true
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ const ComplainDetail = (resolve) =>
|
|||||||
require(["@/pages/home/memberCenter/ComplainDetail"], resolve);
|
require(["@/pages/home/memberCenter/ComplainDetail"], resolve);
|
||||||
const Point = (resolve) =>
|
const Point = (resolve) =>
|
||||||
require(["@/pages/home/memberCenter/Point"], resolve);
|
require(["@/pages/home/memberCenter/Point"], resolve);
|
||||||
|
const MemberGrade = (resolve) =>
|
||||||
|
require(["@/pages/home/memberCenter/MemberGrade"], resolve);
|
||||||
const MsgList = (resolve) =>
|
const MsgList = (resolve) =>
|
||||||
require(["@/pages/home/memberCenter/memberMsg/MsgList"], resolve);
|
require(["@/pages/home/memberCenter/memberMsg/MsgList"], resolve);
|
||||||
const MsgDetail = (resolve) =>
|
const MsgDetail = (resolve) =>
|
||||||
@@ -326,6 +328,12 @@ export default new Router({
|
|||||||
component: Point,
|
component: Point,
|
||||||
meta: { title: "我的积分" },
|
meta: { title: "我的积分" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "MemberGrade",
|
||||||
|
name: "MemberGrade",
|
||||||
|
component: MemberGrade,
|
||||||
|
meta: { title: "我的等级" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "Profile",
|
path: "Profile",
|
||||||
name: "Profile",
|
name: "Profile",
|
||||||
|
|||||||
@@ -175,3 +175,56 @@ export const addMemberGroupUsers = (groupId, memberIds) => {
|
|||||||
memberIds: Array.isArray(memberIds) ? memberIds.join(",") : memberIds,
|
memberIds: Array.isArray(memberIds) ? memberIds.join(",") : memberIds,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 客户等级
|
||||||
|
export const getMemberGradeByPage = (params) => {
|
||||||
|
return getRequest("/member/memberGrade/getByPage");
|
||||||
|
};
|
||||||
|
export const getMemberGrade = (id) => {
|
||||||
|
return getRequest(`/member/memberGrade/get/${id}`);
|
||||||
|
};
|
||||||
|
export const addMemberGrade = (params) => {
|
||||||
|
return postRequest(`/member/memberGrade`, params);
|
||||||
|
};
|
||||||
|
export const updateMemberGrade = (id, params) => {
|
||||||
|
return putRequest(`/member/memberGrade/update/${id}`, params);
|
||||||
|
};
|
||||||
|
export const updateMemberGradeState = (id, state) => {
|
||||||
|
return putRequest(`/member/memberGrade/state/${id}?state=${state}`);
|
||||||
|
};
|
||||||
|
export const deleteMemberGrade = (id) => {
|
||||||
|
return deleteRequest(`/member/memberGrade/delete/${id}`);
|
||||||
|
};
|
||||||
|
export const getMemberExperienceByPage = (params) => {
|
||||||
|
return getRequest(`/member/memberGrade/experience/getByPage`, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 客户权益
|
||||||
|
export const getMemberBenefitByPage = (params) => {
|
||||||
|
return getRequest("/member/benefit/getByPage", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMemberBenefit = (id) => {
|
||||||
|
return getRequest(`/member/benefit/get/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addMemberBenefit = (params) => {
|
||||||
|
return postRequest("/member/benefit", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMemberBenefit = (id, params) => {
|
||||||
|
return putRequest(`/member/benefit/update/${id}`, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMemberBenefitState = (id, state) => {
|
||||||
|
return putRequest(`/member/benefit/state/${id}?state=${state}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMemberBenefit = (id) => {
|
||||||
|
return deleteRequest(`/member/benefit/delete/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 客户权益类型枚举(管理端) */
|
||||||
|
export const getMemberBenefitTypes = () => {
|
||||||
|
return getRequest("/member/benefit/types");
|
||||||
|
};
|
||||||
|
|||||||
@@ -39,14 +39,20 @@
|
|||||||
<!--</Upload>-->
|
<!--</Upload>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal title="图片预览" v-model="viewImage" :styles="{top: '30px'}" draggable>
|
<Modal
|
||||||
|
title="图片预览"
|
||||||
|
v-model="viewImage"
|
||||||
|
:styles="{ top: '30px' }"
|
||||||
|
:z-index="3500"
|
||||||
|
draggable
|
||||||
|
>
|
||||||
<img :src="currentValue" alt="该资源不存在" style="max-width: 300px;margin: 0 auto;display: block;" />
|
<img :src="currentValue" alt="该资源不存在" style="max-width: 300px;margin: 0 auto;display: block;" />
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
<Button @click="viewImage=false">关闭</Button>
|
<Button @click="viewImage=false">关闭</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal width="1200px" v-model="picModalFlag">
|
<Modal width="1200px" v-model="picModalFlag" :z-index="3500">
|
||||||
<ossManage @callback="callbackSelected" ref="ossManage" :isComponent="true" :initialize="picModalFlag" />
|
<ossManage @callback="callbackSelected" ref="ossManage" :isComponent="true" :initialize="picModalFlag" />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const otherRouter = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "category",
|
path: "category",
|
||||||
title: "类型列表",
|
title: "分类列表",
|
||||||
name: "category",
|
name: "category",
|
||||||
component: () => import("@/views/goods/goods-manage/category.vue")
|
component: () => import("@/views/goods/goods-manage/category.vue")
|
||||||
},
|
},
|
||||||
@@ -105,16 +105,10 @@ export const otherRouter = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "goods-parameter-edit",
|
path: "goods-parameter-edit",
|
||||||
title: "商品参数维护",
|
title: "编辑商品参数",
|
||||||
name: "goods-parameter-edit",
|
name: "goods-parameter-edit",
|
||||||
component: () => import("@/views/goods/goods-manage/parameter-edit.vue")
|
component: () => import("@/views/goods/goods-manage/parameter-edit.vue")
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "goods-spec",
|
|
||||||
title: "商品参数",
|
|
||||||
name: "goods-spec",
|
|
||||||
component: () => import("@/views/goods/goods-manage/spec.vue")
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "order-complaint-detail",
|
path: "order-complaint-detail",
|
||||||
title: "投诉详情",
|
title: "投诉详情",
|
||||||
@@ -159,6 +153,30 @@ export const otherRouter = {
|
|||||||
name: "member-group",
|
name: "member-group",
|
||||||
component: () => import("@/views/member/group/index.vue")
|
component: () => import("@/views/member/group/index.vue")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "member-grade",
|
||||||
|
title: "客户等级",
|
||||||
|
name: "member-grade",
|
||||||
|
component: () => import("@/views/member/grade/index.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "member-benefit",
|
||||||
|
title: "客户权益管理",
|
||||||
|
name: "member-benefit",
|
||||||
|
component: () => import("@/views/member/benefit/index.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "member-grade-experience",
|
||||||
|
title: "等级权益设置",
|
||||||
|
name: "member-grade-experience",
|
||||||
|
component: () => import("@/views/member/grade/experience-setting.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "member-grade-experience-log",
|
||||||
|
title: "客户经验值记录",
|
||||||
|
name: "member-grade-experience-log",
|
||||||
|
component: () => import("@/views/member/grade/experience-log.vue")
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -85,3 +85,36 @@ export function memberPromotionsStatusRender(h, status) {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台优惠券「活动时间 / 有效期」文案(活动获取券:effectiveDays 为领取后有效天数)
|
||||||
|
*/
|
||||||
|
export function formatPromotionCouponValidityHtml(row) {
|
||||||
|
if (!row) return "-";
|
||||||
|
if (row.getType === "ACTIVITY") {
|
||||||
|
const days = row.effectiveDays;
|
||||||
|
if (days !== undefined && days !== null && days !== "") {
|
||||||
|
const n = Number(days);
|
||||||
|
if (!Number.isNaN(n) && n > 0) {
|
||||||
|
return `领取后${n}天有效`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row.startTime && row.endTime) {
|
||||||
|
return `${row.startTime}<br/>${row.endTime}`;
|
||||||
|
}
|
||||||
|
if (row.getType === "ACTIVITY" && row.rangeDayType === "DYNAMICTIME") {
|
||||||
|
return "长期有效";
|
||||||
|
}
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Table 列 render:活动时间(与 formatPromotionCouponValidityHtml 一致) */
|
||||||
|
export function promotionsCouponActivityTimeRender(h, params) {
|
||||||
|
const html = formatPromotionCouponValidityHtml(params.row);
|
||||||
|
if (html === "-") return h("div", "-");
|
||||||
|
if (html.indexOf("<br/>") !== -1) {
|
||||||
|
return h("div", { domProps: { innerHTML: html } });
|
||||||
|
}
|
||||||
|
return h("div", html);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,9 +15,6 @@
|
|||||||
>
|
>
|
||||||
<template slot="action" slot-scope="scope">
|
<template slot="action" slot-scope="scope">
|
||||||
<div class="ops">
|
<div class="ops">
|
||||||
<template v-if="scope.row.level == 2">
|
|
||||||
<a class="ops-link" @click="parameterOperation(scope.row)">编辑绑定参数</a>
|
|
||||||
</template>
|
|
||||||
<a class="ops-link" @click="edit(scope.row)">编辑</a>
|
<a class="ops-link" @click="edit(scope.row)">编辑</a>
|
||||||
<a class="ops-link" @click="remove(scope.row)">删除</a>
|
<a class="ops-link" @click="remove(scope.row)">删除</a>
|
||||||
<a v-show="scope.row.level != 2" class="ops-link" @click="addChildren(scope.row)">添加子分类</a>
|
<a v-show="scope.row.level != 2" class="ops-link" @click="addChildren(scope.row)">添加子分类</a>
|
||||||
@@ -340,10 +337,6 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// 编辑绑定参数
|
|
||||||
parameterOperation(v) {
|
|
||||||
this.$router.push({ name: "parameter", query: { id: v.id } });
|
|
||||||
},
|
|
||||||
// 添加子分类
|
// 添加子分类
|
||||||
addChildren(v) {
|
addChildren(v) {
|
||||||
this.modalType = 0;
|
this.modalType = 0;
|
||||||
|
|||||||
@@ -1,341 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="search">
|
|
||||||
<Card>
|
|
||||||
<Form @submit.native.prevent @keydown.enter.native="handleSearch" ref="searchForm" :model="searchForm" inline :label-width="70"
|
|
||||||
class="search-form">
|
|
||||||
<Form-item label="规格名称" prop="specName">
|
|
||||||
<Input type="text" v-model="searchForm.specName" placeholder="请输入规格名称" clearable style="width: 240px" />
|
|
||||||
</Form-item>
|
|
||||||
<Button @click="handleSearch" type="primary" class="search-btn">搜索</Button>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<Row class="operation padding-row">
|
|
||||||
<Button @click="add" type="primary">添加</Button>
|
|
||||||
<Button @click="delAll">批量删除</Button>
|
|
||||||
</Row>
|
|
||||||
<Table :loading="loading" border :columns="columns" :data="data" ref="table" sortable="custom"
|
|
||||||
@on-sort-change="changeSort" @on-selection-change="changeSelect">
|
|
||||||
</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>
|
|
||||||
<Modal :title="modalTitle" v-model="modalVisible" :mask-closable="false" :width="500">
|
|
||||||
<Form ref="form" :model="form" :label-width="100" :rules="formValidate">
|
|
||||||
<FormItem label="规格名称" prop="specName">
|
|
||||||
<Input v-model="form.specName" maxlength="30" clearable style="width: 100%" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="规格值" prop="specValue">
|
|
||||||
<Select v-model="form.specValue" placeholder="输入后回车添加" multiple filterable allow-create
|
|
||||||
:popper-append-to-body="false" popper-class="spec-values-popper"
|
|
||||||
style="width: 100%; text-align: left; margin-right: 10px" @on-create="handleCreate2">
|
|
||||||
<Option v-for="item in specValue" :value="item" :label="item" :key="item">
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
<div slot="footer">
|
|
||||||
<Button type="text" @click="modalVisible = false">取消</Button>
|
|
||||||
<Button type="primary" :loading="submitLoading" @click="saveSpec">提交</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { getSpecListData, insertSpec, updateSpec, delSpec } from "@/api/goods";
|
|
||||||
|
|
||||||
import { regular } from "@/utils";
|
|
||||||
export default {
|
|
||||||
name: "spec",
|
|
||||||
components: {},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
loading: true, // 表单加载状态
|
|
||||||
modalType: 0, // 添加或编辑标识
|
|
||||||
modalVisible: false, // 添加或编辑显示
|
|
||||||
modalTitle: "", // 添加或编辑标题
|
|
||||||
searchForm: {
|
|
||||||
// 搜索框初始化对象
|
|
||||||
pageNumber: 1, // 当前页数
|
|
||||||
pageSize: 20, // 页面大小
|
|
||||||
sort: "createTime", // 默认排序字段
|
|
||||||
order: "asc", // 默认排序方式
|
|
||||||
},
|
|
||||||
// 表单验证规则
|
|
||||||
formValidate: {
|
|
||||||
specName: [
|
|
||||||
regular.REQUIRED,
|
|
||||||
// regular.VARCHAR20
|
|
||||||
],
|
|
||||||
specValue: [regular.REQUIRED, regular.VARCHAR255],
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
// 添加或编辑表单对象初始化数据
|
|
||||||
specName: "",
|
|
||||||
specValue: "",
|
|
||||||
},
|
|
||||||
/** 编辑规格值 */
|
|
||||||
specValue: [],
|
|
||||||
submitLoading: false, // 添加或编辑提交状态
|
|
||||||
selectList: [], // 多选数据
|
|
||||||
selectCount: 0, // 多选计数
|
|
||||||
columns: [
|
|
||||||
// 表头
|
|
||||||
{
|
|
||||||
type: "selection",
|
|
||||||
width: 60,
|
|
||||||
align: "center",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "规格名称",
|
|
||||||
key: "specName",
|
|
||||||
width: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "规格值",
|
|
||||||
key: "specValue",
|
|
||||||
minWidth: 250,
|
|
||||||
tooltip: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
key: "action",
|
|
||||||
align: "center",
|
|
||||||
fixed: "right",
|
|
||||||
width: 250,
|
|
||||||
render: (h, params) => {
|
|
||||||
return h("div", [
|
|
||||||
h(
|
|
||||||
"a",
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
color: "#2d8cf0",
|
|
||||||
cursor: "pointer",
|
|
||||||
textDecoration: "none",
|
|
||||||
marginRight: "5px",
|
|
||||||
},
|
|
||||||
on: {
|
|
||||||
click: () => {
|
|
||||||
this.edit(params.row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"编辑"
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
"span",
|
|
||||||
{ style: { margin: "0 8px", color: "#dcdee2" } },
|
|
||||||
"|"
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
"a",
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
color: "#2d8cf0",
|
|
||||||
cursor: "pointer",
|
|
||||||
textDecoration: "none",
|
|
||||||
},
|
|
||||||
on: {
|
|
||||||
click: () => {
|
|
||||||
this.remove(params.row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"删除"
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
data: [], // 表单数据
|
|
||||||
total: 0, // 表单数据总数
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleCreate2 (v) {
|
|
||||||
console.log(v);
|
|
||||||
},
|
|
||||||
//初始化,获取数据
|
|
||||||
init () {
|
|
||||||
this.getDataList();
|
|
||||||
},
|
|
||||||
//修改分页
|
|
||||||
changePage (v) {
|
|
||||||
this.searchForm.pageNumber = v;
|
|
||||||
this.getDataList();
|
|
||||||
this.clearSelectAll();
|
|
||||||
},
|
|
||||||
//修改页面大小
|
|
||||||
changePageSize (v) {
|
|
||||||
this.searchForm.pageSize = v;
|
|
||||||
this.getDataList();
|
|
||||||
},
|
|
||||||
//搜索参数
|
|
||||||
handleSearch () {
|
|
||||||
this.searchForm.pageNumber = 1;
|
|
||||||
this.searchForm.pageSize = 20;
|
|
||||||
this.getDataList();
|
|
||||||
},
|
|
||||||
//重置搜索参数
|
|
||||||
handleReset () {
|
|
||||||
this.$refs.searchForm.resetFields();
|
|
||||||
this.searchForm.pageNumber = 1;
|
|
||||||
this.searchForm.pageSize = 20;
|
|
||||||
// 重新加载数据
|
|
||||||
this.getDataList();
|
|
||||||
},
|
|
||||||
//更改排序
|
|
||||||
changeSort (e) {
|
|
||||||
this.searchForm.sort = e.key;
|
|
||||||
this.searchForm.order = e.order;
|
|
||||||
if (e.order === "normal") {
|
|
||||||
this.searchForm.order = "";
|
|
||||||
}
|
|
||||||
this.getDataList();
|
|
||||||
},
|
|
||||||
//清除已选择
|
|
||||||
clearSelectAll () {
|
|
||||||
this.$refs.table.selectAll(false);
|
|
||||||
},
|
|
||||||
//修改已选择
|
|
||||||
changeSelect (e) {
|
|
||||||
this.selectList = e;
|
|
||||||
this.selectCount = e.length;
|
|
||||||
},
|
|
||||||
//获取数据
|
|
||||||
getDataList () {
|
|
||||||
this.loading = true;
|
|
||||||
// 带多条件搜索参数获取表单数据 请自行修改接口
|
|
||||||
getSpecListData(this.searchForm).then((res) => {
|
|
||||||
this.loading = false;
|
|
||||||
if (res.success) {
|
|
||||||
this.data = res.result.records;
|
|
||||||
this.total = res.result.total;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
//新增规格
|
|
||||||
saveSpec () {
|
|
||||||
this.$refs.form.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
this.submitLoading = true;
|
|
||||||
if (this.modalType === 0) {
|
|
||||||
if (this.data.find((item) => item.specName == this.form.specName)) {
|
|
||||||
this.$Message.error("请勿添加重复规格名称!");
|
|
||||||
this.submitLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 添加 避免编辑后传入id等数据
|
|
||||||
delete this.form.id;
|
|
||||||
insertSpec(this.form).then((res) => {
|
|
||||||
this.submitLoading = false;
|
|
||||||
if (res.success) {
|
|
||||||
this.$Message.success("操作成功");
|
|
||||||
this.getDataList();
|
|
||||||
this.modalVisible = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 编辑
|
|
||||||
updateSpec(this.form.id, this.form).then((res) => {
|
|
||||||
this.submitLoading = false;
|
|
||||||
if (res.success) {
|
|
||||||
this.$Message.success("操作成功");
|
|
||||||
this.getDataList();
|
|
||||||
this.modalVisible = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//弹出添加框
|
|
||||||
add () {
|
|
||||||
this.modalType = 0;
|
|
||||||
this.modalTitle = "添加";
|
|
||||||
this.$refs.form.resetFields();
|
|
||||||
this.specValue = "";
|
|
||||||
delete this.form.id;
|
|
||||||
this.modalVisible = true;
|
|
||||||
},
|
|
||||||
//弹出编辑框
|
|
||||||
edit (v) {
|
|
||||||
this.modalType = 1;
|
|
||||||
this.modalTitle = "编辑";
|
|
||||||
// 转换null为""
|
|
||||||
for (let attr in v) {
|
|
||||||
if (v[attr] === null) {
|
|
||||||
v[attr] = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let localVal = v.specValue;
|
|
||||||
this.form.specName = v.specName;
|
|
||||||
this.form.id = v.id;
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$set(this.form, 'specValue', v.specValue);
|
|
||||||
})
|
|
||||||
|
|
||||||
if (localVal && localVal.indexOf("," > 0)) {
|
|
||||||
this.$set(this.form, 'specValue', localVal.split(","));
|
|
||||||
this.specValue = this.form.specValue;
|
|
||||||
} else {
|
|
||||||
this.specValue = [];
|
|
||||||
}
|
|
||||||
this.modalVisible = true;
|
|
||||||
},
|
|
||||||
// 删除规格
|
|
||||||
remove (v) {
|
|
||||||
this.$Modal.confirm({
|
|
||||||
title: "确认删除",
|
|
||||||
content: "您确认要删除 " + v.specName + " ?",
|
|
||||||
loading: true,
|
|
||||||
onOk: () => {
|
|
||||||
delSpec(v.id).then((res) => {
|
|
||||||
this.$Modal.remove();
|
|
||||||
if (res.success) {
|
|
||||||
this.$Message.success("操作成功");
|
|
||||||
this.getDataList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 批量删除
|
|
||||||
delAll () {
|
|
||||||
if (this.selectCount <= 0) {
|
|
||||||
this.$Message.warning("您还未选择要删除的数据");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$Modal.confirm({
|
|
||||||
title: "确认删除",
|
|
||||||
content: "您确认要删除所选的 " + this.selectCount + " 条数据?",
|
|
||||||
loading: true,
|
|
||||||
onOk: () => {
|
|
||||||
let ids = "";
|
|
||||||
this.selectList.forEach(function (e) {
|
|
||||||
ids += e.id + ",";
|
|
||||||
});
|
|
||||||
ids = ids.substring(0, ids.length - 1);
|
|
||||||
delSpec(ids).then((res) => {
|
|
||||||
this.$Modal.remove();
|
|
||||||
if (res.success) {
|
|
||||||
this.$Message.success("删除成功");
|
|
||||||
this.clearSelectAll();
|
|
||||||
this.searchForm.pageNumber = 1;
|
|
||||||
this.getDataList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<Menu
|
<Menu
|
||||||
ref="childrenMenu"
|
ref="childrenMenu"
|
||||||
:active-name="$route.name"
|
:active-name="$route.name"
|
||||||
width="100px"
|
width="120px"
|
||||||
@on-select="changeMenu"
|
@on-select="changeMenu"
|
||||||
>
|
>
|
||||||
<template v-for="item in menuList">
|
<template v-for="item in menuList">
|
||||||
@@ -71,7 +71,7 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.ivu-shrinkable-menu{
|
.ivu-shrinkable-menu{
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
width: 180px;
|
width: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: block;
|
display: block;
|
||||||
padding-left: 180px;
|
padding-left: 200px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
@@ -218,12 +218,12 @@
|
|||||||
.single-page-con {
|
.single-page-con {
|
||||||
min-width: 740px;
|
min-width: 740px;
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 180px;
|
left: 220px;
|
||||||
top: 100px;
|
top: 100px;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: calc(100% - 110px);
|
height: calc(100% - 110px);
|
||||||
width: calc(100% - 180px);
|
width: calc(100% - 220px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
992
manager/src/views/member/benefit/index.vue
Normal file
992
manager/src/views/member/benefit/index.vue
Normal file
@@ -0,0 +1,992 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search member-benefit">
|
||||||
|
<Card>
|
||||||
|
<Row class="operation padding-row">
|
||||||
|
<Button type="primary" @click="openAdd">添加权益设置</Button>
|
||||||
|
</Row>
|
||||||
|
<Table
|
||||||
|
:loading="loading"
|
||||||
|
stripe
|
||||||
|
:columns="columns"
|
||||||
|
:data="data"
|
||||||
|
no-data-text="暂无数据"
|
||||||
|
class="mt_10 benefit-list-table benefit-list-table--no-vertical-borders"
|
||||||
|
></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, 30, 50]"
|
||||||
|
size="small"
|
||||||
|
show-total
|
||||||
|
show-elevator
|
||||||
|
show-sizer
|
||||||
|
></Page>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 平台优惠券选择(与「添加优惠券活动」coupon-publish 相同组件) -->
|
||||||
|
<Modal
|
||||||
|
v-model="couponPickerVisible"
|
||||||
|
title="选择优惠券"
|
||||||
|
width="80%"
|
||||||
|
:z-index="3000"
|
||||||
|
:mask-closable="false"
|
||||||
|
@on-ok="handleCouponPickerClose"
|
||||||
|
@on-cancel="handleCouponPickerClose"
|
||||||
|
>
|
||||||
|
<couponTemplate
|
||||||
|
v-if="couponPickerVisible"
|
||||||
|
:selectedList="couponPickerSelectedList"
|
||||||
|
getType="ACTIVITY"
|
||||||
|
promotionStatus="START"
|
||||||
|
@selected="onCouponTemplateSelected"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
v-model="addFlag"
|
||||||
|
title="添加权益设置"
|
||||||
|
width="1120"
|
||||||
|
placement="right"
|
||||||
|
:z-index="950"
|
||||||
|
:mask-closable="false"
|
||||||
|
:closable="true"
|
||||||
|
scrollable
|
||||||
|
class-name="benefit-form-drawer"
|
||||||
|
>
|
||||||
|
<Form ref="addForm" :model="formAdd" :rules="rulesAdd" :label-width="110">
|
||||||
|
<FormItem label="权益类型" prop="benefitType">
|
||||||
|
<Select
|
||||||
|
v-model="formAdd.benefitType"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择权益类型"
|
||||||
|
style="width: 100%"
|
||||||
|
@on-change="onAddBenefitTypeChange"
|
||||||
|
>
|
||||||
|
<Option v-for="item in benefitTypeOptions" :key="item.value" :value="item.value">{{ item.description }}</Option>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-show="formAdd.benefitType === 'GIFT_POINT'"
|
||||||
|
label="赠送积分"
|
||||||
|
prop="giftPoint"
|
||||||
|
:required="formAdd.benefitType === 'GIFT_POINT'"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
v-model="formAdd.giftPoint"
|
||||||
|
:min="0"
|
||||||
|
:max="99999"
|
||||||
|
:precision="0"
|
||||||
|
style="width: 220px"
|
||||||
|
placeholder="必填,范围 0-99999"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-show="formAdd.benefitType === 'COUPON_PACKAGE'"
|
||||||
|
label="赠送优惠券"
|
||||||
|
prop="couponPackageRows"
|
||||||
|
:required="formAdd.benefitType === 'COUPON_PACKAGE'"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Button type="dashed" icon="ios-add" class="mb_10" @click="openCouponPicker('add')">添加优惠券</Button>
|
||||||
|
<Table
|
||||||
|
border
|
||||||
|
size="small"
|
||||||
|
:columns="couponPackageTableColumns"
|
||||||
|
:data="formAdd.couponPackageRows"
|
||||||
|
no-data-text="请添加优惠券"
|
||||||
|
></Table>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="权益名称" prop="benefitName">
|
||||||
|
<Input v-model="formAdd.benefitName" maxlength="50" placeholder="请输入权益名称" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="权益LOGO" prop="benefitLogo">
|
||||||
|
<div class="benefit-logo-field">
|
||||||
|
<div v-if="formAdd.benefitLogo" class="benefit-logo-thumb">
|
||||||
|
<img :src="formAdd.benefitLogo" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="benefit-logo-upload">
|
||||||
|
<upload-pic-input v-model="formAdd.benefitLogo" placeholder="请上传权益 LOGO 图片地址" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="权益介绍" prop="benefitDesc">
|
||||||
|
<Input v-model="formAdd.benefitDesc" type="textarea" :rows="4" maxlength="500" placeholder="请输入权益介绍" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="启用状态" prop="benefitState">
|
||||||
|
<RadioGroup v-model="formAdd.benefitState">
|
||||||
|
<Radio label="OPEN">开启</Radio>
|
||||||
|
<Radio label="CLOSE">关闭</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="排序" prop="benefitSort">
|
||||||
|
<InputNumber v-model="formAdd.benefitSort" :min="1" :max="9999" :precision="0" style="width: 220px" />
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<div class="benefit-drawer-footer-btns">
|
||||||
|
<Button @click="addFlag = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="submitAddLoading" @click="submitAdd">提交</Button>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
v-model="editFlag"
|
||||||
|
title="编辑权益设置"
|
||||||
|
width="1120"
|
||||||
|
placement="right"
|
||||||
|
:z-index="950"
|
||||||
|
:mask-closable="false"
|
||||||
|
:closable="true"
|
||||||
|
scrollable
|
||||||
|
class-name="benefit-form-drawer"
|
||||||
|
>
|
||||||
|
<Form ref="editForm" :model="formEdit" :rules="rulesEdit" :label-width="110">
|
||||||
|
<Input v-model="formEdit.id" v-show="false" />
|
||||||
|
<FormItem label="权益类型" prop="benefitType">
|
||||||
|
<Select
|
||||||
|
v-model="formEdit.benefitType"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择权益类型"
|
||||||
|
style="width: 100%"
|
||||||
|
@on-change="onEditBenefitTypeChange"
|
||||||
|
>
|
||||||
|
<Option v-for="item in benefitTypeOptions" :key="item.value" :value="item.value">{{ item.description }}</Option>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-show="formEdit.benefitType === 'GIFT_POINT'"
|
||||||
|
label="赠送积分"
|
||||||
|
prop="giftPoint"
|
||||||
|
:required="formEdit.benefitType === 'GIFT_POINT'"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
v-model="formEdit.giftPoint"
|
||||||
|
:min="0"
|
||||||
|
:max="99999"
|
||||||
|
:precision="0"
|
||||||
|
style="width: 220px"
|
||||||
|
placeholder="必填,范围 0-99999"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-show="formEdit.benefitType === 'COUPON_PACKAGE'"
|
||||||
|
label="赠送优惠券"
|
||||||
|
prop="couponPackageRows"
|
||||||
|
:required="formEdit.benefitType === 'COUPON_PACKAGE'"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Button type="dashed" icon="ios-add" class="mb_10" @click="openCouponPicker('edit')">添加优惠券</Button>
|
||||||
|
<Table
|
||||||
|
border
|
||||||
|
size="small"
|
||||||
|
:columns="couponPackageTableColumnsEdit"
|
||||||
|
:data="formEdit.couponPackageRows"
|
||||||
|
no-data-text="请添加优惠券"
|
||||||
|
></Table>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="权益名称" prop="benefitName">
|
||||||
|
<Input v-model="formEdit.benefitName" maxlength="50" placeholder="请输入权益名称" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="权益LOGO" prop="benefitLogo">
|
||||||
|
<div class="benefit-logo-field">
|
||||||
|
<div v-if="formEdit.benefitLogo" class="benefit-logo-thumb">
|
||||||
|
<img :src="formEdit.benefitLogo" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="benefit-logo-upload">
|
||||||
|
<upload-pic-input v-model="formEdit.benefitLogo" placeholder="请上传权益 LOGO 图片地址" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="权益介绍" prop="benefitDesc">
|
||||||
|
<Input v-model="formEdit.benefitDesc" type="textarea" :rows="4" maxlength="500" placeholder="请输入权益介绍" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="启用状态" prop="benefitState">
|
||||||
|
<RadioGroup v-model="formEdit.benefitState">
|
||||||
|
<Radio label="OPEN">开启</Radio>
|
||||||
|
<Radio label="CLOSE">关闭</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="排序" prop="benefitSort">
|
||||||
|
<InputNumber v-model="formEdit.benefitSort" :min="1" :max="9999" :precision="0" style="width: 220px" />
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<div class="benefit-drawer-footer-btns">
|
||||||
|
<Button @click="editFlag = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="submitEditLoading" @click="submitEdit">提交</Button>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Member from "@/api/member.js";
|
||||||
|
import { getPlatformCoupon } from "@/api/promotion";
|
||||||
|
import { formatPromotionCouponValidityHtml } from "@/utils/promotions";
|
||||||
|
import couponTemplate from "@/views/promotions/coupon/coupon.vue";
|
||||||
|
import uploadPicInput from "@/components/lili/upload-pic-input.vue";
|
||||||
|
|
||||||
|
const GIFT_POINT = "GIFT_POINT";
|
||||||
|
const COUPON_PACKAGE = "COUPON_PACKAGE";
|
||||||
|
|
||||||
|
const buildDefaultForm = () => ({
|
||||||
|
id: "",
|
||||||
|
benefitName: "",
|
||||||
|
benefitCode: "",
|
||||||
|
benefitType: "",
|
||||||
|
benefitLogo: "",
|
||||||
|
benefitDesc: "",
|
||||||
|
benefitSort: 1,
|
||||||
|
benefitState: "OPEN",
|
||||||
|
benefitConfig: "",
|
||||||
|
/** 仅 UI:赠送积分数量;提交时写入 benefitConfig */
|
||||||
|
giftPoint: null,
|
||||||
|
/** 仅 UI:券礼包已选优惠券(含展示字段);提交时写入 benefitConfig.coupons */
|
||||||
|
couponPackageRows: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "memberBenefit",
|
||||||
|
components: {
|
||||||
|
uploadPicInput,
|
||||||
|
couponTemplate,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
benefitTypeOptions: [],
|
||||||
|
loading: false,
|
||||||
|
total: 0,
|
||||||
|
searchForm: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
title: "权益类型",
|
||||||
|
key: "benefitType",
|
||||||
|
width: 132,
|
||||||
|
render: (h, params) => {
|
||||||
|
const v = params.row.benefitType;
|
||||||
|
let text = "-";
|
||||||
|
if (v) {
|
||||||
|
const opt = this.benefitTypeOptions.find((o) => o.value === v);
|
||||||
|
text = opt ? opt.description : v;
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
attrs: { title: text },
|
||||||
|
class: "benefit-list-cell-ellipsis benefit-list-cell-ellipsis--type",
|
||||||
|
},
|
||||||
|
text
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "权益名称",
|
||||||
|
key: "benefitName",
|
||||||
|
width: 188,
|
||||||
|
render: (h, params) => {
|
||||||
|
const text = params.row.benefitName || "-";
|
||||||
|
return h(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
attrs: { title: text },
|
||||||
|
class: "benefit-list-cell-ellipsis benefit-list-cell-ellipsis--name",
|
||||||
|
},
|
||||||
|
text
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "权益LOGO",
|
||||||
|
key: "benefitLogo",
|
||||||
|
width: 116,
|
||||||
|
align: "center",
|
||||||
|
render: (h, params) => {
|
||||||
|
const src = params.row.benefitLogo;
|
||||||
|
if (!src) return h("span", "-");
|
||||||
|
return h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "benefit-logo-thumb benefit-logo-thumb--table",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("img", {
|
||||||
|
attrs: { src, alt: "" },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
key: "benefitState",
|
||||||
|
width: 118,
|
||||||
|
align: "center",
|
||||||
|
render: (h, params) => {
|
||||||
|
const row = params.row;
|
||||||
|
return h("i-switch", {
|
||||||
|
props: {
|
||||||
|
value: row.benefitState === "OPEN",
|
||||||
|
size: "large",
|
||||||
|
loading: !!row._benefitStateLoading,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
"on-change": (checked) => {
|
||||||
|
this.onBenefitStateSwitch(row, checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
h("span", { slot: "open" }, "开启"),
|
||||||
|
h("span", { slot: "close" }, "关闭"),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
align: "center",
|
||||||
|
width: 140,
|
||||||
|
render: (h, params) => {
|
||||||
|
const linkStyle = {
|
||||||
|
color: "#2d8cf0",
|
||||||
|
cursor: "pointer",
|
||||||
|
textDecoration: "none",
|
||||||
|
};
|
||||||
|
const sep = h("span", { style: { margin: "0 8px", color: "#dcdee2" } }, "|");
|
||||||
|
return h("div", { 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(),
|
||||||
|
couponPickerVisible: false,
|
||||||
|
couponPickerWhich: "add",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rulesAdd() {
|
||||||
|
return this.buildFormRules("add");
|
||||||
|
},
|
||||||
|
rulesEdit() {
|
||||||
|
return this.buildFormRules("edit");
|
||||||
|
},
|
||||||
|
couponPackageTableColumns() {
|
||||||
|
return this.buildCouponPackageTableColumns("add");
|
||||||
|
},
|
||||||
|
couponPackageTableColumnsEdit() {
|
||||||
|
return this.buildCouponPackageTableColumns("edit");
|
||||||
|
},
|
||||||
|
/** 传给优惠券列表组件,用于勾选回显(与 coupon-publish 一致用 id) */
|
||||||
|
couponPickerSelectedList() {
|
||||||
|
const form = this.couponPickerWhich === "add" ? this.formAdd : this.formEdit;
|
||||||
|
return (form.couponPackageRows || []).map((r) => ({
|
||||||
|
id: r.couponId,
|
||||||
|
couponName: r.couponName || "",
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildFormRules(which) {
|
||||||
|
const formKey = which === "add" ? "formAdd" : "formEdit";
|
||||||
|
return {
|
||||||
|
benefitType: [{ required: true, message: "请选择权益类型", trigger: "change" }],
|
||||||
|
benefitName: [{ required: true, message: "请输入权益名称", trigger: "blur" }],
|
||||||
|
benefitLogo: [{ required: true, message: "请配置权益LOGO", trigger: "change" }],
|
||||||
|
benefitSort: [{ required: true, type: "number", message: "请输入排序", trigger: "change" }],
|
||||||
|
benefitState: [{ required: true, message: "请选择启用状态", trigger: "change" }],
|
||||||
|
giftPoint: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
const form = this[formKey];
|
||||||
|
this.validateGiftPoint(form, value, callback);
|
||||||
|
},
|
||||||
|
trigger: ["change", "blur"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
couponPackageRows: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
const form = this[formKey];
|
||||||
|
this.validateCouponPackage(form, callback);
|
||||||
|
},
|
||||||
|
trigger: "change",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validateGiftPoint(form, value, callback) {
|
||||||
|
if (form.benefitType !== GIFT_POINT) return callback();
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return callback(new Error("请输入赠送积分"));
|
||||||
|
}
|
||||||
|
const n = Number(value);
|
||||||
|
if (Number.isNaN(n) || n < 0 || n > 99999) {
|
||||||
|
return callback(new Error("赠送积分范围为 0-99999"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
validateCouponPackage(form, callback) {
|
||||||
|
if (form.benefitType !== COUPON_PACKAGE) return callback();
|
||||||
|
const rows = form.couponPackageRows || [];
|
||||||
|
if (!rows.length) {
|
||||||
|
return callback(new Error("请添加至少一张优惠券"));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const q = Number(rows[i].quantity);
|
||||||
|
if (Number.isNaN(q) || q < 1 || q > 10) {
|
||||||
|
return callback(new Error("每张券赠送张数需在 1~10 之间"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
formatCouponFace(row) {
|
||||||
|
if (!row) return "-";
|
||||||
|
if (row.price !== undefined && row.price !== null && row.price !== "") {
|
||||||
|
return `¥${row.price}`;
|
||||||
|
}
|
||||||
|
if (row.couponDiscount !== undefined && row.couponDiscount !== null && row.couponDiscount !== "") {
|
||||||
|
return `${row.couponDiscount}折`;
|
||||||
|
}
|
||||||
|
return "-";
|
||||||
|
},
|
||||||
|
formatCouponValidity(row) {
|
||||||
|
return formatPromotionCouponValidityHtml(row);
|
||||||
|
},
|
||||||
|
buildCouponDisplayRow(detail, quantity) {
|
||||||
|
const qty = Math.min(10, Math.max(1, Number(quantity) || 1));
|
||||||
|
return {
|
||||||
|
couponId: detail.id,
|
||||||
|
quantity: qty,
|
||||||
|
couponName: detail.couponName || "",
|
||||||
|
faceValueLabel: this.formatCouponFace(detail),
|
||||||
|
validRange: this.formatCouponValidity(detail),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
buildCouponPackageTableColumns(which) {
|
||||||
|
const formKey = which === "add" ? "formAdd" : "formEdit";
|
||||||
|
const linkStyle = { color: "#2d8cf0", cursor: "pointer" };
|
||||||
|
return [
|
||||||
|
{ title: "优惠券名称", key: "couponName", minWidth: 100, tooltip: true },
|
||||||
|
{
|
||||||
|
title: "有效期",
|
||||||
|
minWidth: 100,
|
||||||
|
render: (h, p) =>
|
||||||
|
h("span", {
|
||||||
|
domProps: { innerHTML: p.row.validRange || "-" },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ title: "面额", key: "faceValueLabel", minWidth: 100 },
|
||||||
|
{
|
||||||
|
title: "赠送张数",
|
||||||
|
key: "quantity",
|
||||||
|
width: 150,
|
||||||
|
render: (h, params) =>
|
||||||
|
h("InputNumber", {
|
||||||
|
props: {
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
precision: 0,
|
||||||
|
value: params.row.quantity,
|
||||||
|
},
|
||||||
|
style: { width: "100px" },
|
||||||
|
on: {
|
||||||
|
"on-change": (val) => {
|
||||||
|
this.$set(this[formKey].couponPackageRows[params.index], "quantity", val);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const ref = which === "add" ? this.$refs.addForm : this.$refs.editForm;
|
||||||
|
if (ref) ref.validateField("couponPackageRows");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
width: 90,
|
||||||
|
align: "center",
|
||||||
|
render: (h, params) =>
|
||||||
|
h(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
style: linkStyle,
|
||||||
|
on: {
|
||||||
|
click: () => {
|
||||||
|
this.removeCouponRow(which, params.index);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"删除"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
/** 构造 benefitConfig JSON 字符串 */
|
||||||
|
buildBenefitConfigString(slice) {
|
||||||
|
const { benefitType, giftPoint, couponPackageRows } = slice;
|
||||||
|
if (benefitType === GIFT_POINT) {
|
||||||
|
return JSON.stringify({ giftPoint: Number(giftPoint) });
|
||||||
|
}
|
||||||
|
if (benefitType === COUPON_PACKAGE) {
|
||||||
|
const coupons = (couponPackageRows || []).map((r) => ({
|
||||||
|
couponId: String(r.couponId),
|
||||||
|
quantity: Math.min(10, Math.max(1, Number(r.quantity) || 1)),
|
||||||
|
}));
|
||||||
|
return JSON.stringify({ coupons });
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
/** 从详情 benefitConfig 解析赠送积分,解析失败时返回 null */
|
||||||
|
parseGiftPointFromConfig(benefitType, benefitConfigStr) {
|
||||||
|
if (benefitType !== GIFT_POINT || !benefitConfigStr) return null;
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(benefitConfigStr);
|
||||||
|
if (o && typeof o.giftPoint !== "undefined") {
|
||||||
|
const n = Number(o.giftPoint);
|
||||||
|
return Number.isNaN(n) ? null : n;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onAddBenefitTypeChange(val) {
|
||||||
|
if (val !== GIFT_POINT) {
|
||||||
|
this.formAdd.giftPoint = null;
|
||||||
|
}
|
||||||
|
if (val !== COUPON_PACKAGE) {
|
||||||
|
this.formAdd.couponPackageRows = [];
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.addForm && val !== GIFT_POINT) {
|
||||||
|
this.$refs.addForm.validateField("giftPoint");
|
||||||
|
}
|
||||||
|
if (this.$refs.addForm && val !== COUPON_PACKAGE) {
|
||||||
|
this.$refs.addForm.validateField("couponPackageRows");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onEditBenefitTypeChange(val) {
|
||||||
|
if (val !== GIFT_POINT) {
|
||||||
|
this.formEdit.giftPoint = null;
|
||||||
|
}
|
||||||
|
if (val !== COUPON_PACKAGE) {
|
||||||
|
this.formEdit.couponPackageRows = [];
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.editForm && val !== GIFT_POINT) {
|
||||||
|
this.$refs.editForm.validateField("giftPoint");
|
||||||
|
}
|
||||||
|
if (this.$refs.editForm && val !== COUPON_PACKAGE) {
|
||||||
|
this.$refs.editForm.validateField("couponPackageRows");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseCouponsFromConfig(str) {
|
||||||
|
if (!str) return [];
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(str);
|
||||||
|
if (o && Array.isArray(o.coupons)) {
|
||||||
|
return o.coupons.map((c) => ({
|
||||||
|
couponId: c.couponId,
|
||||||
|
quantity: c.quantity,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
/** 与添加时 formatCouponFace 一致;仅有 faceValueText 时去掉「减免」前缀 */
|
||||||
|
normalizeCouponFaceFromDetail(item) {
|
||||||
|
let face = this.formatCouponFace(item);
|
||||||
|
if (face !== "-") return face;
|
||||||
|
const raw = item.faceValueText;
|
||||||
|
if (raw == null || raw === "") return "-";
|
||||||
|
return String(raw)
|
||||||
|
.replace(/^减免现金\s*/, "")
|
||||||
|
.replace(/^减免\s*/, "")
|
||||||
|
.trim() || "-";
|
||||||
|
},
|
||||||
|
/** 管理端详情 couponItems → 表格行(面额展示与添加一致) */
|
||||||
|
mapCouponItemsToPackageRows(items) {
|
||||||
|
if (!Array.isArray(items)) return [];
|
||||||
|
return items.map((item) => {
|
||||||
|
const face = this.normalizeCouponFaceFromDetail(item);
|
||||||
|
const validHtml = item.validityText
|
||||||
|
? String(item.validityText).replace(/\n/g, "<br/>")
|
||||||
|
: "-";
|
||||||
|
return {
|
||||||
|
couponId: item.couponId,
|
||||||
|
quantity: Math.min(10, Math.max(1, Number(item.quantity) || 1)),
|
||||||
|
couponName: item.couponName || "",
|
||||||
|
faceValueLabel: face,
|
||||||
|
validRange: validHtml,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async hydrateCouponRowsForEdit(snippets) {
|
||||||
|
const rows = [];
|
||||||
|
for (const s of snippets) {
|
||||||
|
const cid = s.couponId;
|
||||||
|
const qty = s.quantity;
|
||||||
|
try {
|
||||||
|
const res = await getPlatformCoupon(cid);
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
rows.push(this.buildCouponDisplayRow(res.result, qty));
|
||||||
|
} else {
|
||||||
|
rows.push({
|
||||||
|
couponId: cid,
|
||||||
|
quantity: Math.min(10, Math.max(1, Number(qty) || 1)),
|
||||||
|
couponName: String(cid),
|
||||||
|
faceValueLabel: "-",
|
||||||
|
validRange: "-",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rows.push({
|
||||||
|
couponId: cid,
|
||||||
|
quantity: Math.min(10, Math.max(1, Number(qty) || 1)),
|
||||||
|
couponName: String(cid),
|
||||||
|
faceValueLabel: "-",
|
||||||
|
validRange: "-",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$set(this.formEdit, "couponPackageRows", rows);
|
||||||
|
},
|
||||||
|
handleCouponPickerClose() {
|
||||||
|
this.couponPickerVisible = false;
|
||||||
|
},
|
||||||
|
openCouponPicker(which) {
|
||||||
|
this.couponPickerWhich = which;
|
||||||
|
this.couponPickerVisible = true;
|
||||||
|
},
|
||||||
|
/** 与 coupon-publish `selectedCoupon`:表格多选变更时同步到券礼包配置 */
|
||||||
|
onCouponTemplateSelected(selectedRows) {
|
||||||
|
const which = this.couponPickerWhich;
|
||||||
|
const form = which === "add" ? this.formAdd : this.formEdit;
|
||||||
|
const refName = which === "add" ? "addForm" : "editForm";
|
||||||
|
const list = selectedRows || [];
|
||||||
|
const rows = list.map((row) => {
|
||||||
|
const existing = (form.couponPackageRows || []).find((r) => String(r.couponId) === String(row.id));
|
||||||
|
const qty = existing ? existing.quantity : 1;
|
||||||
|
return this.buildCouponDisplayRow(row, qty);
|
||||||
|
});
|
||||||
|
this.$set(form, "couponPackageRows", rows);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const ref = this.$refs[refName];
|
||||||
|
if (ref) ref.validateField("couponPackageRows");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeCouponRow(which, index) {
|
||||||
|
const form = which === "add" ? this.formAdd : this.formEdit;
|
||||||
|
const refName = which === "add" ? "addForm" : "editForm";
|
||||||
|
form.couponPackageRows.splice(index, 1);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const ref = this.$refs[refName];
|
||||||
|
if (ref) ref.validateField("couponPackageRows");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loadBenefitTypes();
|
||||||
|
this.getData();
|
||||||
|
},
|
||||||
|
loadBenefitTypes() {
|
||||||
|
API_Member.getMemberBenefitTypes().then((res) => {
|
||||||
|
if (res && res.success && Array.isArray(res.result)) {
|
||||||
|
this.benefitTypeOptions = res.result;
|
||||||
|
} else {
|
||||||
|
this.benefitTypeOptions = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
changePage(page) {
|
||||||
|
this.searchForm.pageNumber = page;
|
||||||
|
this.getData();
|
||||||
|
},
|
||||||
|
changePageSize(size) {
|
||||||
|
this.searchForm.pageSize = size;
|
||||||
|
this.searchForm.pageNumber = 1;
|
||||||
|
this.getData();
|
||||||
|
},
|
||||||
|
getData() {
|
||||||
|
this.loading = true;
|
||||||
|
const params = { ...this.searchForm };
|
||||||
|
API_Member.getMemberBenefitByPage(params).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
this.data = Array.isArray(res.result.records) ? res.result.records : [];
|
||||||
|
this.total = Number(res.result.total) || 0;
|
||||||
|
} else {
|
||||||
|
this.data = [];
|
||||||
|
this.total = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openAdd() {
|
||||||
|
this.addFlag = true;
|
||||||
|
this.submitAddLoading = false;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.addForm) this.$refs.addForm.resetFields();
|
||||||
|
this.formAdd = buildDefaultForm();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitAdd() {
|
||||||
|
if (!this.$refs.addForm) {
|
||||||
|
this.$Message.warning("表单未就绪,请稍后重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$refs.addForm.validate((valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
this.submitAddLoading = true;
|
||||||
|
const { giftPoint, couponPackageRows, ...rest } = this.formAdd;
|
||||||
|
const payload = {
|
||||||
|
...rest,
|
||||||
|
benefitConfig: this.buildBenefitConfigString({
|
||||||
|
benefitType: this.formAdd.benefitType,
|
||||||
|
giftPoint,
|
||||||
|
couponPackageRows,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
API_Member.addMemberBenefit(payload).then((res) => {
|
||||||
|
this.submitAddLoading = false;
|
||||||
|
if (res && res.success) {
|
||||||
|
this.$Message.success("添加成功");
|
||||||
|
this.addFlag = false;
|
||||||
|
this.getData();
|
||||||
|
} else if (res && res.message) {
|
||||||
|
this.$Message.error(res.message);
|
||||||
|
} else {
|
||||||
|
this.$Message.error("添加失败");
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.submitAddLoading = false;
|
||||||
|
this.$Message.error("网络异常,请稍后重试");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openEdit(row) {
|
||||||
|
this.editFlag = true;
|
||||||
|
this.submitEditLoading = false;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.editForm) this.$refs.editForm.resetFields();
|
||||||
|
});
|
||||||
|
API_Member.getMemberBenefit(row.id).then((res) => {
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
const raw = res.result;
|
||||||
|
/** 新接口:{ benefit, couponItems };旧接口:扁平 MemberBenefit */
|
||||||
|
const detail = raw.benefit != null ? raw.benefit : raw;
|
||||||
|
const couponItems = raw.couponItems;
|
||||||
|
const bt = detail.benefitType || "";
|
||||||
|
const parsed = this.parseGiftPointFromConfig(bt, detail.benefitConfig || "");
|
||||||
|
const couponSnippets = this.parseCouponsFromConfig(detail.benefitConfig || "");
|
||||||
|
|
||||||
|
let couponRows = [];
|
||||||
|
if (bt === COUPON_PACKAGE && Array.isArray(couponItems) && couponItems.length) {
|
||||||
|
couponRows = this.mapCouponItemsToPackageRows(couponItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formEdit = {
|
||||||
|
id: detail.id || row.id || "",
|
||||||
|
benefitName: detail.benefitName || "",
|
||||||
|
benefitCode: detail.benefitCode || "",
|
||||||
|
benefitType: bt,
|
||||||
|
benefitLogo: detail.benefitLogo || "",
|
||||||
|
benefitDesc: detail.benefitDesc || "",
|
||||||
|
benefitSort: Number(detail.benefitSort) > 0 ? Number(detail.benefitSort) : 1,
|
||||||
|
benefitState: detail.benefitState || "OPEN",
|
||||||
|
benefitConfig: detail.benefitConfig || "",
|
||||||
|
giftPoint: bt === GIFT_POINT ? (parsed !== null ? parsed : null) : null,
|
||||||
|
couponPackageRows: couponRows,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bt === COUPON_PACKAGE && couponRows.length === 0 && couponSnippets.length) {
|
||||||
|
this.hydrateCouponRowsForEdit(couponSnippets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitEdit() {
|
||||||
|
if (!this.$refs.editForm) {
|
||||||
|
this.$Message.warning("表单未就绪,请稍后重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$refs.editForm.validate((valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
this.submitEditLoading = true;
|
||||||
|
const { id, giftPoint, couponPackageRows, ...rest } = this.formEdit;
|
||||||
|
const payload = {
|
||||||
|
...rest,
|
||||||
|
benefitConfig: this.buildBenefitConfigString({
|
||||||
|
benefitType: this.formEdit.benefitType,
|
||||||
|
giftPoint,
|
||||||
|
couponPackageRows,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
API_Member.updateMemberBenefit(id, payload).then((res) => {
|
||||||
|
this.submitEditLoading = false;
|
||||||
|
if (res && res.success) {
|
||||||
|
this.$Message.success("修改成功");
|
||||||
|
this.editFlag = false;
|
||||||
|
this.getData();
|
||||||
|
} else if (res && res.message) {
|
||||||
|
this.$Message.error(res.message);
|
||||||
|
} else {
|
||||||
|
this.$Message.error("修改失败");
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.submitEditLoading = false;
|
||||||
|
this.$Message.error("网络异常,请稍后重试");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onBenefitStateSwitch(row, checked) {
|
||||||
|
const nextState = checked ? "OPEN" : "CLOSE";
|
||||||
|
const prevState = row.benefitState;
|
||||||
|
if (nextState === prevState) return;
|
||||||
|
const text = checked ? "开启" : "关闭";
|
||||||
|
this.$Modal.confirm({
|
||||||
|
title: "提示",
|
||||||
|
content: `<p>确定${text}该客户权益?</p>`,
|
||||||
|
onOk: () => {
|
||||||
|
this.$set(row, "_benefitStateLoading", true);
|
||||||
|
return API_Member.updateMemberBenefitState(row.id, nextState)
|
||||||
|
.then((res) => {
|
||||||
|
this.$set(row, "_benefitStateLoading", false);
|
||||||
|
if (res && res.success) {
|
||||||
|
this.$Message.success(`${text}成功`);
|
||||||
|
this.$set(row, "benefitState", nextState);
|
||||||
|
} else {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.$set(row, "_benefitStateLoading", false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove(row) {
|
||||||
|
this.$Modal.confirm({
|
||||||
|
title: "提示",
|
||||||
|
content: "<p>确定删除该客户权益?</p>",
|
||||||
|
onOk: () => {
|
||||||
|
API_Member.deleteMemberBenefit(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>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.benefit-drawer-footer-btns {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e8eaec;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.benefit-drawer-footer-btns .ivu-btn + .ivu-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
权益 LOGO 缩略图:固定 100×100。
|
||||||
|
不使用 .member-benefit 前缀:Modal 默认 transfer 到 body,弹窗内节点不在 .member-benefit 下。
|
||||||
|
Table 列 render 生成的节点在子组件内,同样用非 scoped。
|
||||||
|
-->
|
||||||
|
<style lang="scss">
|
||||||
|
.benefit-logo-thumb {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid #dcdee2;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f8f8f9;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.benefit-logo-thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.benefit-logo-thumb--table {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 权益列表:固定类型/名称列宽并省略过长文案(悬浮 title 可看全文) */
|
||||||
|
.member-benefit .benefit-list-table table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
.member-benefit .benefit-list-cell-ellipsis {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.member-benefit .benefit-list-cell-ellipsis--type {
|
||||||
|
max-width: 118px;
|
||||||
|
}
|
||||||
|
.member-benefit .benefit-list-cell-ellipsis--name {
|
||||||
|
max-width: 172px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表不出现列与列之间的竖线(保留行间分隔) */
|
||||||
|
.member-benefit .benefit-list-table--no-vertical-borders .ivu-table th,
|
||||||
|
.member-benefit .benefit-list-table--no-vertical-borders .ivu-table td {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.benefit-logo-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.benefit-logo-upload {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
197
manager/src/views/member/grade/experience-log.vue
Normal file
197
manager/src/views/member/grade/experience-log.vue
Normal 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>
|
||||||
556
manager/src/views/member/grade/experience-setting.vue
Normal file
556
manager/src/views/member/grade/experience-setting.vue
Normal 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>
|
||||||
547
manager/src/views/member/grade/index.vue
Normal file
547
manager/src/views/member/grade/index.vue
Normal 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>
|
||||||
@@ -23,6 +23,11 @@
|
|||||||
<Option v-for="item in memberGroupList" :value="item.id" :key="item.id">{{ item.groupName }}</Option>
|
<Option v-for="item in memberGroupList" :value="item.id" :key="item.id">{{ item.groupName }}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form-item>
|
</Form-item>
|
||||||
|
<Form-item label="会员等级" prop="gradeId">
|
||||||
|
<Select v-model="searchForm.gradeId" clearable filterable style="width: 240px">
|
||||||
|
<Option v-for="item in memberGradeList" :value="item.id" :key="item.id">{{ item.gradeName }}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form-item>
|
||||||
<Button @click="handleSearch" class="search-btn" type="primary" icon="ios-search">搜索</Button>
|
<Button @click="handleSearch" class="search-btn" type="primary" icon="ios-search">搜索</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -255,6 +260,7 @@ export default {
|
|||||||
groupId: [{ required: true, message: "请选择会员分组", trigger: "change" }],
|
groupId: [{ required: true, message: "请选择会员分组", trigger: "change" }],
|
||||||
},
|
},
|
||||||
memberGroupList: [],
|
memberGroupList: [],
|
||||||
|
memberGradeList: [],
|
||||||
memberPointRule: {
|
memberPointRule: {
|
||||||
type: [{ required: true, message: "请选择类型", trigger: "change" }],
|
type: [{ required: true, message: "请选择类型", trigger: "change" }],
|
||||||
point: [
|
point: [
|
||||||
@@ -358,6 +364,23 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "经验值",
|
||||||
|
key: "experience",
|
||||||
|
minWidth: 100,
|
||||||
|
render: (h, params) => {
|
||||||
|
return h("div", {}, params.row.experience == null ? "0" : params.row.experience);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "会员等级",
|
||||||
|
key: "gradeName",
|
||||||
|
minWidth: 120,
|
||||||
|
tooltip: true,
|
||||||
|
render: (h, params) => {
|
||||||
|
return h("div", {}, params.row.gradeName || "-");
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "余额",
|
title: "余额",
|
||||||
key: "memberWallet",
|
key: "memberWallet",
|
||||||
@@ -589,6 +612,13 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
loadMemberGradeList() {
|
||||||
|
API_Member.getMemberGradeByPage({ pageNumber: 1, pageSize: 1000 }).then((res) => {
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
this.memberGradeList = res.result || [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
if (this.$refs.table && this.$refs.table.clearSelection) {
|
if (this.$refs.table && this.$refs.table.clearSelection) {
|
||||||
this.$refs.table.clearSelection();
|
this.$refs.table.clearSelection();
|
||||||
@@ -867,6 +897,7 @@ export default {
|
|||||||
this.getData();
|
this.getData();
|
||||||
if (!this.selectedMember) {
|
if (!this.selectedMember) {
|
||||||
this.loadMemberGroupList();
|
this.loadMemberGroupList();
|
||||||
|
this.loadMemberGradeList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
promotionsStatusRender,
|
promotionsStatusRender,
|
||||||
promotionsScopeTypeRender,
|
promotionsScopeTypeRender,
|
||||||
|
promotionsCouponActivityTimeRender,
|
||||||
} from "@/utils/promotions";
|
} from "@/utils/promotions";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -148,13 +149,14 @@ export default {
|
|||||||
// 表头
|
// 表头
|
||||||
{
|
{
|
||||||
type: "selection",
|
type: "selection",
|
||||||
width: 100,
|
width: 52,
|
||||||
align: "center",
|
align: "center",
|
||||||
fixed: "left",
|
/* 不使用 fixed:左侧固定列与主体表格分开渲染,叠加全局 .ivu-table table{width:100%} 易在接缝处留白;勾选列无需横向钉住 */
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "优惠券名称",
|
title: "优惠券名称",
|
||||||
key: "couponName",
|
key: "couponName",
|
||||||
|
minWidth: 140,
|
||||||
width: 180,
|
width: 180,
|
||||||
tooltip: true,
|
tooltip: true,
|
||||||
},
|
},
|
||||||
@@ -238,21 +240,7 @@ export default {
|
|||||||
{
|
{
|
||||||
title: "活动时间",
|
title: "活动时间",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (h, params) => {
|
render: promotionsCouponActivityTimeRender,
|
||||||
if (
|
|
||||||
params?.row?.getType === "ACTIVITY" &&
|
|
||||||
params?.row?.rangeDayType === "DYNAMICTIME"
|
|
||||||
) {
|
|
||||||
return h("div", "长期有效");
|
|
||||||
} else if (params?.row?.startTime && params?.row?.endTime) {
|
|
||||||
return h("div", {
|
|
||||||
domProps: {
|
|
||||||
innerHTML:
|
|
||||||
params.row.startTime + "<br/>" + params.row.endTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "状态",
|
title: "状态",
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
ruleValidate: {}, // 验证规则
|
ruleValidate: {}, // 验证规则
|
||||||
way: { // 类型
|
way: { // 类型
|
||||||
APP: "移动应用端",
|
APP: "APP",
|
||||||
H5: "移动端",
|
H5: "手机网页(微商城)",
|
||||||
WECHAT_MP: "小程序端",
|
WECHAT_MP: "微信小程序",
|
||||||
PC: "PC端",
|
PC: "WEB网页",
|
||||||
},
|
},
|
||||||
formValidate: {} // 表单数据
|
formValidate: {} // 表单数据
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user