feat: 会员详情弹框样式

This commit is contained in:
lzhizhao 2025-08-08 23:14:40 +08:00
parent 8aa41006de
commit 562a766257
9 changed files with 1244 additions and 290 deletions

View File

@ -414,7 +414,7 @@ export default {
menuId: getUUID(),
parentId: 0,
parentName: null,
name: "用户管理",
name: "会员列表",
url: "marketing/user/index",
perms: "",
type: 1,

View File

@ -32,6 +32,12 @@ const globalRoutes = [
component: _import("common/login"),
name: "login",
meta: { title: "登录" }
},
{
path: "/test-member-detail",
component: _import("test-member-detail"),
name: "test-member-detail",
meta: { title: "会员详情样式测试" }
}
//test用会员管理
];

View File

@ -6,34 +6,34 @@
* @Description:
* @FilePath: \background-front-end\src\utils\httpRequest.js
*/
import Vue from 'vue'
import axios from 'axios'
import router from '@/router'
import qs from 'qs'
import merge from 'lodash/merge'
import { Message } from 'element-ui'
import { clearLoginInfo } from '@/utils'
import commonUtil from './common'
import des from './des.js'
import Vue from "vue";
import axios from "axios";
import router from "@/router";
import qs from "qs";
import merge from "lodash/merge";
import { Message } from "element-ui";
import { clearLoginInfo } from "@/utils";
import commonUtil from "./common";
import des from "./des.js";
const http = axios.create({
baseURL:
process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY
? '/proxyApi/'
process.env.NODE_ENV !== "production" && process.env.OPEN_PROXY
? "/proxyApi/"
: window.SITE_CONFIG.baseUrl,
timeout: 1000 * 30,
withCredentials: true,
headers: {
'Content-Type': 'application/json; charset=utf-8'
"Content-Type": "application/json; charset=utf-8"
}
})
});
/**
* 请求拦截
*/
http.interceptors.request.use(
config => {
config.headers['token'] = `Bearer ${Vue.cookie.get('token')}` // 请求头带上token
config.headers["token"] = `Bearer ${Vue.cookie.get("token")}`; // 请求头带上token
// 针对post请求加密
// if (config.method.toLowerCase() === "post" && process.env.NODE_ENV === "production" ) {
// //如果已经转成字符串了,就不用再转了
@ -47,16 +47,16 @@ http.interceptors.request.use(
// );
// }
// }
if (process.env.NODE_ENV !== 'production') {
console.log(`【请求】${config.url}`, config)
if (process.env.NODE_ENV !== "production") {
console.log(`【请求】${config.url}`, config);
}
return config
return config;
},
error => {
return Promise.reject(error)
return Promise.reject(error);
}
)
);
// /**
// * 响应拦截
@ -147,56 +147,56 @@ http.interceptors.request.use(
http.interceptors.response.use(
response => {
//开发环境下才可以打印日志
if (process.env.NODE_ENV !== 'production') {
console.log(`【响应】${response.config.url}`, response)
if (process.env.NODE_ENV !== "production") {
console.log(`【响应】${response.config.url}`, response);
}
if (response.data && response.data.code == 401) {
// 401, 权限未认证
clearLoginInfo()
router.push({ name: 'login' })
return Promise.resolve(response)
clearLoginInfo();
router.push({ name: "login" });
return Promise.resolve(response);
} else if (response.data && response.data.code != 200) {
// Message.closeAll();
Message({
message: response.data.msg || response.data.message,
type: 'error'
})
return Promise.reject(response)
type: "error"
});
return Promise.reject(response);
} else {
//请求成功的情况
//如果是开发模式下,都要弹出
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== "production") {
// Message.closeAll();
if (!response.config.hidemsg) {
Message({
message: response.data.msg || response.data.message,
type: 'success'
})
// Message({
// message: response.data.msg || response.data.message,
// type: 'success'
// })
}
}
// 生产环境下限制性弹出
else {
if (
response.data.msg != 'success' &&
response.data.msg != '查询成功' &&
Object.prototype.toString.call(response.data) === '[object Object]'
response.data.msg != "success" &&
response.data.msg != "查询成功" &&
Object.prototype.toString.call(response.data) === "[object Object]"
) {
// Message.closeAll();
if (!response.config.hidemsg) {
Message({
message: response.data.msg || response.data.message,
type: 'success'
})
type: "success"
});
}
}
}
return Promise.resolve(response)
return Promise.resolve(response);
}
},
error => {
return Promise.reject(error)
return Promise.reject(error);
}
)
);
/**
* 请求地址处理
@ -205,11 +205,11 @@ http.interceptors.response.use(
http.adornUrl = actionName => {
// 非生产环境 && 开启代理, 接口前缀统一使用[/proxyApi/]前缀做代理拦截!
return (
(process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY
? '/proxyApi/'
(process.env.NODE_ENV !== "production" && process.env.OPEN_PROXY
? "/proxyApi/"
: window.SITE_CONFIG.baseUrl) + actionName
)
}
);
};
/**
* get请求参数处理
@ -219,9 +219,9 @@ http.adornUrl = actionName => {
http.adornParams = (params = {}, openDefultParams = true) => {
var defaults = {
t: new Date().getTime()
}
return openDefultParams ? merge(defaults, params) : params
}
};
return openDefultParams ? merge(defaults, params) : params;
};
/**
* post请求数据处理
@ -231,12 +231,12 @@ http.adornParams = (params = {}, openDefultParams = true) => {
* json: 'application/json; charset=utf-8'
* form: 'application/x-www-form-urlencoded; charset=utf-8'
*/
http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => {
http.adornData = (data = {}, openDefultdata = true, contentType = "json") => {
var defaults = {
t: new Date().getTime()
}
data = openDefultdata ? merge(defaults, data) : data
return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
}
};
data = openDefultdata ? merge(defaults, data) : data;
return contentType === "json" ? JSON.stringify(data) : qs.stringify(data);
};
export default http
export default http;

View File

@ -951,7 +951,6 @@ export default {
<style lang="scss" scoped>
.level-detail {
padding: 20px;
background: #fff;
min-height: calc(100vh - 84px);

View File

@ -12,7 +12,7 @@
>
<template slot="tableTop">
<el-form :inline="true" :model="form" class="demo-form-inline">
<el-form-item v-if="storeList.length > 1" label="店铺">
<el-form-item v-if="storeList.length > 1" label="所属店铺">
<el-select v-model="formInline.shopId" placeholder="请选择店铺">
<el-option
v-for="item in storeList"
@ -22,40 +22,23 @@
></el-option>
</el-select>
</el-form-item>
<el-form-item label="用户ID">
<el-input
placeholder="请输入用户ID"
v-model="form.userId"
></el-input>
</el-form-item>
<el-form-item label="用户昵称">
<el-input
placeholder="请输入用户昵称"
v-model="form.username"
></el-input>
</el-form-item>
<el-form-item label="会员等级id">
<el-input
placeholder="请输入会员等级id"
<el-form-item label="会员等级">
<el-select
v-model="form.levelId"
></el-input>
</el-form-item>
<el-form-item label="手机号">
<el-input
placeholder="请输入手机号"
v-model="form.mobile"
></el-input>
</el-form-item>
<el-form-item label="会员状态">
<el-select v-model="form.isMemberUser" placeholder="请选择会员状态">
placeholder="请选择会员等级"
clearable
>
<el-option
v-for="item in [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
]"
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in memberLevelList"
:key="item.levelId"
:label="item.levelName"
:value="item.levelId"
></el-option>
</el-select>
</el-form-item>
@ -90,31 +73,32 @@
<div class="stat-item">
<i style="font-size: 22px" class="el-icon-user-solid"></i>
<div class="stat-right">
<div class="stat-title">用户数量</div>
<div class="stat-value">
<span style="font-size: 20px">{{
overviewList.totalCount
}}</span>
</div>
<div class="stat-title">会员总数</div>
</div>
</div>
<div class="stat-item">
<i style="font-size: 22px" class="el-icon-s-custom"></i>
<div class="stat-right">
<div class="stat-title">今日新增</div>
<div class="stat-value">
<span style="font-size: 20px">{{
overviewList.todayCount
}}</span>
</div>
<div class="stat-title">今日新增</div>
</div>
</div>
</div>
<div class="mb-2">
<el-button type="success" size="small" @click="userExport"
>导出</el-button
>
</div>
<!-- 测试按钮 -->
<!-- <div class="test-buttons" style="margin-bottom: 10px;">
<el-button type="success" size="small" @click="showTestDetail">
测试查看会员详情样式
</el-button>
</div> -->
</template>
</obj-table-plus>
<!-- 修改积分或成长值 -->
@ -128,8 +112,6 @@
</template>
<script>
import * as XLSX from "xlsx";
import { saveAs } from "file-saver";
import modifyPoints from "./popup/modify-points.vue";
import viewDetails from "./popup/view-details.vue";
import { mapState } from "vuex";
@ -141,7 +123,7 @@ export default {
formInline: {
marketId: "",
shopId: "",
unitType: "",
unitType: ""
},
form: {},
tableProp: {
@ -149,12 +131,13 @@ export default {
border: true,
height: "auto",
"row-id": "id",
"show-overflow": false,
"show-overflow": false
},
productFilterType: "SALE",
selectList: [],
value1: [],
overviewList: {},
memberLevelList: [] //
};
},
created() {
@ -164,45 +147,57 @@ export default {
? 3
: 2,
marketId: this.marketId,
shopId: this.shopId,
shopId: this.shopId
};
this.getMemberLevelList();
this.$nextTick(() => {
this.$refs.oTable.reload();
});
},
methods: {
// getData() {
// this.$api.mer_admin
// .storeList({ marketId: this.formInline.marketId })
// .then((res) => {
// this.storeList = res.data.data;
// this.formInline.shopId = res.data.data[0].shopId;
// this.$nextTick(() => {
// this.$refs.oTable.reload();
// });
// });
// },
//
getMemberLevelList() {
this.$api.marketing
.marketingLevelPage({
...this.formInline
})
.then(res => {
if (res.data.code === 0) {
this.memberLevelList = res.data.data || [];
}
})
.catch(() => {
this.memberLevelList = [];
});
},
//
maskMobile(mobile) {
if (!mobile || mobile.length !== 11) {
return mobile;
}
return mobile.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
},
queryList(pageNo, pageSize) {
this.$api.marketing
.marketingUserPage({
pageNumber: pageNo,
pageSize: pageSize,
...this.formInline,
...this.form,
...this.form
})
.then((res) => {
.then(res => {
console.log(res);
this.$refs.oTable.complete(
res.data.data.data,
Number(res.data.data.total)
);
})
.catch((err) => {
.catch(() => {
this.$refs.oTable.complete(false);
});
this.$api.marketing
.overview({ ...this.formInline, ...this.form })
.then((res) => {
.then(res => {
this.overviewList = res.data.data;
});
},
@ -212,6 +207,27 @@ export default {
this.$refs.oTable.reload();
});
},
//
showTestDetail() {
//
const testMemberData = {
userId: "3",
username: "张三",
mobile: "13812341234",
birthday: "1990-05-15",
headUrl:
"https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png",
levelName: "LV2",
growthValue: 1250,
memberPoints: 890,
joinTime: "2023-01-15 10:30:00",
registrationTime: "2023-01-15 10:30:00",
id: 1 //
};
//
this.$refs.viewDetails.toggle(testMemberData).add();
},
changeTime(val) {
if (val) {
this.form.startRegistrationTime = val[0];
@ -220,56 +236,24 @@ export default {
this.form.startRegistrationTime = "";
this.form.endRegistrationTime = "";
}
},
async userExport() {
let now = new Date();
let year = now.getFullYear();
let month = now.getMonth() + 1;
let day = now.getDate();
let res = await this.$api.marketing.marketingUserPage({
pageNumber: 1,
pageSize: 9999999,
...this.formInline,
...this.form,
});
console.log(res);
let userData = res.data.data.data.map((item) => {
return {
用户ID: item.userId,
用户头像: item.headUrl,
用户昵称: item.username,
手机号: item.mobile,
会员等级id: item.levelId,
会员等级名称: item.levelName,
生日: item.birthday,
注册时间: item.registrationTime,
};
});
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(userData);
XLSX.utils.book_append_sheet(workbook, worksheet, "用户管理");
const excelData = XLSX.write(workbook, {
type: "array",
bookType: "xlsx",
});
const blob = new Blob([excelData], { type: "application/octet-stream" });
saveAs(blob, `用户管理-${year + "" + month + day}.xlsx`);
},
}
},
computed: {
tableCols() {
return [
{ type: "checkbox", width: "60px", fixed: "left" },
// { type: "seq", width: "60px", align: "center", title: "" },
{
title: "用戶ID",
title: "所属店铺",
align: "center",
field: "userId",
field: "shopName"
},
{
title: "用戶头像",
title: "用户ID",
align: "center",
field: "userId"
},
{
title: "用户头像",
align: "center",
field: "headUrl",
type: "jsx",
@ -281,52 +265,60 @@ export default {
preview-src-list={[row.headUrl]}
></el-image>
);
},
}
},
{
title: "用昵称",
title: "用昵称",
align: "center",
field: "username",
field: "username"
},
{
title: "手机号",
align: "center",
field: "mobile",
},
// {
// title: "",
// align: "center",
// field: "name",
// },
// {
// title: "",
// align: "center",
// field: "name",
// },
// {
// title: "",
// align: "center",
// field: "name",
// },
{
title: "会员等级id",
align: "center",
field: "levelId",
type: "jsx",
render: ({ row }) => {
return <span>{this.maskMobile(row.mobile)}</span>;
}
},
{
title: "会员等级名称",
title: "会员等级",
align: "center",
field: "levelName",
width: "120px",
type: "jsx",
render: ({ row }) => {
if (!row.levelName) {
return <span>-</span>;
}
//
const getTagType = levelName => {
if (levelName.includes("LV1") || levelName.includes("1"))
return "";
if (levelName.includes("LV2") || levelName.includes("2"))
return "success";
if (levelName.includes("LV3") || levelName.includes("3"))
return "warning";
if (levelName.includes("LV4") || levelName.includes("4"))
return "danger";
return "info";
};
return (
<el-tag type={getTagType(row.levelName)} size="small">
{row.levelName}
</el-tag>
);
}
},
{
title: "生日",
align: "center",
field: "birthday",
field: "birthday"
},
{
title: "注册时间",
align: "center",
field: "registrationTime",
field: "registrationTime"
},
// {
// title: "",
@ -338,66 +330,28 @@ export default {
fixed: "right",
type: "jsx",
align: "center",
width: "280px",
width: "100px",
render: ({ row }) => {
let viewDetails = () => {
this.$api.marketing
.memberUnitUserDetail({
...this.formInline,
userId: row.userId,
userId: row.userId
})
.then((res) => {
.then(res => {
console.log(res);
this.$refs.viewDetails.toggle(res.data.data).add();
});
};
let points = () => {
this.$api.marketing
.memberUnitUserDetail({
...this.formInline,
userId: row.userId,
})
.then((res) => {
console.log(res);
this.$refs.modifyPoints.toggle(res.data.data).add();
});
};
let growthValue = () => {
this.$api.marketing
.memberUnitUserDetail({
...this.formInline,
userId: row.userId,
})
.then((res) => {
console.log(res);
this.$refs.modifyPoints.toggle(res.data.data).update();
});
};
return (
<div>
<el-button size="mini" type="primary" onClick={viewDetails}>
详情
</el-button>
<el-button
v-show={!row.id}
size="mini"
type="primary"
onClick={points}
>
修改积分
</el-button>
<el-button
v-show={!row.id}
size="mini"
type="primary"
onClick={growthValue}
>
修改成长值
</el-button>
</div>
);
},
},
}
}
];
},
tableEvent() {
@ -407,7 +361,7 @@ export default {
},
"checkbox-change": ({ records, reserves }) => {
this.selectList = [...records, ...reserves];
},
}
};
},
...mapState("userData", [
@ -415,9 +369,9 @@ export default {
"marketList",
"storeList",
"marketId",
"shopId",
]),
},
"shopId"
])
}
};
</script>
@ -433,7 +387,7 @@ export default {
}
.stat-item {
width: 30%;
margin: 20px 20px;
margin: 8px;
display: flex;
align-items: center;
}
@ -444,4 +398,4 @@ export default {
.stat-title {
margin-bottom: 5px;
}
</style>
</style>

View File

@ -0,0 +1,153 @@
<template>
<el-dialog
title="成长值详情"
:visible.sync="visible"
width="500px"
:close-on-click-modal="false"
@close="handleClose"
>
<div class="detail-content">
<!-- 基础信息 -->
<div class="detail-row">
<span class="label">变化原因</span>
<span class="value">{{ detailData.changeReason || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">变化时间</span>
<span class="value">{{ detailData.changeTime || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">成长值增加</span>
<span class="value growth-value" :style="{ color: getGrowthColor(detailData.growthChange) }">
{{ detailData.growthChange > 0 ? '+' : '' }}{{ detailData.growthChange || 0 }}
</span>
</div>
<!-- 有关联订单时显示的字段 -->
<template v-if="hasRelatedOrder">
<div class="detail-row">
<span class="label">订单编号</span>
<span class="value">{{ detailData.relatedOrder || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">订单总金额</span>
<span class="value">¥{{ detailData.orderAmount || '0.00' }}</span>
</div>
<div class="detail-row">
<span class="label">订单时间</span>
<span class="value">{{ detailData.orderTime || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">订单归属摊位</span>
<span class="value">{{ detailData.shopName || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">成长值变化</span>
<span class="value growth-value" :style="{ color: getGrowthColor(detailData.growthChange) }">
{{ detailData.growthChange > 0 ? '+' : '' }}{{ detailData.growthChange || 0 }}
</span>
</div>
</template>
<!-- 无关联订单时显示的描述 -->
<template v-if="!hasRelatedOrder && detailData.description">
<div class="description-section">
<div class="description-text">
{{ detailData.description }}
</div>
</div>
</template>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "GrowthDetail",
data() {
return {
visible: false,
detailData: {}
};
},
computed: {
hasRelatedOrder() {
return this.detailData.relatedOrder && this.detailData.relatedOrder !== '-';
}
},
methods: {
show(data) {
this.detailData = data;
this.visible = true;
},
handleClose() {
this.visible = false;
this.detailData = {};
},
getGrowthColor(value) {
return value > 0 ? '#67C23A' : '#F56C6C';
}
}
};
</script>
<style lang="scss" scoped>
.detail-content {
padding: 10px 0;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.label {
color: #606266;
font-weight: 500;
min-width: 100px;
}
.value {
color: #303133;
flex: 1;
text-align: right;
}
.growth-value {
font-weight: bold;
font-size: 16px;
}
.description-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px dashed #e4e7ed;
}
.description-text {
color: #909399;
font-size: 14px;
line-height: 1.5;
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #409eff;
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<el-dialog
title="积分详情"
:visible.sync="visible"
width="500px"
:close-on-click-modal="false"
@close="handleClose"
>
<div class="detail-content">
<!-- 基础信息 -->
<div class="detail-row">
<span class="label">变化原因</span>
<span class="value">{{ detailData.changeReason || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">变化时间</span>
<span class="value">{{ detailData.changeTime || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">积分变动</span>
<span class="value points-value" :style="{ color: getPointsColor(detailData.pointsChange) }">
{{ detailData.pointsChange > 0 ? '+' : '' }}{{ detailData.pointsChange || 0 }}
</span>
</div>
<!-- 有关联订单时显示的字段 -->
<template v-if="hasRelatedOrder">
<div class="detail-row">
<span class="label">订单编号</span>
<span class="value">{{ detailData.relatedOrder || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">订单总金额</span>
<span class="value">¥{{ detailData.orderAmount || '0.00' }}</span>
</div>
<div class="detail-row">
<span class="label">订单时间</span>
<span class="value">{{ detailData.orderTime || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">订单归属摊位</span>
<span class="value">{{ detailData.shopName || '-' }}</span>
</div>
</template>
<!-- 无关联订单且是使用积分时显示的字段 -->
<template v-if="!hasRelatedOrder && isPointsUsage">
<div class="detail-row">
<span class="label">兑换商品</span>
<span class="value">{{ detailData.exchangeProduct || '-' }}</span>
</div>
<div class="detail-row">
<span class="label">兑换数量</span>
<span class="value">{{ detailData.exchangeQuantity || '-' }}</span>
</div>
<div class="description-section">
<div class="description-text">
{{ detailData.usageDescription || '使用积分兑换商品,兑换成功后积分将被扣除且不可退还。' }}
</div>
</div>
</template>
<!-- 无关联订单且是获得积分时显示的描述 -->
<template v-if="!hasRelatedOrder && !isPointsUsage && detailData.description">
<div class="description-section">
<div class="description-text">
{{ detailData.description }}
</div>
</div>
</template>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "PointsDetail",
data() {
return {
visible: false,
detailData: {}
};
},
computed: {
hasRelatedOrder() {
return this.detailData.relatedOrder && this.detailData.relatedOrder !== '-';
},
isPointsUsage() {
return this.detailData.pointsChange < 0;
}
},
methods: {
show(data) {
this.detailData = data;
this.visible = true;
},
handleClose() {
this.visible = false;
this.detailData = {};
},
getPointsColor(value) {
return value > 0 ? '#67C23A' : '#F56C6C';
}
}
};
</script>
<style lang="scss" scoped>
.detail-content {
padding: 10px 0;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.label {
color: #606266;
font-weight: 500;
min-width: 100px;
}
.value {
color: #303133;
flex: 1;
text-align: right;
}
.points-value {
font-weight: bold;
font-size: 16px;
}
.description-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px dashed #e4e7ed;
}
.description-text {
color: #909399;
font-size: 14px;
line-height: 1.5;
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #409eff;
}
</style>

View File

@ -9,55 +9,285 @@
:modalHandles="modalHandles"
>
<template slot="dialog__after">
<div style="padding: 20px">
<el-descriptions title="用户信息">
<el-descriptions-item label="头像">
<el-image
style="width: 60px; height: 60px"
:src="modalData.headUrl"
:preview-src-list="[modalData.headUrl]"
>
</el-image>
</el-descriptions-item>
<el-descriptions-item label="用户名">{{
modalData.username
}}</el-descriptions-item>
<el-descriptions-item label="ID">{{
modalData.userId
}}</el-descriptions-item>
<el-descriptions-item label="手机号">{{
modalData.mobile
}}</el-descriptions-item>
<!-- <el-descriptions-item label=""
>江苏省苏州市吴中区吴中大道 1188 </el-descriptions-item
> -->
</el-descriptions>
<!-- 用户信息顶部展示 -->
<div class="user-info-header">
<div class="user-basic-info">
<div class="info-row">
<div class="info-item">
<span class="label">会员姓名</span>
<span class="value">{{ modalData.username || "-" }}</span>
</div>
<div class="info-item">
<span class="label">系统编号</span>
<span class="value">{{ modalData.userId || "-" }}</span>
</div>
<div class="info-item">
<span class="label">手机号</span>
<span class="value">{{ maskMobile(modalData.mobile) }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="label">生日</span>
<span class="value">{{ modalData.birthday || "-" }}</span>
</div>
</div>
</div>
<div class="user-stats">
<div class="stat-card">
<div class="stat-number">{{ modalData.growthValue || 0 }}</div>
<div class="stat-label">会员成长值</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ modalData.memberPoints || 0 }}</div>
<div class="stat-label">会员积分</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ couponCount }}</div>
<div class="stat-label">会员优惠券</div>
</div>
</div>
</div>
<div
v-if="modalData.id"
style="margin-top: 20px; border-top: 1px solid #ccc; padding: 20px"
>
<el-descriptions title="会员信息">
<el-descriptions-item label="会员名称">{{
modalData.levelName
}}</el-descriptions-item>
<el-descriptions-item label="生日">{{
modalData.birthday
}}</el-descriptions-item>
<el-descriptions-item label="加入会员时间">{{
modalData.joinTime
}}</el-descriptions-item>
</el-descriptions>
<!-- Tab分页内容 -->
<div class="tab-content">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="会员成长值明细" name="growth">
<el-table
:data="growthList"
border
style="width: 100%"
v-loading="growthLoading"
>
<el-table-column
prop="changeTime"
label="变化时间"
width="180"
align="center"
>
</el-table-column>
<el-table-column
prop="growthChange"
label="成长值变动"
width="120"
align="center"
>
<template slot-scope="scope">
<span
:style="{
color:
scope.row.growthChange > 0 ? '#67C23A' : '#F56C6C'
}"
>
{{ scope.row.growthChange > 0 ? "+" : ""
}}{{ scope.row.growthChange }}
</span>
</template>
</el-table-column>
<el-table-column
prop="relatedOrder"
label="关联订单"
align="center"
>
</el-table-column>
<el-table-column label="详情" width="100" align="center">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="viewGrowthDetail(scope.row)"
>
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleGrowthSizeChange"
@current-change="handleGrowthCurrentChange"
:current-page="growthPagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="growthPagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="growthPagination.total"
style="margin-top: 20px; text-align: center"
>
</el-pagination>
</el-tab-pane>
<el-tab-pane label="会员积分明细" name="points">
<el-table
:data="pointsList"
border
style="width: 100%"
v-loading="pointsLoading"
>
<el-table-column
prop="changeTime"
label="变化时间"
width="180"
align="center"
>
</el-table-column>
<el-table-column
prop="pointsChange"
label="积分变动"
width="120"
align="center"
>
<template slot-scope="scope">
<span
:style="{
color:
scope.row.pointsChange > 0 ? '#67C23A' : '#F56C6C'
}"
>
{{ scope.row.pointsChange > 0 ? "+" : ""
}}{{ scope.row.pointsChange }}
</span>
</template>
</el-table-column>
<el-table-column
prop="changeType"
label="变动类型"
width="120"
align="center"
>
</el-table-column>
<el-table-column
prop="relatedOrder"
label="关联订单"
align="center"
>
</el-table-column>
<el-table-column label="详情" width="100" align="center">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="viewPointsDetail(scope.row)"
>
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handlePointsSizeChange"
@current-change="handlePointsCurrentChange"
:current-page="pointsPagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pointsPagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pointsPagination.total"
style="margin-top: 20px; text-align: center"
>
</el-pagination>
</el-tab-pane>
<el-tab-pane label="会员优惠券" name="coupons">
<el-table
:data="couponsList"
border
style="width: 100%"
v-loading="couponsLoading"
>
<el-table-column
prop="couponName"
label="优惠券名称"
align="center"
>
</el-table-column>
<el-table-column
prop="couponType"
label="类型"
width="100"
align="center"
>
</el-table-column>
<el-table-column
prop="faceValue"
label="面额"
width="100"
align="center"
>
<template slot-scope="scope">
<span v-if="scope.row.couponType === '折扣券'"
>{{ scope.row.faceValue }}</span
>
<span v-else>¥{{ scope.row.faceValue }}</span>
</template>
</el-table-column>
<el-table-column
prop="useCondition"
label="使用条件"
width="120"
align="center"
>
</el-table-column>
<el-table-column
prop="receiveTime"
label="获得时间"
width="180"
align="center"
>
</el-table-column>
<el-table-column
prop="validPeriod"
label="有效期"
width="180"
align="center"
>
</el-table-column>
<el-table-column
prop="useStatus"
label="使用状态"
width="100"
align="center"
>
<template slot-scope="scope">
<el-tag
:type="getStatusTagType(scope.row.useStatus)"
size="small"
>
{{ scope.row.useStatus }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleCouponsSizeChange"
@current-change="handleCouponsCurrentChange"
:current-page="couponsPagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="couponsPagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="couponsPagination.total"
style="margin-top: 20px; text-align: center"
>
</el-pagination>
</el-tab-pane>
</el-tabs>
</div>
</template>
</obj-modal>
<!-- 成长值详情弹框 -->
<growth-detail ref="growthDetail"></growth-detail>
<!-- 积分详情弹框 -->
<points-detail ref="pointsDetail"></points-detail>
</div>
</template>
<script>
import { debounce, cloneDeep } from "lodash";
import { Divider } from "element-ui";
import { cloneDeep } from "lodash";
import GrowthDetail from "./growth-detail.vue";
import PointsDetail from "./points-detail.vue";
export default {
components: {},
components: {
GrowthDetail,
PointsDetail
},
data() {
return {
isAdd: true,
@ -65,9 +295,36 @@ export default {
modalConfig: {
title: "用户详情",
show: false,
width: "60%",
width: "80%"
},
modalData: {},
// Tab
activeTab: "growth",
couponCount: 0,
//
growthList: [],
growthLoading: false,
growthPagination: {
currentPage: 1,
pageSize: 10,
total: 0
},
//
pointsList: [],
pointsLoading: false,
pointsPagination: {
currentPage: 1,
pageSize: 10,
total: 0
},
//
couponsList: [],
couponsLoading: false,
couponsPagination: {
currentPage: 1,
pageSize: 10,
total: 0
}
};
},
watch: {
@ -78,10 +335,17 @@ export default {
this.$refs.modal.resetFields();
});
}
},
}
},
methods: {
queryTableData(pageNo, pageSize) {},
//
maskMobile(mobile) {
if (!mobile || mobile.length !== 11) {
return mobile;
}
return mobile.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
},
queryTableData() {},
toggle(e) {
if (this.modalConfig.show == false) {
this.modalConfig.show = true;
@ -98,12 +362,222 @@ export default {
},
update: () => {
this.isAdd = false;
},
}
};
},
init(row) {
this.modalData = row;
this.loadTabData();
},
// Tab
handleTabClick(tab) {
this.activeTab = tab.name;
this.loadTabData();
},
// Tab
loadTabData() {
switch (this.activeTab) {
case "growth":
this.loadGrowthData();
break;
case "points":
this.loadPointsData();
break;
case "coupons":
this.loadCouponsData();
break;
}
},
//
loadGrowthData() {
this.growthLoading = true;
// API
setTimeout(() => {
this.growthList = [
{
changeTime: "2025-07-24 17:20",
growthChange: 35,
relatedOrder: "ORD9296",
changeReason: "订单消费",
orderAmount: "89.99",
orderTime: "2025-07-24 17:20",
shopName: "美食餐厅"
},
{
changeTime: "2025-07-12 17:20",
growthChange: 100,
relatedOrder: "-",
changeReason: "活动奖励",
description: "参与平台活动获得的成长值奖励"
},
{
changeTime: "2024-01-05 16:45:33",
growthChange: 30,
relatedOrder: "ORDER202401503",
changeReason: "订单消费",
orderAmount: "65.50",
orderTime: "2024-01-05 16:45:33",
shopName: "便利超市"
}
];
this.growthPagination.total = 3;
this.growthLoading = false;
}, 500);
},
//
loadPointsData() {
this.pointsLoading = true;
// API
setTimeout(() => {
this.pointsList = [
{
changeTime: "2025-07-24 17:20",
pointsChange: 25,
changeType: "订单消费",
relatedOrder: "ORD9296",
changeReason: "订单消费",
orderAmount: "78.30",
orderTime: "2025-07-24 17:20",
shopName: "运动健身店"
},
{
changeTime: "2025-07-20 15:30",
pointsChange: -50,
changeType: "兑换商品",
relatedOrder: "-",
changeReason: "兑换商品",
exchangeProduct: "精美水杯",
exchangeQuantity: "1个",
usageDescription:
"使用50积分兑换商品兑换成功后积分将被扣除且不可退还。"
},
{
changeTime: "2025-07-12 17:20",
pointsChange: 80,
changeType: "完成任务",
relatedOrder: "-",
changeReason: "完成任务",
description: "完成平台指定任务获得的积分奖励"
},
{
changeTime: "2024-01-08 15:45:30",
pointsChange: 10,
changeType: "签到奖励",
relatedOrder: "-",
changeReason: "签到奖励",
description: "每日签到获得的积分奖励"
}
];
this.pointsPagination.total = 4;
this.pointsLoading = false;
}, 500);
},
//
loadCouponsData() {
this.couponsLoading = true;
// API
setTimeout(() => {
this.couponsList = [
{
couponName: "新用户专享券",
couponType: "满减券",
faceValue: 20,
useCondition: "满100元可用",
receiveTime: "2024-01-01 10:00:00",
validPeriod: "2024-02-01",
useStatus: "可使用"
},
{
couponName: "生日特惠券",
couponType: "折扣券",
faceValue: 9,
useCondition: "全品类可用",
receiveTime: "2024-01-05 12:30:15",
validPeriod: "2024-02-05",
useStatus: "可使用"
},
{
couponName: "积分兑换券",
couponType: "满减券",
faceValue: 50,
useCondition: "满300元可用",
receiveTime: "2024-01-10 16:20:30",
validPeriod: "2024-03-10",
useStatus: "可使用"
},
{
couponName: "限时抢购券",
couponType: "满减券",
faceValue: 10,
useCondition: "满50元可用",
receiveTime: "2023-12-20 14:15:20",
validPeriod: "2024-01-20",
useStatus: "已过期"
},
{
couponName: "会员专属券",
couponType: "满减券",
faceValue: 30,
useCondition: "满200元可用",
receiveTime: "2023-12-15 09:45:10",
validPeriod: "2024-01-15",
useStatus: "已使用"
}
];
this.couponsPagination.total = 5;
this.couponCount = this.couponsList.filter(
item => item.useStatus === "可使用"
).length;
this.couponsLoading = false;
}, 500);
},
//
handleGrowthSizeChange(val) {
this.growthPagination.pageSize = val;
this.loadGrowthData();
},
handleGrowthCurrentChange(val) {
this.growthPagination.currentPage = val;
this.loadGrowthData();
},
//
handlePointsSizeChange(val) {
this.pointsPagination.pageSize = val;
this.loadPointsData();
},
handlePointsCurrentChange(val) {
this.pointsPagination.currentPage = val;
this.loadPointsData();
},
//
handleCouponsSizeChange(val) {
this.couponsPagination.pageSize = val;
this.loadCouponsData();
},
handleCouponsCurrentChange(val) {
this.couponsPagination.currentPage = val;
this.loadCouponsData();
},
//
viewGrowthDetail(row) {
this.$refs.growthDetail.show(row);
},
viewPointsDetail(row) {
this.$refs.pointsDetail.show(row);
},
//
getStatusTagType(status) {
switch (status) {
case "可使用":
return "success";
case "已使用":
return "info";
case "已过期":
return "danger";
default:
return "";
}
}
},
computed: {
modalCols() {
@ -112,30 +586,70 @@ export default {
modalHandles() {
return [
{
label: "取消",
label: "关闭",
handle: () => {
this.toggle();
},
},
{
label: this.isAdd ? "确认添加" : "确认",
type: "primary",
loading: this.isLoading,
submit: true,
handle: () => {
console.log(JSON.parse(JSON.stringify(this.modalData)));
this.$emit(
"addCouponData",
JSON.parse(JSON.stringify(this.modalData))
);
this.toggle();
},
},
}
}
];
},
}
},
asyncComputed: {},
asyncComputed: {}
};
</script>
<style lang="scss" scoped>
</style>
.user-info-header {
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.user-basic-info {
flex: 1;
}
.info-row {
display: flex;
margin-bottom: 15px;
}
.info-item {
margin-right: 40px;
}
.label {
color: #606266;
font-weight: 500;
}
.value {
color: #303133;
margin-left: 5px;
}
.user-stats {
display: flex;
gap: 20px;
}
.stat-card {
text-align: center;
padding: 15px 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
min-width: 100px;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #409eff;
margin-bottom: 5px;
}
.stat-label {
font-size: 12px;
color: #909399;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<div class="test-page">
<div class="page-header">
<h2>会员详情弹框样式测试页面</h2>
<p>点击下面的按钮查看不同会员数据的详情弹框效果</p>
</div>
<div class="test-buttons">
<el-button type="primary" @click="showMemberDetail1">
查看会员详情 - 张三 (LV2会员)
</el-button>
<el-button type="success" @click="showMemberDetail2">
查看会员详情 - 李四 (LV3会员)
</el-button>
<el-button type="warning" @click="showMemberDetail3">
查看会员详情 - 王五 (LV1会员)
</el-button>
</div>
<div class="feature-list">
<h3>功能特点说明</h3>
<ul>
<li> 用户信息顶部展示会员姓名系统编号手机号(脱敏)生日</li>
<li> 右侧统计卡片会员成长值会员积分会员优惠券数量</li>
<li> 三个Tab分页会员成长值明细会员积分明细会员优惠券</li>
<li> 成长值明细变化时间成长值变动(带颜色)关联订单查看详情</li>
<li> 积分明细变化时间积分变动(带颜色)变动类型关联订单查看详情</li>
<li> 优惠券列表名称类型面额使用条件获得时间有效期使用状态(Tag)</li>
<li> 完整的分页功能</li>
<li> 响应式布局和美观的样式</li>
</ul>
</div>
<!-- 会员详情弹框组件 -->
<viewDetails ref="viewDetails"></viewDetails>
</div>
</template>
<script>
import viewDetails from "./modules/marketing/user/popup/view-details.vue";
export default {
name: "TestMemberDetail",
components: {
viewDetails
},
data() {
return {};
},
methods: {
// 1 -
showMemberDetail1() {
const testMemberData = {
userId: "3",
username: "张三",
mobile: "13812341234",
birthday: "1990-05-15",
headUrl: "https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png",
levelName: "LV2",
growthValue: 1250,
memberPoints: 890,
joinTime: "2023-01-15 10:30:00",
registrationTime: "2023-01-15 10:30:00",
id: 1
};
this.$refs.viewDetails.toggle(testMemberData).add();
},
// 2 -
showMemberDetail2() {
const testMemberData = {
userId: "15",
username: "李四",
mobile: "15987654321",
birthday: "1985-12-20",
headUrl: "https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png",
levelName: "LV3",
growthValue: 2800,
memberPoints: 1560,
joinTime: "2022-08-10 14:20:00",
registrationTime: "2022-08-10 14:20:00",
id: 2
};
this.$refs.viewDetails.toggle(testMemberData).add();
},
// 3 -
showMemberDetail3() {
const testMemberData = {
userId: "28",
username: "王五",
mobile: "18666888999",
birthday: "1995-03-08",
headUrl: "https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png",
levelName: "LV1",
growthValue: 350,
memberPoints: 120,
joinTime: "2024-01-05 09:15:00",
registrationTime: "2024-01-05 09:15:00",
id: 3
};
this.$refs.viewDetails.toggle(testMemberData).add();
}
}
};
</script>
<style lang="scss" scoped>
.test-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #303133;
margin-bottom: 10px;
}
p {
color: #606266;
font-size: 14px;
}
}
.test-buttons {
text-align: center;
margin-bottom: 40px;
.el-button {
margin: 0 10px 10px 0;
}
}
.feature-list {
background: #f5f7fa;
padding: 20px;
border-radius: 4px;
h3 {
color: #303133;
margin-bottom: 15px;
}
ul {
margin: 0;
padding-left: 20px;
li {
margin-bottom: 8px;
color: #606266;
line-height: 1.5;
}
}
}
</style>