From 747ab6fbb33d3a362f15d5f37d08e3d40b98dc31 Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Fri, 6 Feb 2026 17:58:02 +0800 Subject: [PATCH] feat: update member portrait and birthday features --- antis-ncc-admin/.env.development | 4 ++-- antis-ncc-admin/src/components/member-portrait-dialog.vue | 1381 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- antis-ncc-admin/src/views/lqKhxx/index.vue | 366 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- antis-ncc-admin/src/views/lqKhxxBirthday/index.vue | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- antis-ncc-admin/src/views/personalPerformanceStatistics/index.vue | 30 ++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxBirthdayOutput.cs | 5 +++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatisticsPersonalPerformance/LqStatisticsPersonalPerformanceListOutput.cs | 30 ++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatisticsPersonalPerformance/LqStatisticsPersonalPerformanceListQueryInput.cs | 5 +++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/MemberPortrait/MemberPortraitDtos.cs | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------- netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs | 45 +++++++++++++++++++++++++++++++++++++++++---- netcore/src/Modularity/Extend/NCC.Extend/MemberPortraitService.cs | 460 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ 12 files changed, 1956 insertions(+), 618 deletions(-) diff --git a/antis-ncc-admin/.env.development b/antis-ncc-admin/.env.development index 198955a..6462393 100644 --- a/antis-ncc-admin/.env.development +++ b/antis-ncc-admin/.env.development @@ -2,8 +2,8 @@ VUE_CLI_BABEL_TRANSPILE_MODULES = true # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com' -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' -# VUE_APP_BASE_API = 'http://localhost:2011' +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com' +VUE_APP_BASE_API = 'http://localhost:2011' # VUE_APP_BASE_API = 'http://localhost:2011' VUE_APP_IMG_API = '' VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket' diff --git a/antis-ncc-admin/src/components/member-portrait-dialog.vue b/antis-ncc-admin/src/components/member-portrait-dialog.vue index 4ddf5e6..c111d42 100644 --- a/antis-ncc-admin/src/components/member-portrait-dialog.vue +++ b/antis-ncc-admin/src/components/member-portrait-dialog.vue @@ -2,87 +2,73 @@
- -
-
+ +
+ +
-
-
-
-

{{ baseInfo.MemberName || '—' }}

- - {{ getConsumeLevelText(baseInfo.ConsumeLevel) }} - -
-
-
- - {{ baseInfo.Mobile || '—' }} -
-
- - {{ baseInfo.StoreName || '—' }} -
-
- - {{ baseInfo.Channel }} -
-
- - 编码:{{ baseInfo.MemberCode }} -
+

{{ baseInfo.MemberName || '—' }}

+ + {{ getConsumeLevelText(baseInfo.ConsumeLevel) }} + +
+
档案号:{{ baseInfo.MemberCode || '无' }}
+
手机号:{{ baseInfo.Mobile || '无' }}
+ + 修改资料 +
-
-
-
- 剩余权益 - ¥{{ formatMoney(behaviorSummary.RemainingRightsAmount) }} -
-
- 累计开单 - ¥{{ formatMoney(behaviorSummary.TotalBillingAmount) }} -
-
- 累计消耗 - ¥{{ formatMoney(behaviorSummary.TotalConsumeAmount) }} -
-
- 沉睡天数 - {{ baseInfo.SleepDays || 0 }} 天 + + +
+
+
性别:{{ baseInfo.Gender || '无' }}
+
年龄:{{ getDisplayAge() }}
+
客户类型:{{ baseInfo.CustomerTypeName || '无' }}
+
归属门店:{{ baseInfo.StoreName || '无' }}
+
进店渠道:{{ baseInfo.Channel || '无' }}
+
注册时间:{{ formatDate(baseInfo.RegisterTime) || '无' }}
+
生日:{{ formatBirthdayDisplay() }}
+
沉睡天数: + {{ baseInfo.SleepDays || 0 }} 天
+
剩余权益:¥{{ formatMoney(behaviorSummary.RemainingRightsAmount) }}
+
累计开单:¥{{ formatMoney(behaviorSummary.TotalBillingAmount) }}
+
累计消耗:¥{{ formatMoney(behaviorSummary.TotalConsumeAmount) }}
+
推荐人:{{ baseInfo.ReferrerName || '无' }}
+
拓客人员:{{ baseInfo.ExpandUserName || '无' }}
+
主健康师:{{ baseInfo.MainHealthUserName || '无' }}
+
负责顾问:{{ baseInfo.SubHealthUserName || '无' }}
+
首次到店:{{ formatDateTime(baseInfo.FirstVisitTime) || '无' }}
+
最后到店:{{ formatDateTime(baseInfo.LastVisitTime) || '无' }}
+
联系地址:{{ baseInfo.Address || '无' }}
+
备注:{{ baseInfo.Remark || '无' }}
- -
-
-
- - {{ type.TypeName }}会员 - - - - {{ formatDate(type.BecomeTime) }} - -
-
+ +
+ + {{ type.TypeName }}会员 + {{ formatDate(type.BecomeTime) }} +
- + -
+
-
-
+
+
- 消费行为 + 消费行为
-
+
@@ -169,24 +155,24 @@
- -
-
+ +
+
- 近12个月消费趋势 + 近12个月消费趋势
-
+
-
-
+
+
- 消费分析 + 消费分析
-
+
@@ -227,132 +213,334 @@ -
-
-
- - 权益明细 -
-
- - - - +
+ + + + - - - - - - + + + + + + -
+
+ + + + +
+ + + + + + + + + + + + +
+
- - -
-
-
- - 开单列表 -
-
- + + +
+ + + + + + + + + + + +
+ +
+
+
+ + + +
+ - - + + + + + + - + - + -
- -
-
+
+
- - -
-
-
- - 消耗列表 -
-
- - + + +
+ + - - + + + + + + - + + + + -
- -
-
+
+ +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+
+
+ + + +
+ + + + + + + + + + + + + + + + + +
+
-
-
-
- - 退卡列表 -
-
- +
+ - - + + - + - + -
- -
-
+
+
+ + + +
+
+ {{ formatDateTime(serviceLogImageRow.CreateTime) }} + {{ serviceLogImageRow.CreatorName || '—' }} +
+
+
+
服务前
+
+ +
+ + 暂无图片 +
+
+
+
+
服务后
+
+ +
+ + 暂无图片 +
+
+
+
+
+ 备注:{{ serviceLogImageRow.Remark }} +
+
+
+ + + +
+ +
+
+ + + +
+
+ +
+
+
+
+ {{ formatDateTime(row.CreateTime) }} + {{ row.CreatorName || '—' }} +
+
+
+
服务前
+
+ +
+ + 暂无图片 +
+
+
+
+
服务后
+
+ +
+ + 暂无图片 +
+
+
+
+
+ 备注:{{ row.Remark }} +
+
+ 科技部备注:{{ row.KjbRemark }} +
+
+
+
+
@@ -400,7 +588,33 @@ export default { pageIndex: 1, pageSize: 10, total: 0 - } + }, + // 预约记录 + appointmentList: [], + appointmentLoading: false, + appointmentPagination: { pageIndex: 1, pageSize: 10, total: 0 }, + // 邀约记录 + inviteList: [], + inviteLoading: false, + invitePagination: { pageIndex: 1, pageSize: 10, total: 0 }, + // 服务日志 + serviceLogList: [], + serviceLogLoading: false, + serviceLogPagination: { pageIndex: 1, pageSize: 10, total: 0 }, + // 旧日志 + oldLogList: [], + oldLogLoading: false, + oldLogPagination: { pageIndex: 1, pageSize: 10, total: 0 }, + // 服务日志图片预览 + serviceLogImageVisible: false, + serviceLogImageRow: null, + // 消耗记录-服务日志弹窗(与服务日志tab同款) + consumeServiceLogVisible: false, + consumeServiceLogList: [], + consumeServiceLogLoading: false, + // 旧日志-图片预览 + oldLogImageVisible: false, + oldLogImageUrls: [] } }, watch: { @@ -427,6 +641,14 @@ export default { this.fetchConsumeList() } else if (newVal === 'refund' && this.refundList.length === 0) { this.fetchRefundList() + } else if (newVal === 'appointment' && this.appointmentList.length === 0) { + this.fetchAppointmentList() + } else if (newVal === 'invite' && this.inviteList.length === 0) { + this.fetchInviteList() + } else if (newVal === 'serviceLog' && this.serviceLogList.length === 0) { + this.fetchServiceLogList() + } else if (newVal === 'oldLog') { + this.fetchOldLogList() } } }, @@ -444,8 +666,38 @@ export default { this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 } this.refundList = [] this.refundPagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.appointmentList = [] + this.appointmentPagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.inviteList = [] + this.invitePagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.serviceLogList = [] + this.serviceLogPagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.oldLogList = [] + this.oldLogPagination = { pageIndex: 1, pageSize: 10, total: 0 } this.activeTab = 'overview' }, + /** 年龄显示:优先用后端返回的 Age,否则根据阳历生日计算 */ + getDisplayAge() { + const info = this.baseInfo + if (!info) return '无' + let age = info.Age + if (age == null && info.Yanglsr) { + const birth = new Date(info.Yanglsr) + if (!isNaN(birth.getTime())) { + const now = new Date() + age = now.getFullYear() - birth.getFullYear() + if (now.getMonth() < birth.getMonth() || (now.getMonth() === birth.getMonth() && now.getDate() < birth.getDate())) { + age-- + } + } + } + return age != null ? age + '岁' : '无' + }, + /** 修改资料:关闭画像并触发父组件打开编辑 */ + handleEdit() { + this.$emit('edit', this.memberId) + this.$emit('update:visible', false) + }, async fetchData() { if (!this.memberId) { this.$message.warning('会员ID不能为空') @@ -459,13 +711,21 @@ export default { this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 } this.refundList = [] this.refundPagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.appointmentList = [] + this.appointmentPagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.inviteList = [] + this.invitePagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.serviceLogList = [] + this.serviceLogPagination = { pageIndex: 1, pageSize: 10, total: 0 } + this.oldLogList = [] + this.oldLogPagination = { pageIndex: 1, pageSize: 10, total: 0 } this.loading = true try { const res = await request({ url: '/api/Extend/MemberPortrait/overview', method: 'GET', - params: { memberId: this.memberId } + data: { memberId: this.memberId } }) if (res.code === 200 && res.data) { @@ -496,7 +756,7 @@ export default { const res = await request({ url: '/api/Extend/MemberPortrait/billing-list', method: 'GET', - params: { + data: { memberId: this.memberId, pageIndex: this.billingPagination.pageIndex, pageSize: this.billingPagination.pageSize @@ -524,7 +784,7 @@ export default { const res = await request({ url: '/api/Extend/MemberPortrait/consume-list', method: 'GET', - params: { + data: { memberId: this.memberId, pageIndex: this.consumePagination.pageIndex, pageSize: this.consumePagination.pageSize @@ -552,7 +812,7 @@ export default { const res = await request({ url: '/api/Extend/MemberPortrait/refund-list', method: 'GET', - params: { + data: { memberId: this.memberId, pageIndex: this.refundPagination.pageIndex, pageSize: this.refundPagination.pageSize @@ -599,6 +859,229 @@ export default { this.refundPagination.pageIndex = 1 this.fetchRefundList() }, + async fetchAppointmentList() { + if (!this.memberId) return + this.appointmentLoading = true + try { + const res = await request({ + url: '/api/Extend/MemberPortrait/appointment-list', + method: 'GET', + data: { + memberId: this.memberId, + pageIndex: this.appointmentPagination.pageIndex, + pageSize: this.appointmentPagination.pageSize + } + }) + if (res.code === 200 && res.data) { + this.appointmentList = res.data.List || [] + this.appointmentPagination.total = res.data.Total || 0 + } else { + this.$message.error(res.msg || '获取预约记录失败') + } + } catch (error) { + console.error('获取预约记录失败:', error) + this.$message.error('获取预约记录失败: ' + (error.message || '未知错误')) + } finally { + this.appointmentLoading = false + } + }, + handleAppointmentPageChange(page) { + this.appointmentPagination.pageIndex = page + this.fetchAppointmentList() + }, + handleAppointmentSizeChange(size) { + this.appointmentPagination.pageSize = size + this.appointmentPagination.pageIndex = 1 + this.fetchAppointmentList() + }, + async fetchInviteList() { + if (!this.memberId) return + this.inviteLoading = true + try { + const res = await request({ + url: '/api/Extend/MemberPortrait/invite-list', + method: 'GET', + data: { + memberId: this.memberId, + pageIndex: this.invitePagination.pageIndex, + pageSize: this.invitePagination.pageSize + } + }) + if (res.code === 200 && res.data) { + this.inviteList = res.data.List || [] + this.invitePagination.total = res.data.Total || 0 + } else { + this.$message.error(res.msg || '获取邀约记录失败') + } + } catch (error) { + console.error('获取邀约记录失败:', error) + this.$message.error('获取邀约记录失败: ' + (error.message || '未知错误')) + } finally { + this.inviteLoading = false + } + }, + handleInvitePageChange(page) { + this.invitePagination.pageIndex = page + this.fetchInviteList() + }, + handleInviteSizeChange(size) { + this.invitePagination.pageSize = size + this.invitePagination.pageIndex = 1 + this.fetchInviteList() + }, + async fetchServiceLogList() { + if (!this.memberId) return + this.serviceLogLoading = true + try { + const res = await request({ + url: '/api/Extend/MemberPortrait/service-log-list', + method: 'GET', + data: { + memberId: this.memberId, + pageIndex: this.serviceLogPagination.pageIndex, + pageSize: this.serviceLogPagination.pageSize + } + }) + if (res.code === 200 && res.data) { + this.serviceLogList = res.data.List || [] + this.serviceLogPagination.total = res.data.Total || 0 + } else { + this.$message.error(res.msg || '获取服务日志失败') + } + } catch (error) { + console.error('获取服务日志失败:', error) + this.$message.error('获取服务日志失败: ' + (error.message || '未知错误')) + } finally { + this.serviceLogLoading = false + } + }, + handleServiceLogPageChange(page) { + this.serviceLogPagination.pageIndex = page + this.fetchServiceLogList() + }, + handleServiceLogSizeChange(size) { + this.serviceLogPagination.pageSize = size + this.serviceLogPagination.pageIndex = 1 + this.fetchServiceLogList() + }, + async fetchOldLogList() { + const memberCode = this.baseInfo.MemberCode || '' + const mobile = this.baseInfo.Mobile || '' + if (!memberCode && !mobile) { + this.$message.warning('会员编号和手机号均无,无法查询旧日志') + return + } + + this.oldLogLoading = true + try { + const res = await request({ + url: '/api/Extend/MemberPortrait/old-log-list', + method: 'GET', + data: { + memberCode, + mobile, + pageIndex: this.oldLogPagination.pageIndex, + pageSize: this.oldLogPagination.pageSize + } + }) + + if (res.code === 200 && res.data) { + const list = res.data.List || res.data.list || [] + const total = (res.data.Total != null ? res.data.Total : res.data.total) || 0 + this.oldLogList = list + this.oldLogPagination.total = total + // 调试:输出接口返回数据 + console.log('[旧日志] 接口返回:', { data: res.data, list, total, firstItem: list[0] }) + } else { + this.$message.error(res.msg || '获取旧日志失败') + } + } catch (error) { + console.error('获取旧日志失败:', error) + this.$message.error('获取旧日志失败: ' + (error.message || '未知错误')) + } finally { + this.oldLogLoading = false + } + }, + handleOldLogPageChange(page) { + this.oldLogPagination.pageIndex = page + this.fetchOldLogList() + }, + handleOldLogSizeChange(size) { + this.oldLogPagination.pageSize = size + this.oldLogPagination.pageIndex = 1 + this.fetchOldLogList() + }, + showServiceLogImages(row) { + this.serviceLogImageRow = row + this.serviceLogImageVisible = true + }, + async showConsumeServiceLog(consumeId) { + this.consumeServiceLogVisible = true + this.consumeServiceLogLoading = true + this.consumeServiceLogList = [] + try { + const res = await request({ + url: `/api/Extend/lqxhfeedback/GetByConsumeId/${consumeId}`, + method: 'GET' + }) + if (res.code === 200 && res.data) { + const list = Array.isArray(res.data) ? res.data : [] + this.consumeServiceLogList = list.map(item => ({ + Id: item.id, + CreateTime: item.createTime, + CreatorName: item.createUserName, + Remark: item.remark, + KjbRemark: item.kjbRemark, + BeforeImage: item.beforeImage, + AfterImage: item.afterImage + })) + } + } catch (error) { + console.error('获取服务日志失败:', error) + this.$message.error('获取服务日志失败') + } finally { + this.consumeServiceLogLoading = false + } + }, + parseServiceLogImages(imageStr) { + if (!imageStr) return [] + try { + const arr = typeof imageStr === 'string' ? JSON.parse(imageStr) : imageStr + return Array.isArray(arr) ? arr : [] + } catch { + return [] + } + }, + getServiceLogImageUrl(img) { + if (!img || !img.url) return '' + const url = img.url + if (url.startsWith('http://') || url.startsWith('https://')) return url + const base = process.env.VUE_APP_IMG_API || process.env.VUE_APP_BASE_API || '' + return base ? (base.replace(/\/$/, '') + (url.startsWith('/') ? url : '/' + url)) : url + }, + getServiceLogPreviewList(imageStr) { + return this.parseServiceLogImages(imageStr).map(img => this.getServiceLogImageUrl(img)).filter(Boolean) + }, + hasServiceLogImages(row) { + const before = this.parseServiceLogImages(row && row.BeforeImage).length + const after = this.parseServiceLogImages(row && row.AfterImage).length + return before > 0 || after > 0 + }, + hasOldLogImage(row) { + const name = row && (row.imageName || row.ImageName) + if (!name || typeof name !== 'string') return false + return name.split(';').filter(s => s && s.trim()).length > 0 + }, + parseOldLogImageUrls(imageStr) { + if (!imageStr || typeof imageStr !== 'string') return [] + return imageStr.split(';').map(s => s.trim()).filter(Boolean) + }, + showOldLogImages(row) { + const urls = this.parseOldLogImageUrls(row && (row.imageName || row.ImageName)) + if (urls.length === 0) return + this.oldLogImageUrls = urls + this.oldLogImageVisible = true + }, renderTrendChart() { if (!this.$refs.trendChart) return @@ -677,10 +1160,17 @@ export default { if (amount === null || amount === undefined || amount === '') return '0.00' return Number(amount).toFixed(2) }, + sortNumber(prop) { + return (a, b) => { + const va = Number(a[prop]) || 0 + const vb = Number(b[prop]) || 0 + return va - vb + } + }, formatDateTime(timestamp) { if (!timestamp) return '无' - if (typeof timestamp === 'number') { - const date = new Date(timestamp) + const date = typeof timestamp === 'number' ? new Date(timestamp) : new Date(timestamp) + if (!isNaN(date.getTime())) { return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', @@ -693,8 +1183,8 @@ export default { }, formatDate(timestamp) { if (!timestamp) return '' - if (typeof timestamp === 'number') { - const date = new Date(timestamp) + const date = typeof timestamp === 'number' ? new Date(timestamp) : new Date(timestamp) + if (!isNaN(date.getTime())) { return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', @@ -703,15 +1193,27 @@ export default { } return timestamp }, + formatBirthdayDisplay() { + const info = this.baseInfo + if (!info) return '无' + if (info.BirthdayType === 1 && info.Yinlsr) return `${info.BirthdayTypeName || '农历'} ${info.Yinlsr}` + if (info.Yanglsr) { + const d = new Date(info.Yanglsr) + if (!isNaN(d.getTime())) return `${info.BirthdayTypeName || '阳历'} ${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日` + } + if (info.Yinlsr) return `农历 ${info.Yinlsr}` + return '无' + }, getConsumeLevelText(level) { const levelMap = { - 0: '普通', - 1: '银卡', - 2: '金卡', - 3: '钻石', - 4: 'VIP' + 0: 'D', + 1: 'C', + 2: 'B', + 3: 'A', + 4: 'A+', + 5: 'A++' } - return levelMap[level] || '普通' + return levelMap[level] || 'D' }, getConsumeLevelTagType(level) { const typeMap = { @@ -719,7 +1221,8 @@ export default { 1: '', 2: 'warning', 3: 'success', - 4: 'danger' + 4: 'success', + 5: 'danger' } return typeMap[level] || 'info' }, @@ -805,227 +1308,259 @@ export default { padding: 20px; background: #f5f7fa; color: #303133; - overflow-y: auto; - max-height: calc(90vh - 120px); + overflow: hidden; + height: calc(90vh - 120px); + display: flex; + flex-direction: column; } } .portrait-wrapper { - // 顶部会员信息卡片 - .portrait-header { - background: #ffffff; - border-radius: 8px; - padding: 20px; - margin-bottom: 16px; + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; + + // 中部左右分栏 + .portrait-main-panel { + flex-shrink: 0; display: flex; - align-items: flex-start; - gap: 20px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + gap: 24px; + margin-bottom: 16px; + background: linear-gradient(180deg, #fff 0%, #fafbfc 100%); + border-radius: 12px; + padding: 24px; border: 1px solid #e4e7ed; - transition: all 0.2s ease; - - &:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - } + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(64, 158, 255, 0.04); + min-height: 180px; - .header-avatar { + .panel-left { flex-shrink: 0; + width: 220px; + min-width: 220px; + display: flex; + flex-direction: column; + align-items: center; + padding-right: 24px; + border-right: 1px solid #ebeef5; .avatar-circle { - width: 72px; - height: 72px; - border-radius: 8px; + width: 64px; + height: 64px; + border-radius: 12px; background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%); display: flex; align-items: center; justify-content: center; color: #fff; - font-size: 32px; - box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - - &:hover { - transform: scale(1.05) rotate(5deg); - box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4); - background: linear-gradient(135deg, #66b1ff 0%, #409EFF 100%); - } + font-size: 28px; + margin-bottom: 12px; + box-shadow: 0 4px 12px rgba(64, 158, 255, 0.35); } - } - - .header-main { - flex: 1; - min-width: 0; - .member-name-row { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 16px; - - .member-name { - font-size: 24px; - font-weight: 600; - color: #303133; - margin: 0; - line-height: 1.2; - } - - .level-tag { - font-weight: 600; - } + .member-name { + font-size: 18px; + font-weight: 600; + color: #303133; + margin: 0 0 8px 0; + line-height: 1.3; + text-align: center; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - .member-meta { - display: flex; - flex-wrap: wrap; - gap: 20px; + .level-tag { + margin-bottom: 12px; + } - .meta-item { - font-size: 14px; - color: #606266; - display: flex; - align-items: center; - gap: 8px; + .left-meta { + width: 100%; + font-size: 13px; + color: #606266; + margin-bottom: 12px; - i { - color: #909399; - font-size: 16px; - } + .meta-row { + padding: 5px 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-radius: 6px; + background: #f8f9fa; - span { - line-height: 1.5; + &.phone { + color: #67C23A; + font-weight: 500; } } } + + .btn-edit { + width: 100%; + } } - .header-right { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 12px; - flex-shrink: 0; + .panel-right { + flex: 1; + min-width: 0; + overflow: hidden; - .header-stats { - display: flex; - gap: 12px; - flex-shrink: 0; + .attr-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px 20px; - .stat-card { - min-width: 110px; - padding: 12px 16px; - border-radius: 6px; - border: 1px solid #e4e7ed; + .attr-item { display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; - transition: all 0.2s ease; - background: #ffffff; + align-items: center; + font-size: 13px; + min-height: 28px; + min-width: 0; + padding: 4px 10px; + border-radius: 6px; + background: #f8f9fa; + transition: background 0.2s ease; &:hover { - border-color: #c0c4cc; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #f0f2f5; } - .stat-label-tag { - font-weight: 600; - font-size: 12px; - padding: 2px 10px; - margin: 0; - flex-shrink: 0; + &.attr-item-full { + grid-column: 1 / -1; + white-space: normal; + word-break: break-all; + padding: 8px 12px; } - .stat-value { - font-size: 16px; - font-weight: 600; - color: #303133; + &:not(.attr-item-full) { white-space: nowrap; - line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; } - &.stat-primary { - border-left: 3px solid #409EFF; - background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(102, 177, 255, 0.05) 100%); + .attr-label { + color: #909399; + flex-shrink: 0; + margin-right: 8px; + font-size: 12px; } - &.stat-success { - border-left: 3px solid #67C23A; - background: linear-gradient(135deg, rgba(103, 194, 58, 0.08) 0%, rgba(133, 206, 97, 0.05) 100%); - } + .attr-value { + color: #303133; + overflow: hidden; + text-overflow: ellipsis; - &.stat-info { - border-left: 3px solid #909399; - background: linear-gradient(135deg, rgba(144, 147, 153, 0.08) 0%, rgba(169, 172, 178, 0.05) 100%); - } + &.highlight { + font-weight: 600; + color: #303133; + } - &.stat-warning { - border-left: 3px solid #E6A23C; - background: linear-gradient(135deg, rgba(230, 162, 60, 0.08) 0%, rgba(240, 180, 90, 0.05) 100%); + &.primary { + color: #409EFF; + font-weight: 600; + } } - &.stat-default { - border-left: 3px solid #409EFF; - background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(102, 177, 255, 0.05) 100%); + .el-tag { + flex-shrink: 0; } } } - .member-types-section { + .member-types-row { + margin-top: 14px; + padding-top: 14px; + border-top: 1px dashed #e4e7ed; display: flex; - align-items: center; - gap: 12px; flex-wrap: wrap; + gap: 8px 16px; - .member-types-label { - font-size: 13px; - color: #909399; - display: flex; + .member-type-wrap { + display: inline-flex; align-items: center; gap: 6px; - flex-shrink: 0; + padding: 4px 10px; + background: #f8f9fa; + border-radius: 6px; + transition: background 0.2s ease; - i { - color: #409EFF; - font-size: 14px; + &:hover { + background: #f0f2f5; + } + + .type-date { + font-size: 12px; + color: #909399; } } + } + } + } - .member-types-list { + // 个人档案样式(与 detail-dialog 一致) + .profile-layout { + .detail-section { + margin-bottom: 20px; + background: #fff; + border-radius: 8px; + overflow: hidden; + border: 1px solid #ebeef5; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); + + .section-title { + background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); + color: #fff; + padding: 12px 20px; + font-size: 15px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + + i { + font-size: 18px; + } + } + + .section-content { + padding: 20px; + + .info-row { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 20px 40px; - .member-type-badge { + .info-item { + flex: 0 0 calc(33.333% - 27px); + min-width: 220px; display: flex; align-items: center; - gap: 8px; - padding: 6px 12px; - background: #f5f7fa; - border-radius: 6px; - border: 1px solid #e4e7ed; - transition: all 0.2s ease; - - &:hover { - background: #f0f2f5; - border-color: #c0c4cc; + padding: 6px 0; + + &.info-item-full { + flex: 1 1 100%; + min-width: 100%; } - .member-type-tag { - font-weight: 600; - font-size: 12px; - padding: 2px 10px; + .label { + color: #606266; + font-weight: 500; + margin-right: 8px; + flex-shrink: 0; } - .member-type-date { - font-size: 12px; - color: #909399; - display: flex; - align-items: center; - gap: 4px; + .value { + color: #303133; + + &.highlight { + font-weight: 600; + color: #303133; + } - i { - font-size: 12px; + &.primary { + color: #409EFF; } } } @@ -1037,6 +1572,28 @@ export default { // 选项卡样式 .portrait-tabs { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + + ::v-deep .el-tabs__content { + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + } + + ::v-deep .el-tab-pane { + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + } + ::v-deep .el-tabs__header { margin-bottom: 16px; background: #ffffff; @@ -1075,8 +1632,23 @@ export default { } .tab-content { - height: 40vh; - overflow-y: scroll; + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + + &.tab-content-direct { + padding: 18px; + background: #fff; + border-radius: 8px; + border: 1px solid #e4e7ed; + + .pagination-bar { + padding: 12px 0 0; + margin-top: 12px; + border-top: 1px solid #e4e7ed; + } + } .content-card { background: #ffffff; border-radius: 8px; @@ -1339,3 +1911,166 @@ export default { } } + + diff --git a/antis-ncc-admin/src/views/lqKhxx/index.vue b/antis-ncc-admin/src/views/lqKhxx/index.vue index 17abe4d..79015f4 100644 --- a/antis-ncc-admin/src/views/lqKhxx/index.vue +++ b/antis-ncc-admin/src/views/lqKhxx/index.vue @@ -217,40 +217,28 @@
- - 手机号 - - {{ item.sjh || '-' }} + 手机号 + {{ item.sjh || '无' }}
- - 归属门店 - - {{ - item.gsmdName || - '-' - }} + 归属门店 + {{ item.gsmdName || '无' }}
- - 客户类型 - - {{ item.khlxName || '-' }} + 客户类型 + {{ item.khlxName || '无' }}
- - 沉睡天数 - - {{ - item.sleepDays || 0 - }}天 + 沉睡 + {{ (item.sleepDays != null ? item.sleepDays : 0) }}天
-
- - 最后到店 - - {{ formatDate(item.lastVisitTime) }} +
+ 生日 + {{ formatBirthdayDisplay(item) }} +
+
+ 最后到店 + {{ formatDate(item.lastVisitTime) }}
@@ -364,6 +352,16 @@ + + + + + :member-id="memberPortraitDialog.memberId" + @edit="handlePortraitEdit" />