Commit 747ab6fbb33d3a362f15d5f37d08e3d40b98dc31

Authored by “wangming”
1 parent 9a267e98

feat: update member portrait and birthday features

- Swapped the API base URL in the development environment to use localhost for local testing.
- Enhanced the member portrait dialog with improved layout and additional member attributes, including age and birthday type.
- Added birthday type filtering options in the birthday view, allowing users to display members with solar and lunar birthdays.
- Updated the backend to support querying members based on specified date ranges and birthday types.
- Introduced new performance metrics in the personal performance statistics view, categorizing performance by various service types.
antis-ncc-admin/.env.development
@@ -2,8 +2,8 @@ @@ -2,8 +2,8 @@
2 2
3 VUE_CLI_BABEL_TRANSPILE_MODULES = true 3 VUE_CLI_BABEL_TRANSPILE_MODULES = true
4 # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com' 4 # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com'
5 -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'  
6 -# VUE_APP_BASE_API = 'http://localhost:2011' 5 +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
  6 +VUE_APP_BASE_API = 'http://localhost:2011'
7 # VUE_APP_BASE_API = 'http://localhost:2011' 7 # VUE_APP_BASE_API = 'http://localhost:2011'
8 VUE_APP_IMG_API = '' 8 VUE_APP_IMG_API = ''
9 VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket' 9 VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket'
antis-ncc-admin/src/components/member-portrait-dialog.vue
@@ -2,87 +2,73 @@ @@ -2,87 +2,73 @@
2 <el-dialog :visible.sync="visibleSync" title="会员画像" :width="'1500px'" append-to-body top="8vh" 2 <el-dialog :visible.sync="visibleSync" title="会员画像" :width="'1500px'" append-to-body top="8vh"
3 custom-class="member-portrait-dialog" :close-on-click-modal="false" @closed="handleClosed"> 3 custom-class="member-portrait-dialog" :close-on-click-modal="false" @closed="handleClosed">
4 <div v-loading="loading" class="portrait-wrapper"> 4 <div v-loading="loading" class="portrait-wrapper">
5 - <!-- 顶部:会员核心信息卡片 -->  
6 - <div class="portrait-header">  
7 - <div class="header-avatar"> 5 + <!-- 中部:左右分栏(参考个人档案排版) -->
  6 + <div class="portrait-main-panel">
  7 + <!-- 左侧:头像 + 姓名 + 关键信息 + 操作 -->
  8 + <div class="panel-left">
8 <div class="avatar-circle"> 9 <div class="avatar-circle">
9 <i class="el-icon-user-solid"></i> 10 <i class="el-icon-user-solid"></i>
10 </div> 11 </div>
11 - </div>  
12 - <div class="header-main">  
13 - <div class="member-name-row">  
14 - <h2 class="member-name">{{ baseInfo.MemberName || '—' }}</h2>  
15 - <el-tag v-if="baseInfo.ConsumeLevel !== undefined" :type="getConsumeLevelTagType(baseInfo.ConsumeLevel)" size="small" class="level-tag">  
16 - {{ getConsumeLevelText(baseInfo.ConsumeLevel) }}  
17 - </el-tag>  
18 - </div>  
19 - <div class="member-meta">  
20 - <div class="meta-item">  
21 - <i class="el-icon-phone"></i>  
22 - <span>{{ baseInfo.Mobile || '—' }}</span>  
23 - </div>  
24 - <div class="meta-item">  
25 - <i class="el-icon-office-building"></i>  
26 - <span>{{ baseInfo.StoreName || '—' }}</span>  
27 - </div>  
28 - <div class="meta-item" v-if="baseInfo.Channel">  
29 - <i class="el-icon-connection"></i>  
30 - <span>{{ baseInfo.Channel }}</span>  
31 - </div>  
32 - <div class="meta-item" v-if="baseInfo.MemberCode">  
33 - <i class="el-icon-tickets"></i>  
34 - <span>编码:{{ baseInfo.MemberCode }}</span>  
35 - </div> 12 + <h2 class="member-name">{{ baseInfo.MemberName || '—' }}</h2>
  13 + <el-tag v-if="baseInfo.ConsumeLevel !== undefined" :type="getConsumeLevelTagType(baseInfo.ConsumeLevel)" size="small" class="level-tag">
  14 + {{ getConsumeLevelText(baseInfo.ConsumeLevel) }}
  15 + </el-tag>
  16 + <div class="left-meta">
  17 + <div class="meta-row">档案号:{{ baseInfo.MemberCode || '无' }}</div>
  18 + <div class="meta-row phone">手机号:{{ baseInfo.Mobile || '无' }}</div>
36 </div> 19 </div>
  20 + <el-button type="primary" size="small" icon="el-icon-edit" class="btn-edit" @click="handleEdit">
  21 + 修改资料
  22 + </el-button>
37 </div> 23 </div>
38 - <div class="header-right">  
39 - <div class="header-stats">  
40 - <div class="stat-card stat-primary">  
41 - <el-tag type="primary" size="small" class="stat-label-tag">剩余权益</el-tag>  
42 - <span class="stat-value">¥{{ formatMoney(behaviorSummary.RemainingRightsAmount) }}</span>  
43 - </div>  
44 - <div class="stat-card stat-success">  
45 - <el-tag type="success" size="small" class="stat-label-tag">累计开单</el-tag>  
46 - <span class="stat-value">¥{{ formatMoney(behaviorSummary.TotalBillingAmount) }}</span>  
47 - </div>  
48 - <div class="stat-card stat-info">  
49 - <el-tag type="info" size="small" class="stat-label-tag">累计消耗</el-tag>  
50 - <span class="stat-value">¥{{ formatMoney(behaviorSummary.TotalConsumeAmount) }}</span>  
51 - </div>  
52 - <div class="stat-card" :class="baseInfo.SleepDays > 30 ? 'stat-warning' : 'stat-default'">  
53 - <el-tag :type="baseInfo.SleepDays > 30 ? 'warning' : 'info'" size="small" class="stat-label-tag">沉睡天数</el-tag>  
54 - <span class="stat-value">{{ baseInfo.SleepDays || 0 }} 天</span> 24 +
  25 + <!-- 右侧:3 列属性网格 -->
  26 + <div class="panel-right">
  27 + <div class="attr-grid">
  28 + <div class="attr-item"><span class="attr-label">性别:</span><span class="attr-value">{{ baseInfo.Gender || '无' }}</span></div>
  29 + <div class="attr-item"><span class="attr-label">年龄:</span><span class="attr-value">{{ getDisplayAge() }}</span></div>
  30 + <div class="attr-item"><span class="attr-label">客户类型:</span><span class="attr-value">{{ baseInfo.CustomerTypeName || '无' }}</span></div>
  31 + <div class="attr-item"><span class="attr-label">归属门店:</span><span class="attr-value">{{ baseInfo.StoreName || '无' }}</span></div>
  32 + <div class="attr-item"><span class="attr-label">进店渠道:</span><span class="attr-value">{{ baseInfo.Channel || '无' }}</span></div>
  33 + <div class="attr-item"><span class="attr-label">注册时间:</span><span class="attr-value">{{ formatDate(baseInfo.RegisterTime) || '无' }}</span></div>
  34 + <div class="attr-item"><span class="attr-label">生日:</span><span class="attr-value">{{ formatBirthdayDisplay() }}</span></div>
  35 + <div class="attr-item"><span class="attr-label">沉睡天数:</span>
  36 + <el-tag :type="baseInfo.SleepDays > 30 ? 'warning' : 'info'" size="mini">{{ baseInfo.SleepDays || 0 }} 天</el-tag>
55 </div> 37 </div>
  38 + <div class="attr-item"><span class="attr-label">剩余权益:</span><span class="attr-value highlight primary">¥{{ formatMoney(behaviorSummary.RemainingRightsAmount) }}</span></div>
  39 + <div class="attr-item"><span class="attr-label">累计开单:</span><span class="attr-value highlight">¥{{ formatMoney(behaviorSummary.TotalBillingAmount) }}</span></div>
  40 + <div class="attr-item"><span class="attr-label">累计消耗:</span><span class="attr-value highlight">¥{{ formatMoney(behaviorSummary.TotalConsumeAmount) }}</span></div>
  41 + <div class="attr-item"><span class="attr-label">推荐人:</span><span class="attr-value">{{ baseInfo.ReferrerName || '无' }}</span></div>
  42 + <div class="attr-item"><span class="attr-label">拓客人员:</span><span class="attr-value">{{ baseInfo.ExpandUserName || '无' }}</span></div>
  43 + <div class="attr-item"><span class="attr-label">主健康师:</span><span class="attr-value">{{ baseInfo.MainHealthUserName || '无' }}</span></div>
  44 + <div class="attr-item"><span class="attr-label">负责顾问:</span><span class="attr-value">{{ baseInfo.SubHealthUserName || '无' }}</span></div>
  45 + <div class="attr-item"><span class="attr-label">首次到店:</span><span class="attr-value">{{ formatDateTime(baseInfo.FirstVisitTime) || '无' }}</span></div>
  46 + <div class="attr-item"><span class="attr-label">最后到店:</span><span class="attr-value">{{ formatDateTime(baseInfo.LastVisitTime) || '无' }}</span></div>
  47 + <div class="attr-item attr-item-full"><span class="attr-label">联系地址:</span><span class="attr-value">{{ baseInfo.Address || '无' }}</span></div>
  48 + <div class="attr-item attr-item-full"><span class="attr-label">备注:</span><span class="attr-value">{{ baseInfo.Remark || '无' }}</span></div>
56 </div> 49 </div>
57 - <!-- 会员类型 -->  
58 - <div class="member-types-section" v-if="baseInfo.MemberTypes && baseInfo.MemberTypes.length > 0">  
59 - <div class="member-types-list">  
60 - <div v-for="type in baseInfo.MemberTypes" :key="type.TypeName" class="member-type-badge">  
61 - <el-tag :type="getMemberTypeTagType(type.TypeName)" size="small" class="member-type-tag">  
62 - {{ type.TypeName }}会员  
63 - </el-tag>  
64 - <span class="member-type-date" v-if="type.BecomeTime">  
65 - <i class="el-icon-calendar"></i>  
66 - {{ formatDate(type.BecomeTime) }}  
67 - </span>  
68 - </div>  
69 - </div> 50 + <!-- 会员类型标签 -->
  51 + <div v-if="baseInfo.MemberTypes && baseInfo.MemberTypes.length > 0" class="member-types-row">
  52 + <span v-for="type in baseInfo.MemberTypes" :key="type.TypeName" class="member-type-wrap">
  53 + <el-tag :type="getMemberTypeTagType(type.TypeName)" size="small">{{ type.TypeName }}会员</el-tag>
  54 + <span v-if="type.BecomeTime" class="type-date">{{ formatDate(type.BecomeTime) }}</span>
  55 + </span>
70 </div> 56 </div>
71 </div> 57 </div>
72 </div> 58 </div>
73 59
74 <!-- 选项卡内容 --> 60 <!-- 选项卡内容 -->
75 <el-tabs v-model="activeTab" class="portrait-tabs"> 61 <el-tabs v-model="activeTab" class="portrait-tabs">
76 - <!-- 概览 --> 62 + <!-- 概览:消费行为、趋势、分析(个人档案已在顶部面板展示) -->
77 <el-tab-pane label="概览" name="overview"> 63 <el-tab-pane label="概览" name="overview">
78 - <div class="tab-content"> 64 + <div class="tab-content profile-layout">
79 <!-- 消费行为 --> 65 <!-- 消费行为 -->
80 - <div class="content-card">  
81 - <div class="card-header"> 66 + <div class="detail-section">
  67 + <div class="section-title">
82 <i class="el-icon-shopping-cart-full"></i> 68 <i class="el-icon-shopping-cart-full"></i>
83 - <span class="card-title">消费行为</span> 69 + <span>消费行为</span>
84 </div> 70 </div>
85 - <div class="card-body"> 71 + <div class="section-content">
86 <div class="behavior-grid"> 72 <div class="behavior-grid">
87 <div class="behavior-item"> 73 <div class="behavior-item">
88 <div class="behavior-icon"> 74 <div class="behavior-icon">
@@ -169,24 +155,24 @@ @@ -169,24 +155,24 @@
169 </div> 155 </div>
170 </div> 156 </div>
171 157
172 - <!-- 近12个月趋势图 -->  
173 - <div class="content-card">  
174 - <div class="card-header"> 158 + <!-- 近12个月消费趋势 -->
  159 + <div class="detail-section">
  160 + <div class="section-title">
175 <i class="el-icon-data-line"></i> 161 <i class="el-icon-data-line"></i>
176 - <span class="card-title">近12个月消费趋势</span> 162 + <span>近12个月消费趋势</span>
177 </div> 163 </div>
178 - <div class="card-body"> 164 + <div class="section-content">
179 <div ref="trendChart" class="trend-chart"></div> 165 <div ref="trendChart" class="trend-chart"></div>
180 </div> 166 </div>
181 </div> 167 </div>
182 168
183 <!-- 消费分析 --> 169 <!-- 消费分析 -->
184 - <div v-if="consumptionAnalysis" class="content-card">  
185 - <div class="card-header"> 170 + <div v-if="consumptionAnalysis" class="detail-section">
  171 + <div class="section-title">
186 <i class="el-icon-data-analysis"></i> 172 <i class="el-icon-data-analysis"></i>
187 - <span class="card-title">消费分析</span> 173 + <span>消费分析</span>
188 </div> 174 </div>
189 - <div class="card-body"> 175 + <div class="section-content">
190 <div class="analysis-layout"> 176 <div class="analysis-layout">
191 <div class="analysis-item"> 177 <div class="analysis-item">
192 <div class="analysis-icon"> 178 <div class="analysis-icon">
@@ -227,132 +213,334 @@ @@ -227,132 +213,334 @@
227 213
228 <!-- 权益明细 --> 214 <!-- 权益明细 -->
229 <el-tab-pane label="权益明细" name="assets"> 215 <el-tab-pane label="权益明细" name="assets">
230 - <div class="tab-content">  
231 - <div class="content-card">  
232 - <div class="card-header">  
233 - <i class="el-icon-wallet"></i>  
234 - <span class="card-title">权益明细</span>  
235 - </div>  
236 - <div class="card-body">  
237 - <el-table :data="remainingItems" size="small" border stripe>  
238 - <el-table-column prop="ItemName" label="品项名称" min-width="140" />  
239 - <el-table-column prop="SourceType" label="来源类型" width="90" />  
240 - <el-table-column prop="UnitPrice" label="单价" width="90"> 216 + <div class="tab-content tab-content-direct">
  217 + <el-table :data="remainingItems" size="small" border stripe :default-sort="{ prop: 'RemainingValue', order: 'descending' }">
  218 + <el-table-column prop="ItemName" label="品项名称" min-width="140" sortable />
  219 + <el-table-column prop="SourceType" label="来源类型" width="90" sortable />
  220 + <el-table-column prop="UnitPrice" label="单价" width="90" sortable :sort-method="sortNumber('UnitPrice')">
241 <template slot-scope="scope">¥{{ formatMoney(scope.row.UnitPrice) }}</template> 221 <template slot-scope="scope">¥{{ formatMoney(scope.row.UnitPrice) }}</template>
242 </el-table-column> 222 </el-table-column>
243 - <el-table-column prop="TotalQuantity" label="总数量" width="80" />  
244 - <el-table-column prop="ConsumedQuantity" label="已消费" width="80" />  
245 - <el-table-column prop="RefundedQuantity" label="已退款" width="80" />  
246 - <el-table-column prop="DeductedQuantity" label="已扣除" width="80" />  
247 - <el-table-column prop="RemainingQuantity" label="剩余" width="80" />  
248 - <el-table-column prop="RemainingValue" label="剩余价值" width="120"> 223 + <el-table-column prop="TotalQuantity" label="总数量" width="80" sortable :sort-method="sortNumber('TotalQuantity')" />
  224 + <el-table-column prop="ConsumedQuantity" label="已消费" width="80" sortable :sort-method="sortNumber('ConsumedQuantity')" />
  225 + <el-table-column prop="RefundedQuantity" label="已退款" width="80" sortable :sort-method="sortNumber('RefundedQuantity')" />
  226 + <el-table-column prop="DeductedQuantity" label="已扣除" width="80" sortable :sort-method="sortNumber('DeductedQuantity')" />
  227 + <el-table-column prop="RemainingQuantity" label="剩余" width="80" sortable :sort-method="sortNumber('RemainingQuantity')" />
  228 + <el-table-column prop="RemainingValue" label="剩余价值" width="120" sortable :sort-method="sortNumber('RemainingValue')">
249 <template slot-scope="scope">¥{{ formatMoney(scope.row.RemainingValue) }}</template> 229 <template slot-scope="scope">¥{{ formatMoney(scope.row.RemainingValue) }}</template>
250 </el-table-column> 230 </el-table-column>
251 </el-table> 231 </el-table>
252 - </div> 232 + </div>
  233 + </el-tab-pane>
  234 +
  235 + <!-- 邀约记录 -->
  236 + <el-tab-pane label="邀约记录" name="invite">
  237 + <div class="tab-content tab-content-direct">
  238 + <el-table v-loading="inviteLoading" :data="inviteList" size="small" border stripe>
  239 + <el-table-column prop="InviteDate" label="邀约时间" width="160">
  240 + <template slot-scope="scope">{{ formatDateTime(scope.row.InviteDate) }}</template>
  241 + </el-table-column>
  242 + <el-table-column prop="StoreName" label="门店" width="120" />
  243 + <el-table-column prop="InviterName" label="邀约人" width="100" />
  244 + <el-table-column prop="ContactTime" label="联系时间" width="160">
  245 + <template slot-scope="scope">{{ formatDateTime(scope.row.ContactTime) }}</template>
  246 + </el-table-column>
  247 + <el-table-column prop="ContactRecord" label="联系记录" min-width="200" show-overflow-tooltip />
  248 + <el-table-column prop="PhoneValid" label="电话有效" width="90" />
  249 + </el-table>
  250 + <div class="pagination-bar">
  251 + <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"
  252 + :total="invitePagination.total" :current-page="invitePagination.pageIndex"
  253 + :page-size="invitePagination.pageSize" @size-change="handleInviteSizeChange"
  254 + @current-change="handleInvitePageChange" />
253 </div> 255 </div>
254 </div> 256 </div>
255 </el-tab-pane> 257 </el-tab-pane>
256 258
257 - <!-- 开单列表 -->  
258 - <el-tab-pane label="开单列表" name="billing">  
259 - <div class="tab-content">  
260 - <div class="content-card">  
261 - <div class="card-header">  
262 - <i class="el-icon-document"></i>  
263 - <span class="card-title">开单列表</span>  
264 - </div>  
265 - <div class="card-body">  
266 - <el-table v-loading="billingLoading" :data="billingList" size="small" border stripe> 259 + <!-- 预约记录 -->
  260 + <el-tab-pane label="预约记录" name="appointment">
  261 + <div class="tab-content tab-content-direct">
  262 + <el-table v-loading="appointmentLoading" :data="appointmentList" size="small" border stripe>
  263 + <el-table-column prop="AppointmentDate" label="预约时间" width="160">
  264 + <template slot-scope="scope">{{ formatDateTime(scope.row.AppointmentDate) }}</template>
  265 + </el-table-column>
  266 + <el-table-column prop="StoreName" label="门店" width="120" />
  267 + <el-table-column prop="InviterName" label="邀约人" width="100" />
  268 + <el-table-column prop="HealthCoachName" label="预约健康师" width="100" />
  269 + <el-table-column prop="ExperienceItem" label="体验项目" min-width="120" show-overflow-tooltip />
  270 + <el-table-column prop="Status" label="状态" width="80" />
  271 + <el-table-column prop="NoDealRemark" label="未成交说明" min-width="150" show-overflow-tooltip />
  272 + </el-table>
  273 + <div class="pagination-bar">
  274 + <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"
  275 + :total="appointmentPagination.total" :current-page="appointmentPagination.pageIndex"
  276 + :page-size="appointmentPagination.pageSize" @size-change="handleAppointmentSizeChange"
  277 + @current-change="handleAppointmentPageChange" />
  278 + </div>
  279 + </div>
  280 + </el-tab-pane>
  281 +
  282 + <!-- 开单记录 -->
  283 + <el-tab-pane label="开单记录" name="billing">
  284 + <div class="tab-content tab-content-direct">
  285 + <el-table v-loading="billingLoading" :data="billingList" size="small" border stripe>
267 <el-table-column prop="BillingDate" label="开单日期" width="160"> 286 <el-table-column prop="BillingDate" label="开单日期" width="160">
268 <template slot-scope="scope">{{ formatDateTime(scope.row.BillingDate) }}</template> 287 <template slot-scope="scope">{{ formatDateTime(scope.row.BillingDate) }}</template>
269 </el-table-column> 288 </el-table-column>
270 - <el-table-column prop="StoreName" label="门店" width="150" />  
271 - <el-table-column prop="Amount" label="实付金额" width="120"> 289 + <el-table-column prop="StoreName" label="门店" width="100" />
  290 + <el-table-column prop="CreatorName" label="开单人员" width="90" show-overflow-tooltip />
  291 + <el-table-column prop="HealthCoachNames" label="健康师" width="100" show-overflow-tooltip />
  292 + <el-table-column prop="TechTeacherNames" label="科技老师" width="100" show-overflow-tooltip />
  293 + <el-table-column prop="Items" label="开单品项" min-width="180" show-overflow-tooltip />
  294 + <el-table-column prop="Amount" label="实付金额" width="90">
272 <template slot-scope="scope">¥{{ formatMoney(scope.row.Amount) }}</template> 295 <template slot-scope="scope">¥{{ formatMoney(scope.row.Amount) }}</template>
273 </el-table-column> 296 </el-table-column>
274 - <el-table-column prop="DebtAmount" label="欠款金额" width="120"> 297 + <el-table-column prop="DebtAmount" label="欠款金额" width="90">
275 <template slot-scope="scope">¥{{ formatMoney(scope.row.DebtAmount) }}</template> 298 <template slot-scope="scope">¥{{ formatMoney(scope.row.DebtAmount) }}</template>
276 </el-table-column> 299 </el-table-column>
277 - <el-table-column prop="ActivityName" label="活动名称" min-width="150" /> 300 + <el-table-column prop="ActivityName" label="活动名称" width="100" show-overflow-tooltip />
278 </el-table> 301 </el-table>
279 - <div class="pagination-bar">  
280 - <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"  
281 - :total="billingPagination.total" :current-page="billingPagination.pageIndex"  
282 - :page-size="billingPagination.pageSize" @size-change="handleBillingSizeChange"  
283 - @current-change="handleBillingPageChange" />  
284 - </div>  
285 - </div> 302 + <div class="pagination-bar">
  303 + <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"
  304 + :total="billingPagination.total" :current-page="billingPagination.pageIndex"
  305 + :page-size="billingPagination.pageSize" @size-change="handleBillingSizeChange"
  306 + @current-change="handleBillingPageChange" />
286 </div> 307 </div>
287 </div> 308 </div>
288 </el-tab-pane> 309 </el-tab-pane>
289 310
290 - <!-- 消耗列表 -->  
291 - <el-tab-pane label="消耗列表" name="consume">  
292 - <div class="tab-content">  
293 - <div class="content-card">  
294 - <div class="card-header">  
295 - <i class="el-icon-goods"></i>  
296 - <span class="card-title">消耗列表</span>  
297 - </div>  
298 - <div class="card-body">  
299 - <el-table v-loading="consumeLoading" :data="consumeList" size="small" border stripe>  
300 - <el-table-column prop="ConsumeDate" label="消耗日期"> 311 + <!-- 消耗记录 -->
  312 + <el-tab-pane label="消耗记录" name="consume">
  313 + <div class="tab-content tab-content-direct">
  314 + <el-table v-loading="consumeLoading" :data="consumeList" size="small" border stripe>
  315 + <el-table-column prop="ConsumeDate" label="消耗日期" width="160">
301 <template slot-scope="scope">{{ formatDateTime(scope.row.ConsumeDate) }}</template> 316 <template slot-scope="scope">{{ formatDateTime(scope.row.ConsumeDate) }}</template>
302 </el-table-column> 317 </el-table-column>
303 - <el-table-column prop="StoreName" label="门店" />  
304 - <el-table-column prop="Amount" label="消耗金额" > 318 + <el-table-column prop="StoreName" label="门店" width="100" />
  319 + <el-table-column prop="OperatorName" label="操作人员" width="90" show-overflow-tooltip />
  320 + <el-table-column prop="HealthCoachNames" label="健康师" width="100" show-overflow-tooltip />
  321 + <el-table-column prop="TechTeacherNames" label="科技老师" width="100" show-overflow-tooltip />
  322 + <el-table-column prop="Items" label="消耗品项" min-width="180" show-overflow-tooltip />
  323 + <el-table-column prop="Amount" label="消耗金额" width="90">
305 <template slot-scope="scope">¥{{ formatMoney(scope.row.Amount) }}</template> 324 <template slot-scope="scope">¥{{ formatMoney(scope.row.Amount) }}</template>
306 </el-table-column> 325 </el-table-column>
307 - <el-table-column prop="LaborCost" label="手工费" > 326 + <el-table-column prop="LaborCost" label="手工费" width="90">
308 <template slot-scope="scope">¥{{ formatMoney(scope.row.LaborCost) }}</template> 327 <template slot-scope="scope">¥{{ formatMoney(scope.row.LaborCost) }}</template>
309 </el-table-column> 328 </el-table-column>
  329 + <el-table-column label="操作" width="100" align="left" fixed="right">
  330 + <template slot-scope="scope">
  331 + <el-button v-if="scope.row.HasServiceLog" type="text" size="small" icon="el-icon-document"
  332 + @click="showConsumeServiceLog(scope.row.Id)">
  333 + 查看日志
  334 + </el-button>
  335 + <span v-else class="text-muted">—</span>
  336 + </template>
  337 + </el-table-column>
310 </el-table> 338 </el-table>
311 - <div class="pagination-bar">  
312 - <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"  
313 - :total="consumePagination.total" :current-page="consumePagination.pageIndex"  
314 - :page-size="consumePagination.pageSize" @size-change="handleConsumeSizeChange"  
315 - @current-change="handleConsumePageChange" />  
316 - </div>  
317 - </div> 339 + <div class="pagination-bar">
  340 + <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"
  341 + :total="consumePagination.total" :current-page="consumePagination.pageIndex"
  342 + :page-size="consumePagination.pageSize" @size-change="handleConsumeSizeChange"
  343 + @current-change="handleConsumePageChange" />
  344 + </div>
  345 + </div>
  346 + </el-tab-pane>
  347 +
  348 + <!-- 服务日志 -->
  349 + <el-tab-pane label="服务日志" name="serviceLog">
  350 + <div class="tab-content tab-content-direct">
  351 + <el-table v-loading="serviceLogLoading" :data="serviceLogList" size="small" border stripe>
  352 + <el-table-column prop="CreateTime" label="记录时间" width="160">
  353 + <template slot-scope="scope">{{ formatDateTime(scope.row.CreateTime) }}</template>
  354 + </el-table-column>
  355 + <el-table-column prop="CreatorName" label="添加人" width="100" />
  356 + <el-table-column prop="Remark" label="备注" min-width="200" show-overflow-tooltip />
  357 + <el-table-column prop="KjbRemark" label="科技部备注" min-width="120" show-overflow-tooltip />
  358 + <el-table-column label="操作" width="100" align="left">
  359 + <template slot-scope="scope">
  360 + <el-button v-if="hasServiceLogImages(scope.row)" type="text" size="small" icon="el-icon-picture-outline" @click="showServiceLogImages(scope.row)">
  361 + 查看图片
  362 + </el-button>
  363 + <span v-else class="text-muted">—</span>
  364 + </template>
  365 + </el-table-column>
  366 + </el-table>
  367 + <div class="pagination-bar">
  368 + <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"
  369 + :total="serviceLogPagination.total" :current-page="serviceLogPagination.pageIndex"
  370 + :page-size="serviceLogPagination.pageSize" @size-change="handleServiceLogSizeChange"
  371 + @current-change="handleServiceLogPageChange" />
  372 + </div>
  373 + </div>
  374 + </el-tab-pane>
  375 +
  376 + <!-- 旧日志(历史开单记录) -->
  377 + <el-tab-pane label="旧日志" name="oldLog">
  378 + <div class="tab-content tab-content-direct">
  379 + <el-table v-loading="oldLogLoading" :data="oldLogList" size="small" border stripe>
  380 + <el-table-column label="记录时间" width="160">
  381 + <template slot-scope="scope">{{ formatDateTime(scope.row.createTime || scope.row.CreateTime) }}</template>
  382 + </el-table-column>
  383 + <el-table-column label="开单号" width="140" show-overflow-tooltip>
  384 + <template slot-scope="scope">{{ scope.row.orderNo || scope.row.OrderNo || '无' }}</template>
  385 + </el-table-column>
  386 + <el-table-column label="会员名称" width="100" show-overflow-tooltip>
  387 + <template slot-scope="scope">{{ scope.row.memberName || scope.row.MemberName || '无' }}</template>
  388 + </el-table-column>
  389 + <el-table-column label="备注" min-width="280" show-overflow-tooltip>
  390 + <template slot-scope="scope">{{ scope.row.remarks || scope.row.Remarks || '无' }}</template>
  391 + </el-table-column>
  392 + <el-table-column label="操作" width="100" align="left">
  393 + <template slot-scope="scope">
  394 + <el-button v-if="hasOldLogImage(scope.row)" type="text" size="small" icon="el-icon-picture-outline" @click="showOldLogImages(scope.row)">
  395 + 查看图片
  396 + </el-button>
  397 + <span v-else class="text-muted">—</span>
  398 + </template>
  399 + </el-table-column>
  400 + </el-table>
  401 + <div class="pagination-bar">
  402 + <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"
  403 + :total="oldLogPagination.total" :current-page="oldLogPagination.pageIndex"
  404 + :page-size="oldLogPagination.pageSize" @size-change="handleOldLogSizeChange"
  405 + @current-change="handleOldLogPageChange" />
318 </div> 406 </div>
319 </div> 407 </div>
320 </el-tab-pane> 408 </el-tab-pane>
321 409
322 <!-- 退卡列表 --> 410 <!-- 退卡列表 -->
323 <el-tab-pane label="退卡列表" name="refund"> 411 <el-tab-pane label="退卡列表" name="refund">
324 - <div class="tab-content">  
325 - <div class="content-card">  
326 - <div class="card-header">  
327 - <i class="el-icon-refresh-left"></i>  
328 - <span class="card-title">退卡列表</span>  
329 - </div>  
330 - <div class="card-body">  
331 - <el-table v-loading="refundLoading" :data="refundList" size="small" border stripe> 412 + <div class="tab-content tab-content-direct">
  413 + <el-table v-loading="refundLoading" :data="refundList" size="small" border stripe>
332 <el-table-column prop="RefundDate" label="退卡日期" width="160"> 414 <el-table-column prop="RefundDate" label="退卡日期" width="160">
333 <template slot-scope="scope">{{ formatDateTime(scope.row.RefundDate) }}</template> 415 <template slot-scope="scope">{{ formatDateTime(scope.row.RefundDate) }}</template>
334 </el-table-column> 416 </el-table-column>
335 - <el-table-column prop="StoreName" label="门店" />  
336 - <el-table-column prop="RefundAmount" label="退卡金额" > 417 + <el-table-column prop="StoreName" label="门店" width="120" />
  418 + <el-table-column prop="RefundAmount" label="退卡金额" width="100">
337 <template slot-scope="scope">¥{{ formatMoney(scope.row.RefundAmount) }}</template> 419 <template slot-scope="scope">¥{{ formatMoney(scope.row.RefundAmount) }}</template>
338 </el-table-column> 420 </el-table-column>
339 - <el-table-column prop="ActualRefundAmount" label="实际退款" > 421 + <el-table-column prop="ActualRefundAmount" label="实际退款" width="100">
340 <template slot-scope="scope">¥{{ formatMoney(scope.row.ActualRefundAmount) }}</template> 422 <template slot-scope="scope">¥{{ formatMoney(scope.row.ActualRefundAmount) }}</template>
341 </el-table-column> 423 </el-table-column>
342 - <el-table-column prop="RefundReason" label="退卡原因" min-width="150" /> 424 + <el-table-column prop="RefundReason" label="退卡原因" min-width="150" show-overflow-tooltip />
343 </el-table> 425 </el-table>
344 - <div class="pagination-bar">  
345 - <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"  
346 - :total="refundPagination.total" :current-page="refundPagination.pageIndex"  
347 - :page-size="refundPagination.pageSize" @size-change="handleRefundSizeChange"  
348 - @current-change="handleRefundPageChange" />  
349 - </div>  
350 - </div> 426 + <div class="pagination-bar">
  427 + <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]"
  428 + :total="refundPagination.total" :current-page="refundPagination.pageIndex"
  429 + :page-size="refundPagination.pageSize" @size-change="handleRefundSizeChange"
  430 + @current-change="handleRefundPageChange" />
351 </div> 431 </div>
352 </div> 432 </div>
353 </el-tab-pane> 433 </el-tab-pane>
354 </el-tabs> 434 </el-tabs>
355 </div> 435 </div>
  436 +
  437 + <!-- 服务日志图片预览弹窗 -->
  438 + <el-dialog title="服务日志图片" :visible.sync="serviceLogImageVisible" width="900px" append-to-body
  439 + custom-class="service-log-image-dialog" @closed="serviceLogImageRow = null">
  440 + <div v-if="serviceLogImageRow" class="image-preview-content">
  441 + <div class="preview-header">
  442 + <span class="preview-meta"><i class="el-icon-time"></i> {{ formatDateTime(serviceLogImageRow.CreateTime) }}</span>
  443 + <span class="preview-meta"><i class="el-icon-user"></i> {{ serviceLogImageRow.CreatorName || '—' }}</span>
  444 + </div>
  445 + <div class="preview-row">
  446 + <div class="image-section">
  447 + <div class="image-section-title"><i class="el-icon-picture-outline"></i> 服务前</div>
  448 + <div class="image-list">
  449 + <template v-if="parseServiceLogImages(serviceLogImageRow.BeforeImage).length > 0">
  450 + <el-image v-for="(img, idx) in parseServiceLogImages(serviceLogImageRow.BeforeImage)" :key="'b-' + idx"
  451 + :src="getServiceLogImageUrl(img)" :preview-src-list="getServiceLogPreviewList(serviceLogImageRow.BeforeImage)"
  452 + fit="contain" class="preview-img" />
  453 + </template>
  454 + <div v-else class="image-placeholder">
  455 + <i class="el-icon-picture-outline"></i>
  456 + <span>暂无图片</span>
  457 + </div>
  458 + </div>
  459 + </div>
  460 + <div class="image-section">
  461 + <div class="image-section-title"><i class="el-icon-picture-outline"></i> 服务后</div>
  462 + <div class="image-list">
  463 + <template v-if="parseServiceLogImages(serviceLogImageRow.AfterImage).length > 0">
  464 + <el-image v-for="(img, idx) in parseServiceLogImages(serviceLogImageRow.AfterImage)" :key="'a-' + idx"
  465 + :src="getServiceLogImageUrl(img)" :preview-src-list="getServiceLogPreviewList(serviceLogImageRow.AfterImage)"
  466 + fit="contain" class="preview-img" />
  467 + </template>
  468 + <div v-else class="image-placeholder">
  469 + <i class="el-icon-picture-outline"></i>
  470 + <span>暂无图片</span>
  471 + </div>
  472 + </div>
  473 + </div>
  474 + </div>
  475 + <div v-if="serviceLogImageRow.Remark" class="preview-remark">
  476 + <span class="remark-label">备注:</span>{{ serviceLogImageRow.Remark }}
  477 + </div>
  478 + </div>
  479 + </el-dialog>
  480 +
  481 + <!-- 旧日志-图片预览弹窗 -->
  482 + <el-dialog title="旧日志图片" :visible.sync="oldLogImageVisible" width="800px" append-to-body
  483 + custom-class="old-log-image-dialog" @closed="oldLogImageUrls = []">
  484 + <div v-if="oldLogImageUrls.length > 0" class="old-log-image-content">
  485 + <el-image v-for="(url, idx) in oldLogImageUrls" :key="idx"
  486 + :src="url" :preview-src-list="oldLogImageUrls" :initial-index="idx"
  487 + fit="contain" class="old-log-preview-img" />
  488 + </div>
  489 + </el-dialog>
  490 +
  491 + <!-- 消耗记录-服务日志弹窗(与服务日志tab的查看图片弹窗同款样式) -->
  492 + <el-dialog title="服务日志" :visible.sync="consumeServiceLogVisible" width="900px" append-to-body
  493 + custom-class="service-log-image-dialog" @closed="consumeServiceLogList = []">
  494 + <div v-loading="consumeServiceLogLoading" class="consume-service-log-content">
  495 + <div v-if="consumeServiceLogList.length === 0 && !consumeServiceLogLoading" class="no-data">
  496 + <el-empty description="暂无服务日志数据"></el-empty>
  497 + </div>
  498 + <div v-else class="log-list">
  499 + <div v-for="(row, idx) in consumeServiceLogList" :key="row.Id || idx" class="image-preview-content log-item">
  500 + <div class="preview-header">
  501 + <span class="preview-meta"><i class="el-icon-time"></i> {{ formatDateTime(row.CreateTime) }}</span>
  502 + <span class="preview-meta"><i class="el-icon-user"></i> {{ row.CreatorName || '—' }}</span>
  503 + </div>
  504 + <div class="preview-row">
  505 + <div class="image-section">
  506 + <div class="image-section-title"><i class="el-icon-picture-outline"></i> 服务前</div>
  507 + <div class="image-list">
  508 + <template v-if="parseServiceLogImages(row.BeforeImage).length > 0">
  509 + <el-image v-for="(img, i) in parseServiceLogImages(row.BeforeImage)" :key="'b-' + i"
  510 + :src="getServiceLogImageUrl(img)" :preview-src-list="getServiceLogPreviewList(row.BeforeImage)"
  511 + fit="contain" class="preview-img" />
  512 + </template>
  513 + <div v-else class="image-placeholder">
  514 + <i class="el-icon-picture-outline"></i>
  515 + <span>暂无图片</span>
  516 + </div>
  517 + </div>
  518 + </div>
  519 + <div class="image-section">
  520 + <div class="image-section-title"><i class="el-icon-picture-outline"></i> 服务后</div>
  521 + <div class="image-list">
  522 + <template v-if="parseServiceLogImages(row.AfterImage).length > 0">
  523 + <el-image v-for="(img, i) in parseServiceLogImages(row.AfterImage)" :key="'a-' + i"
  524 + :src="getServiceLogImageUrl(img)" :preview-src-list="getServiceLogPreviewList(row.AfterImage)"
  525 + fit="contain" class="preview-img" />
  526 + </template>
  527 + <div v-else class="image-placeholder">
  528 + <i class="el-icon-picture-outline"></i>
  529 + <span>暂无图片</span>
  530 + </div>
  531 + </div>
  532 + </div>
  533 + </div>
  534 + <div v-if="row.Remark" class="preview-remark">
  535 + <span class="remark-label">备注:</span>{{ row.Remark }}
  536 + </div>
  537 + <div v-if="row.KjbRemark" class="preview-remark">
  538 + <span class="remark-label">科技部备注:</span>{{ row.KjbRemark }}
  539 + </div>
  540 + </div>
  541 + </div>
  542 + </div>
  543 + </el-dialog>
356 </el-dialog> 544 </el-dialog>
357 </template> 545 </template>
358 546
@@ -400,7 +588,33 @@ export default { @@ -400,7 +588,33 @@ export default {
400 pageIndex: 1, 588 pageIndex: 1,
401 pageSize: 10, 589 pageSize: 10,
402 total: 0 590 total: 0
403 - } 591 + },
  592 + // 预约记录
  593 + appointmentList: [],
  594 + appointmentLoading: false,
  595 + appointmentPagination: { pageIndex: 1, pageSize: 10, total: 0 },
  596 + // 邀约记录
  597 + inviteList: [],
  598 + inviteLoading: false,
  599 + invitePagination: { pageIndex: 1, pageSize: 10, total: 0 },
  600 + // 服务日志
  601 + serviceLogList: [],
  602 + serviceLogLoading: false,
  603 + serviceLogPagination: { pageIndex: 1, pageSize: 10, total: 0 },
  604 + // 旧日志
  605 + oldLogList: [],
  606 + oldLogLoading: false,
  607 + oldLogPagination: { pageIndex: 1, pageSize: 10, total: 0 },
  608 + // 服务日志图片预览
  609 + serviceLogImageVisible: false,
  610 + serviceLogImageRow: null,
  611 + // 消耗记录-服务日志弹窗(与服务日志tab同款)
  612 + consumeServiceLogVisible: false,
  613 + consumeServiceLogList: [],
  614 + consumeServiceLogLoading: false,
  615 + // 旧日志-图片预览
  616 + oldLogImageVisible: false,
  617 + oldLogImageUrls: []
404 } 618 }
405 }, 619 },
406 watch: { 620 watch: {
@@ -427,6 +641,14 @@ export default { @@ -427,6 +641,14 @@ export default {
427 this.fetchConsumeList() 641 this.fetchConsumeList()
428 } else if (newVal === 'refund' && this.refundList.length === 0) { 642 } else if (newVal === 'refund' && this.refundList.length === 0) {
429 this.fetchRefundList() 643 this.fetchRefundList()
  644 + } else if (newVal === 'appointment' && this.appointmentList.length === 0) {
  645 + this.fetchAppointmentList()
  646 + } else if (newVal === 'invite' && this.inviteList.length === 0) {
  647 + this.fetchInviteList()
  648 + } else if (newVal === 'serviceLog' && this.serviceLogList.length === 0) {
  649 + this.fetchServiceLogList()
  650 + } else if (newVal === 'oldLog') {
  651 + this.fetchOldLogList()
430 } 652 }
431 } 653 }
432 }, 654 },
@@ -444,8 +666,38 @@ export default { @@ -444,8 +666,38 @@ export default {
444 this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 } 666 this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 }
445 this.refundList = [] 667 this.refundList = []
446 this.refundPagination = { pageIndex: 1, pageSize: 10, total: 0 } 668 this.refundPagination = { pageIndex: 1, pageSize: 10, total: 0 }
  669 + this.appointmentList = []
  670 + this.appointmentPagination = { pageIndex: 1, pageSize: 10, total: 0 }
  671 + this.inviteList = []
  672 + this.invitePagination = { pageIndex: 1, pageSize: 10, total: 0 }
  673 + this.serviceLogList = []
  674 + this.serviceLogPagination = { pageIndex: 1, pageSize: 10, total: 0 }
  675 + this.oldLogList = []
  676 + this.oldLogPagination = { pageIndex: 1, pageSize: 10, total: 0 }
447 this.activeTab = 'overview' 677 this.activeTab = 'overview'
448 }, 678 },
  679 + /** 年龄显示:优先用后端返回的 Age,否则根据阳历生日计算 */
  680 + getDisplayAge() {
  681 + const info = this.baseInfo
  682 + if (!info) return '无'
  683 + let age = info.Age
  684 + if (age == null && info.Yanglsr) {
  685 + const birth = new Date(info.Yanglsr)
  686 + if (!isNaN(birth.getTime())) {
  687 + const now = new Date()
  688 + age = now.getFullYear() - birth.getFullYear()
  689 + if (now.getMonth() < birth.getMonth() || (now.getMonth() === birth.getMonth() && now.getDate() < birth.getDate())) {
  690 + age--
  691 + }
  692 + }
  693 + }
  694 + return age != null ? age + '岁' : '无'
  695 + },
  696 + /** 修改资料:关闭画像并触发父组件打开编辑 */
  697 + handleEdit() {
  698 + this.$emit('edit', this.memberId)
  699 + this.$emit('update:visible', false)
  700 + },
449 async fetchData() { 701 async fetchData() {
450 if (!this.memberId) { 702 if (!this.memberId) {
451 this.$message.warning('会员ID不能为空') 703 this.$message.warning('会员ID不能为空')
@@ -459,13 +711,21 @@ export default { @@ -459,13 +711,21 @@ export default {
459 this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 } 711 this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 }
460 this.refundList = [] 712 this.refundList = []
461 this.refundPagination = { pageIndex: 1, pageSize: 10, total: 0 } 713 this.refundPagination = { pageIndex: 1, pageSize: 10, total: 0 }
  714 + this.appointmentList = []
  715 + this.appointmentPagination = { pageIndex: 1, pageSize: 10, total: 0 }
  716 + this.inviteList = []
  717 + this.invitePagination = { pageIndex: 1, pageSize: 10, total: 0 }
  718 + this.serviceLogList = []
  719 + this.serviceLogPagination = { pageIndex: 1, pageSize: 10, total: 0 }
  720 + this.oldLogList = []
  721 + this.oldLogPagination = { pageIndex: 1, pageSize: 10, total: 0 }
462 722
463 this.loading = true 723 this.loading = true
464 try { 724 try {
465 const res = await request({ 725 const res = await request({
466 url: '/api/Extend/MemberPortrait/overview', 726 url: '/api/Extend/MemberPortrait/overview',
467 method: 'GET', 727 method: 'GET',
468 - params: { memberId: this.memberId } 728 + data: { memberId: this.memberId }
469 }) 729 })
470 730
471 if (res.code === 200 && res.data) { 731 if (res.code === 200 && res.data) {
@@ -496,7 +756,7 @@ export default { @@ -496,7 +756,7 @@ export default {
496 const res = await request({ 756 const res = await request({
497 url: '/api/Extend/MemberPortrait/billing-list', 757 url: '/api/Extend/MemberPortrait/billing-list',
498 method: 'GET', 758 method: 'GET',
499 - params: { 759 + data: {
500 memberId: this.memberId, 760 memberId: this.memberId,
501 pageIndex: this.billingPagination.pageIndex, 761 pageIndex: this.billingPagination.pageIndex,
502 pageSize: this.billingPagination.pageSize 762 pageSize: this.billingPagination.pageSize
@@ -524,7 +784,7 @@ export default { @@ -524,7 +784,7 @@ export default {
524 const res = await request({ 784 const res = await request({
525 url: '/api/Extend/MemberPortrait/consume-list', 785 url: '/api/Extend/MemberPortrait/consume-list',
526 method: 'GET', 786 method: 'GET',
527 - params: { 787 + data: {
528 memberId: this.memberId, 788 memberId: this.memberId,
529 pageIndex: this.consumePagination.pageIndex, 789 pageIndex: this.consumePagination.pageIndex,
530 pageSize: this.consumePagination.pageSize 790 pageSize: this.consumePagination.pageSize
@@ -552,7 +812,7 @@ export default { @@ -552,7 +812,7 @@ export default {
552 const res = await request({ 812 const res = await request({
553 url: '/api/Extend/MemberPortrait/refund-list', 813 url: '/api/Extend/MemberPortrait/refund-list',
554 method: 'GET', 814 method: 'GET',
555 - params: { 815 + data: {
556 memberId: this.memberId, 816 memberId: this.memberId,
557 pageIndex: this.refundPagination.pageIndex, 817 pageIndex: this.refundPagination.pageIndex,
558 pageSize: this.refundPagination.pageSize 818 pageSize: this.refundPagination.pageSize
@@ -599,6 +859,229 @@ export default { @@ -599,6 +859,229 @@ export default {
599 this.refundPagination.pageIndex = 1 859 this.refundPagination.pageIndex = 1
600 this.fetchRefundList() 860 this.fetchRefundList()
601 }, 861 },
  862 + async fetchAppointmentList() {
  863 + if (!this.memberId) return
  864 + this.appointmentLoading = true
  865 + try {
  866 + const res = await request({
  867 + url: '/api/Extend/MemberPortrait/appointment-list',
  868 + method: 'GET',
  869 + data: {
  870 + memberId: this.memberId,
  871 + pageIndex: this.appointmentPagination.pageIndex,
  872 + pageSize: this.appointmentPagination.pageSize
  873 + }
  874 + })
  875 + if (res.code === 200 && res.data) {
  876 + this.appointmentList = res.data.List || []
  877 + this.appointmentPagination.total = res.data.Total || 0
  878 + } else {
  879 + this.$message.error(res.msg || '获取预约记录失败')
  880 + }
  881 + } catch (error) {
  882 + console.error('获取预约记录失败:', error)
  883 + this.$message.error('获取预约记录失败: ' + (error.message || '未知错误'))
  884 + } finally {
  885 + this.appointmentLoading = false
  886 + }
  887 + },
  888 + handleAppointmentPageChange(page) {
  889 + this.appointmentPagination.pageIndex = page
  890 + this.fetchAppointmentList()
  891 + },
  892 + handleAppointmentSizeChange(size) {
  893 + this.appointmentPagination.pageSize = size
  894 + this.appointmentPagination.pageIndex = 1
  895 + this.fetchAppointmentList()
  896 + },
  897 + async fetchInviteList() {
  898 + if (!this.memberId) return
  899 + this.inviteLoading = true
  900 + try {
  901 + const res = await request({
  902 + url: '/api/Extend/MemberPortrait/invite-list',
  903 + method: 'GET',
  904 + data: {
  905 + memberId: this.memberId,
  906 + pageIndex: this.invitePagination.pageIndex,
  907 + pageSize: this.invitePagination.pageSize
  908 + }
  909 + })
  910 + if (res.code === 200 && res.data) {
  911 + this.inviteList = res.data.List || []
  912 + this.invitePagination.total = res.data.Total || 0
  913 + } else {
  914 + this.$message.error(res.msg || '获取邀约记录失败')
  915 + }
  916 + } catch (error) {
  917 + console.error('获取邀约记录失败:', error)
  918 + this.$message.error('获取邀约记录失败: ' + (error.message || '未知错误'))
  919 + } finally {
  920 + this.inviteLoading = false
  921 + }
  922 + },
  923 + handleInvitePageChange(page) {
  924 + this.invitePagination.pageIndex = page
  925 + this.fetchInviteList()
  926 + },
  927 + handleInviteSizeChange(size) {
  928 + this.invitePagination.pageSize = size
  929 + this.invitePagination.pageIndex = 1
  930 + this.fetchInviteList()
  931 + },
  932 + async fetchServiceLogList() {
  933 + if (!this.memberId) return
  934 + this.serviceLogLoading = true
  935 + try {
  936 + const res = await request({
  937 + url: '/api/Extend/MemberPortrait/service-log-list',
  938 + method: 'GET',
  939 + data: {
  940 + memberId: this.memberId,
  941 + pageIndex: this.serviceLogPagination.pageIndex,
  942 + pageSize: this.serviceLogPagination.pageSize
  943 + }
  944 + })
  945 + if (res.code === 200 && res.data) {
  946 + this.serviceLogList = res.data.List || []
  947 + this.serviceLogPagination.total = res.data.Total || 0
  948 + } else {
  949 + this.$message.error(res.msg || '获取服务日志失败')
  950 + }
  951 + } catch (error) {
  952 + console.error('获取服务日志失败:', error)
  953 + this.$message.error('获取服务日志失败: ' + (error.message || '未知错误'))
  954 + } finally {
  955 + this.serviceLogLoading = false
  956 + }
  957 + },
  958 + handleServiceLogPageChange(page) {
  959 + this.serviceLogPagination.pageIndex = page
  960 + this.fetchServiceLogList()
  961 + },
  962 + handleServiceLogSizeChange(size) {
  963 + this.serviceLogPagination.pageSize = size
  964 + this.serviceLogPagination.pageIndex = 1
  965 + this.fetchServiceLogList()
  966 + },
  967 + async fetchOldLogList() {
  968 + const memberCode = this.baseInfo.MemberCode || ''
  969 + const mobile = this.baseInfo.Mobile || ''
  970 + if (!memberCode && !mobile) {
  971 + this.$message.warning('会员编号和手机号均无,无法查询旧日志')
  972 + return
  973 + }
  974 +
  975 + this.oldLogLoading = true
  976 + try {
  977 + const res = await request({
  978 + url: '/api/Extend/MemberPortrait/old-log-list',
  979 + method: 'GET',
  980 + data: {
  981 + memberCode,
  982 + mobile,
  983 + pageIndex: this.oldLogPagination.pageIndex,
  984 + pageSize: this.oldLogPagination.pageSize
  985 + }
  986 + })
  987 +
  988 + if (res.code === 200 && res.data) {
  989 + const list = res.data.List || res.data.list || []
  990 + const total = (res.data.Total != null ? res.data.Total : res.data.total) || 0
  991 + this.oldLogList = list
  992 + this.oldLogPagination.total = total
  993 + // 调试:输出接口返回数据
  994 + console.log('[旧日志] 接口返回:', { data: res.data, list, total, firstItem: list[0] })
  995 + } else {
  996 + this.$message.error(res.msg || '获取旧日志失败')
  997 + }
  998 + } catch (error) {
  999 + console.error('获取旧日志失败:', error)
  1000 + this.$message.error('获取旧日志失败: ' + (error.message || '未知错误'))
  1001 + } finally {
  1002 + this.oldLogLoading = false
  1003 + }
  1004 + },
  1005 + handleOldLogPageChange(page) {
  1006 + this.oldLogPagination.pageIndex = page
  1007 + this.fetchOldLogList()
  1008 + },
  1009 + handleOldLogSizeChange(size) {
  1010 + this.oldLogPagination.pageSize = size
  1011 + this.oldLogPagination.pageIndex = 1
  1012 + this.fetchOldLogList()
  1013 + },
  1014 + showServiceLogImages(row) {
  1015 + this.serviceLogImageRow = row
  1016 + this.serviceLogImageVisible = true
  1017 + },
  1018 + async showConsumeServiceLog(consumeId) {
  1019 + this.consumeServiceLogVisible = true
  1020 + this.consumeServiceLogLoading = true
  1021 + this.consumeServiceLogList = []
  1022 + try {
  1023 + const res = await request({
  1024 + url: `/api/Extend/lqxhfeedback/GetByConsumeId/${consumeId}`,
  1025 + method: 'GET'
  1026 + })
  1027 + if (res.code === 200 && res.data) {
  1028 + const list = Array.isArray(res.data) ? res.data : []
  1029 + this.consumeServiceLogList = list.map(item => ({
  1030 + Id: item.id,
  1031 + CreateTime: item.createTime,
  1032 + CreatorName: item.createUserName,
  1033 + Remark: item.remark,
  1034 + KjbRemark: item.kjbRemark,
  1035 + BeforeImage: item.beforeImage,
  1036 + AfterImage: item.afterImage
  1037 + }))
  1038 + }
  1039 + } catch (error) {
  1040 + console.error('获取服务日志失败:', error)
  1041 + this.$message.error('获取服务日志失败')
  1042 + } finally {
  1043 + this.consumeServiceLogLoading = false
  1044 + }
  1045 + },
  1046 + parseServiceLogImages(imageStr) {
  1047 + if (!imageStr) return []
  1048 + try {
  1049 + const arr = typeof imageStr === 'string' ? JSON.parse(imageStr) : imageStr
  1050 + return Array.isArray(arr) ? arr : []
  1051 + } catch {
  1052 + return []
  1053 + }
  1054 + },
  1055 + getServiceLogImageUrl(img) {
  1056 + if (!img || !img.url) return ''
  1057 + const url = img.url
  1058 + if (url.startsWith('http://') || url.startsWith('https://')) return url
  1059 + const base = process.env.VUE_APP_IMG_API || process.env.VUE_APP_BASE_API || ''
  1060 + return base ? (base.replace(/\/$/, '') + (url.startsWith('/') ? url : '/' + url)) : url
  1061 + },
  1062 + getServiceLogPreviewList(imageStr) {
  1063 + return this.parseServiceLogImages(imageStr).map(img => this.getServiceLogImageUrl(img)).filter(Boolean)
  1064 + },
  1065 + hasServiceLogImages(row) {
  1066 + const before = this.parseServiceLogImages(row && row.BeforeImage).length
  1067 + const after = this.parseServiceLogImages(row && row.AfterImage).length
  1068 + return before > 0 || after > 0
  1069 + },
  1070 + hasOldLogImage(row) {
  1071 + const name = row && (row.imageName || row.ImageName)
  1072 + if (!name || typeof name !== 'string') return false
  1073 + return name.split(';').filter(s => s && s.trim()).length > 0
  1074 + },
  1075 + parseOldLogImageUrls(imageStr) {
  1076 + if (!imageStr || typeof imageStr !== 'string') return []
  1077 + return imageStr.split(';').map(s => s.trim()).filter(Boolean)
  1078 + },
  1079 + showOldLogImages(row) {
  1080 + const urls = this.parseOldLogImageUrls(row && (row.imageName || row.ImageName))
  1081 + if (urls.length === 0) return
  1082 + this.oldLogImageUrls = urls
  1083 + this.oldLogImageVisible = true
  1084 + },
602 renderTrendChart() { 1085 renderTrendChart() {
603 if (!this.$refs.trendChart) return 1086 if (!this.$refs.trendChart) return
604 1087
@@ -677,10 +1160,17 @@ export default { @@ -677,10 +1160,17 @@ export default {
677 if (amount === null || amount === undefined || amount === '') return '0.00' 1160 if (amount === null || amount === undefined || amount === '') return '0.00'
678 return Number(amount).toFixed(2) 1161 return Number(amount).toFixed(2)
679 }, 1162 },
  1163 + sortNumber(prop) {
  1164 + return (a, b) => {
  1165 + const va = Number(a[prop]) || 0
  1166 + const vb = Number(b[prop]) || 0
  1167 + return va - vb
  1168 + }
  1169 + },
680 formatDateTime(timestamp) { 1170 formatDateTime(timestamp) {
681 if (!timestamp) return '无' 1171 if (!timestamp) return '无'
682 - if (typeof timestamp === 'number') {  
683 - const date = new Date(timestamp) 1172 + const date = typeof timestamp === 'number' ? new Date(timestamp) : new Date(timestamp)
  1173 + if (!isNaN(date.getTime())) {
684 return date.toLocaleString('zh-CN', { 1174 return date.toLocaleString('zh-CN', {
685 year: 'numeric', 1175 year: 'numeric',
686 month: '2-digit', 1176 month: '2-digit',
@@ -693,8 +1183,8 @@ export default { @@ -693,8 +1183,8 @@ export default {
693 }, 1183 },
694 formatDate(timestamp) { 1184 formatDate(timestamp) {
695 if (!timestamp) return '' 1185 if (!timestamp) return ''
696 - if (typeof timestamp === 'number') {  
697 - const date = new Date(timestamp) 1186 + const date = typeof timestamp === 'number' ? new Date(timestamp) : new Date(timestamp)
  1187 + if (!isNaN(date.getTime())) {
698 return date.toLocaleDateString('zh-CN', { 1188 return date.toLocaleDateString('zh-CN', {
699 year: 'numeric', 1189 year: 'numeric',
700 month: '2-digit', 1190 month: '2-digit',
@@ -703,15 +1193,27 @@ export default { @@ -703,15 +1193,27 @@ export default {
703 } 1193 }
704 return timestamp 1194 return timestamp
705 }, 1195 },
  1196 + formatBirthdayDisplay() {
  1197 + const info = this.baseInfo
  1198 + if (!info) return '无'
  1199 + if (info.BirthdayType === 1 && info.Yinlsr) return `${info.BirthdayTypeName || '农历'} ${info.Yinlsr}`
  1200 + if (info.Yanglsr) {
  1201 + const d = new Date(info.Yanglsr)
  1202 + if (!isNaN(d.getTime())) return `${info.BirthdayTypeName || '阳历'} ${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
  1203 + }
  1204 + if (info.Yinlsr) return `农历 ${info.Yinlsr}`
  1205 + return '无'
  1206 + },
706 getConsumeLevelText(level) { 1207 getConsumeLevelText(level) {
707 const levelMap = { 1208 const levelMap = {
708 - 0: '普通',  
709 - 1: '银卡',  
710 - 2: '金卡',  
711 - 3: '钻石',  
712 - 4: 'VIP' 1209 + 0: 'D',
  1210 + 1: 'C',
  1211 + 2: 'B',
  1212 + 3: 'A',
  1213 + 4: 'A+',
  1214 + 5: 'A++'
713 } 1215 }
714 - return levelMap[level] || '普通' 1216 + return levelMap[level] || 'D'
715 }, 1217 },
716 getConsumeLevelTagType(level) { 1218 getConsumeLevelTagType(level) {
717 const typeMap = { 1219 const typeMap = {
@@ -719,7 +1221,8 @@ export default { @@ -719,7 +1221,8 @@ export default {
719 1: '', 1221 1: '',
720 2: 'warning', 1222 2: 'warning',
721 3: 'success', 1223 3: 'success',
722 - 4: 'danger' 1224 + 4: 'success',
  1225 + 5: 'danger'
723 } 1226 }
724 return typeMap[level] || 'info' 1227 return typeMap[level] || 'info'
725 }, 1228 },
@@ -805,227 +1308,259 @@ export default { @@ -805,227 +1308,259 @@ export default {
805 padding: 20px; 1308 padding: 20px;
806 background: #f5f7fa; 1309 background: #f5f7fa;
807 color: #303133; 1310 color: #303133;
808 - overflow-y: auto;  
809 - max-height: calc(90vh - 120px); 1311 + overflow: hidden;
  1312 + height: calc(90vh - 120px);
  1313 + display: flex;
  1314 + flex-direction: column;
810 } 1315 }
811 } 1316 }
812 1317
813 .portrait-wrapper { 1318 .portrait-wrapper {
814 - // 顶部会员信息卡片  
815 - .portrait-header {  
816 - background: #ffffff;  
817 - border-radius: 8px;  
818 - padding: 20px;  
819 - margin-bottom: 16px; 1319 + display: flex;
  1320 + flex-direction: column;
  1321 + flex: 1;
  1322 + min-height: 0;
  1323 + overflow: hidden;
  1324 +
  1325 + // 中部左右分栏
  1326 + .portrait-main-panel {
  1327 + flex-shrink: 0;
820 display: flex; 1328 display: flex;
821 - align-items: flex-start;  
822 - gap: 20px;  
823 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); 1329 + gap: 24px;
  1330 + margin-bottom: 16px;
  1331 + background: linear-gradient(180deg, #fff 0%, #fafbfc 100%);
  1332 + border-radius: 12px;
  1333 + padding: 24px;
824 border: 1px solid #e4e7ed; 1334 border: 1px solid #e4e7ed;
825 - transition: all 0.2s ease;  
826 -  
827 - &:hover {  
828 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);  
829 - } 1335 + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(64, 158, 255, 0.04);
  1336 + min-height: 180px;
830 1337
831 - .header-avatar { 1338 + .panel-left {
832 flex-shrink: 0; 1339 flex-shrink: 0;
  1340 + width: 220px;
  1341 + min-width: 220px;
  1342 + display: flex;
  1343 + flex-direction: column;
  1344 + align-items: center;
  1345 + padding-right: 24px;
  1346 + border-right: 1px solid #ebeef5;
833 1347
834 .avatar-circle { 1348 .avatar-circle {
835 - width: 72px;  
836 - height: 72px;  
837 - border-radius: 8px; 1349 + width: 64px;
  1350 + height: 64px;
  1351 + border-radius: 12px;
838 background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%); 1352 background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
839 display: flex; 1353 display: flex;
840 align-items: center; 1354 align-items: center;
841 justify-content: center; 1355 justify-content: center;
842 color: #fff; 1356 color: #fff;
843 - font-size: 32px;  
844 - box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);  
845 - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);  
846 -  
847 - &:hover {  
848 - transform: scale(1.05) rotate(5deg);  
849 - box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);  
850 - background: linear-gradient(135deg, #66b1ff 0%, #409EFF 100%);  
851 - } 1357 + font-size: 28px;
  1358 + margin-bottom: 12px;
  1359 + box-shadow: 0 4px 12px rgba(64, 158, 255, 0.35);
852 } 1360 }
853 - }  
854 -  
855 - .header-main {  
856 - flex: 1;  
857 - min-width: 0;  
858 1361
859 - .member-name-row {  
860 - display: flex;  
861 - align-items: center;  
862 - gap: 12px;  
863 - margin-bottom: 16px;  
864 -  
865 - .member-name {  
866 - font-size: 24px;  
867 - font-weight: 600;  
868 - color: #303133;  
869 - margin: 0;  
870 - line-height: 1.2;  
871 - }  
872 -  
873 - .level-tag {  
874 - font-weight: 600;  
875 - } 1362 + .member-name {
  1363 + font-size: 18px;
  1364 + font-weight: 600;
  1365 + color: #303133;
  1366 + margin: 0 0 8px 0;
  1367 + line-height: 1.3;
  1368 + text-align: center;
  1369 + width: 100%;
  1370 + overflow: hidden;
  1371 + text-overflow: ellipsis;
  1372 + white-space: nowrap;
876 } 1373 }
877 1374
878 - .member-meta {  
879 - display: flex;  
880 - flex-wrap: wrap;  
881 - gap: 20px; 1375 + .level-tag {
  1376 + margin-bottom: 12px;
  1377 + }
882 1378
883 - .meta-item {  
884 - font-size: 14px;  
885 - color: #606266;  
886 - display: flex;  
887 - align-items: center;  
888 - gap: 8px; 1379 + .left-meta {
  1380 + width: 100%;
  1381 + font-size: 13px;
  1382 + color: #606266;
  1383 + margin-bottom: 12px;
889 1384
890 - i {  
891 - color: #909399;  
892 - font-size: 16px;  
893 - } 1385 + .meta-row {
  1386 + padding: 5px 10px;
  1387 + white-space: nowrap;
  1388 + overflow: hidden;
  1389 + text-overflow: ellipsis;
  1390 + border-radius: 6px;
  1391 + background: #f8f9fa;
894 1392
895 - span {  
896 - line-height: 1.5; 1393 + &.phone {
  1394 + color: #67C23A;
  1395 + font-weight: 500;
897 } 1396 }
898 } 1397 }
899 } 1398 }
  1399 +
  1400 + .btn-edit {
  1401 + width: 100%;
  1402 + }
900 } 1403 }
901 1404
902 - .header-right {  
903 - display: flex;  
904 - flex-direction: column;  
905 - align-items: flex-end;  
906 - gap: 12px;  
907 - flex-shrink: 0; 1405 + .panel-right {
  1406 + flex: 1;
  1407 + min-width: 0;
  1408 + overflow: hidden;
908 1409
909 - .header-stats {  
910 - display: flex;  
911 - gap: 12px;  
912 - flex-shrink: 0; 1410 + .attr-grid {
  1411 + display: grid;
  1412 + grid-template-columns: repeat(4, minmax(0, 1fr));
  1413 + gap: 10px 20px;
913 1414
914 - .stat-card {  
915 - min-width: 110px;  
916 - padding: 12px 16px;  
917 - border-radius: 6px;  
918 - border: 1px solid #e4e7ed; 1415 + .attr-item {
919 display: flex; 1416 display: flex;
920 - flex-direction: column;  
921 - align-items: flex-start;  
922 - gap: 6px;  
923 - transition: all 0.2s ease;  
924 - background: #ffffff; 1417 + align-items: center;
  1418 + font-size: 13px;
  1419 + min-height: 28px;
  1420 + min-width: 0;
  1421 + padding: 4px 10px;
  1422 + border-radius: 6px;
  1423 + background: #f8f9fa;
  1424 + transition: background 0.2s ease;
925 1425
926 &:hover { 1426 &:hover {
927 - border-color: #c0c4cc;  
928 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 1427 + background: #f0f2f5;
929 } 1428 }
930 1429
931 - .stat-label-tag {  
932 - font-weight: 600;  
933 - font-size: 12px;  
934 - padding: 2px 10px;  
935 - margin: 0;  
936 - flex-shrink: 0; 1430 + &.attr-item-full {
  1431 + grid-column: 1 / -1;
  1432 + white-space: normal;
  1433 + word-break: break-all;
  1434 + padding: 8px 12px;
937 } 1435 }
938 1436
939 - .stat-value {  
940 - font-size: 16px;  
941 - font-weight: 600;  
942 - color: #303133; 1437 + &:not(.attr-item-full) {
943 white-space: nowrap; 1438 white-space: nowrap;
944 - line-height: 1.2; 1439 + overflow: hidden;
  1440 + text-overflow: ellipsis;
945 } 1441 }
946 1442
947 - &.stat-primary {  
948 - border-left: 3px solid #409EFF;  
949 - background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(102, 177, 255, 0.05) 100%); 1443 + .attr-label {
  1444 + color: #909399;
  1445 + flex-shrink: 0;
  1446 + margin-right: 8px;
  1447 + font-size: 12px;
950 } 1448 }
951 1449
952 - &.stat-success {  
953 - border-left: 3px solid #67C23A;  
954 - background: linear-gradient(135deg, rgba(103, 194, 58, 0.08) 0%, rgba(133, 206, 97, 0.05) 100%);  
955 - } 1450 + .attr-value {
  1451 + color: #303133;
  1452 + overflow: hidden;
  1453 + text-overflow: ellipsis;
956 1454
957 - &.stat-info {  
958 - border-left: 3px solid #909399;  
959 - background: linear-gradient(135deg, rgba(144, 147, 153, 0.08) 0%, rgba(169, 172, 178, 0.05) 100%);  
960 - } 1455 + &.highlight {
  1456 + font-weight: 600;
  1457 + color: #303133;
  1458 + }
961 1459
962 - &.stat-warning {  
963 - border-left: 3px solid #E6A23C;  
964 - background: linear-gradient(135deg, rgba(230, 162, 60, 0.08) 0%, rgba(240, 180, 90, 0.05) 100%); 1460 + &.primary {
  1461 + color: #409EFF;
  1462 + font-weight: 600;
  1463 + }
965 } 1464 }
966 1465
967 - &.stat-default {  
968 - border-left: 3px solid #409EFF;  
969 - background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(102, 177, 255, 0.05) 100%); 1466 + .el-tag {
  1467 + flex-shrink: 0;
970 } 1468 }
971 } 1469 }
972 } 1470 }
973 1471
974 - .member-types-section { 1472 + .member-types-row {
  1473 + margin-top: 14px;
  1474 + padding-top: 14px;
  1475 + border-top: 1px dashed #e4e7ed;
975 display: flex; 1476 display: flex;
976 - align-items: center;  
977 - gap: 12px;  
978 flex-wrap: wrap; 1477 flex-wrap: wrap;
  1478 + gap: 8px 16px;
979 1479
980 - .member-types-label {  
981 - font-size: 13px;  
982 - color: #909399;  
983 - display: flex; 1480 + .member-type-wrap {
  1481 + display: inline-flex;
984 align-items: center; 1482 align-items: center;
985 gap: 6px; 1483 gap: 6px;
986 - flex-shrink: 0; 1484 + padding: 4px 10px;
  1485 + background: #f8f9fa;
  1486 + border-radius: 6px;
  1487 + transition: background 0.2s ease;
987 1488
988 - i {  
989 - color: #409EFF;  
990 - font-size: 14px; 1489 + &:hover {
  1490 + background: #f0f2f5;
  1491 + }
  1492 +
  1493 + .type-date {
  1494 + font-size: 12px;
  1495 + color: #909399;
991 } 1496 }
992 } 1497 }
  1498 + }
  1499 + }
  1500 + }
993 1501
994 - .member-types-list { 1502 + // 个人档案样式(与 detail-dialog 一致)
  1503 + .profile-layout {
  1504 + .detail-section {
  1505 + margin-bottom: 20px;
  1506 + background: #fff;
  1507 + border-radius: 8px;
  1508 + overflow: hidden;
  1509 + border: 1px solid #ebeef5;
  1510 + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
  1511 +
  1512 + .section-title {
  1513 + background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
  1514 + color: #fff;
  1515 + padding: 12px 20px;
  1516 + font-size: 15px;
  1517 + font-weight: 600;
  1518 + display: flex;
  1519 + align-items: center;
  1520 + gap: 8px;
  1521 +
  1522 + i {
  1523 + font-size: 18px;
  1524 + }
  1525 + }
  1526 +
  1527 + .section-content {
  1528 + padding: 20px;
  1529 +
  1530 + .info-row {
995 display: flex; 1531 display: flex;
996 flex-wrap: wrap; 1532 flex-wrap: wrap;
997 - gap: 12px; 1533 + gap: 20px 40px;
998 1534
999 - .member-type-badge { 1535 + .info-item {
  1536 + flex: 0 0 calc(33.333% - 27px);
  1537 + min-width: 220px;
1000 display: flex; 1538 display: flex;
1001 align-items: center; 1539 align-items: center;
1002 - gap: 8px;  
1003 - padding: 6px 12px;  
1004 - background: #f5f7fa;  
1005 - border-radius: 6px;  
1006 - border: 1px solid #e4e7ed;  
1007 - transition: all 0.2s ease;  
1008 -  
1009 - &:hover {  
1010 - background: #f0f2f5;  
1011 - border-color: #c0c4cc; 1540 + padding: 6px 0;
  1541 +
  1542 + &.info-item-full {
  1543 + flex: 1 1 100%;
  1544 + min-width: 100%;
1012 } 1545 }
1013 1546
1014 - .member-type-tag {  
1015 - font-weight: 600;  
1016 - font-size: 12px;  
1017 - padding: 2px 10px; 1547 + .label {
  1548 + color: #606266;
  1549 + font-weight: 500;
  1550 + margin-right: 8px;
  1551 + flex-shrink: 0;
1018 } 1552 }
1019 1553
1020 - .member-type-date {  
1021 - font-size: 12px;  
1022 - color: #909399;  
1023 - display: flex;  
1024 - align-items: center;  
1025 - gap: 4px; 1554 + .value {
  1555 + color: #303133;
  1556 +
  1557 + &.highlight {
  1558 + font-weight: 600;
  1559 + color: #303133;
  1560 + }
1026 1561
1027 - i {  
1028 - font-size: 12px; 1562 + &.primary {
  1563 + color: #409EFF;
1029 } 1564 }
1030 } 1565 }
1031 } 1566 }
@@ -1037,6 +1572,28 @@ export default { @@ -1037,6 +1572,28 @@ export default {
1037 1572
1038 // 选项卡样式 1573 // 选项卡样式
1039 .portrait-tabs { 1574 .portrait-tabs {
  1575 + flex: 1;
  1576 + min-height: 0;
  1577 + display: flex;
  1578 + flex-direction: column;
  1579 + overflow: hidden;
  1580 +
  1581 + ::v-deep .el-tabs__content {
  1582 + flex: 1;
  1583 + min-height: 0;
  1584 + overflow: hidden;
  1585 + display: flex;
  1586 + flex-direction: column;
  1587 + }
  1588 +
  1589 + ::v-deep .el-tab-pane {
  1590 + flex: 1;
  1591 + min-height: 0;
  1592 + overflow: hidden;
  1593 + display: flex;
  1594 + flex-direction: column;
  1595 + }
  1596 +
1040 ::v-deep .el-tabs__header { 1597 ::v-deep .el-tabs__header {
1041 margin-bottom: 16px; 1598 margin-bottom: 16px;
1042 background: #ffffff; 1599 background: #ffffff;
@@ -1075,8 +1632,23 @@ export default { @@ -1075,8 +1632,23 @@ export default {
1075 } 1632 }
1076 1633
1077 .tab-content { 1634 .tab-content {
1078 - height: 40vh;  
1079 - overflow-y: scroll; 1635 + flex: 1;
  1636 + min-height: 0;
  1637 + overflow-y: auto;
  1638 + overflow-x: hidden;
  1639 +
  1640 + &.tab-content-direct {
  1641 + padding: 18px;
  1642 + background: #fff;
  1643 + border-radius: 8px;
  1644 + border: 1px solid #e4e7ed;
  1645 +
  1646 + .pagination-bar {
  1647 + padding: 12px 0 0;
  1648 + margin-top: 12px;
  1649 + border-top: 1px solid #e4e7ed;
  1650 + }
  1651 + }
1080 .content-card { 1652 .content-card {
1081 background: #ffffff; 1653 background: #ffffff;
1082 border-radius: 8px; 1654 border-radius: 8px;
@@ -1339,3 +1911,166 @@ export default { @@ -1339,3 +1911,166 @@ export default {
1339 } 1911 }
1340 } 1912 }
1341 </style> 1913 </style>
  1914 +
  1915 +<style lang="scss">
  1916 +/* 旧日志图片预览弹窗 */
  1917 +.old-log-image-dialog {
  1918 + .el-dialog__body {
  1919 + padding: 16px 24px 24px;
  1920 + }
  1921 +
  1922 + .old-log-image-content {
  1923 + display: grid;
  1924 + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  1925 + gap: 12px;
  1926 + }
  1927 +
  1928 + .old-log-preview-img {
  1929 + width: 100%;
  1930 + aspect-ratio: 1;
  1931 + max-height: 200px;
  1932 + border-radius: 8px;
  1933 + border: 1px solid #e4e7ed;
  1934 + cursor: pointer;
  1935 + }
  1936 +}
  1937 +
  1938 +/* 服务日志图片预览弹窗(弹窗 append-to-body,需全局样式) */
  1939 +.service-log-image-dialog {
  1940 + .el-dialog__body {
  1941 + padding: 16px 24px 24px;
  1942 + }
  1943 +
  1944 + .consume-service-log-content {
  1945 + max-height: 70vh;
  1946 + overflow-y: auto;
  1947 + }
  1948 +
  1949 + .log-list .log-item {
  1950 + margin-bottom: 24px;
  1951 +
  1952 + &:last-child {
  1953 + margin-bottom: 0;
  1954 + }
  1955 + }
  1956 +
  1957 + .no-data {
  1958 + padding: 40px 0;
  1959 + }
  1960 +
  1961 + .image-preview-content {
  1962 + min-height: 80px;
  1963 + }
  1964 +
  1965 + .preview-header {
  1966 + display: flex;
  1967 + align-items: center;
  1968 + gap: 24px;
  1969 + padding: 12px 16px;
  1970 + margin-bottom: 16px;
  1971 + background: linear-gradient(135deg, #f8f9fc 0%, #f0f2f7 100%);
  1972 + border-radius: 8px;
  1973 + border-left: 4px solid #409EFF;
  1974 + }
  1975 +
  1976 + .preview-meta {
  1977 + font-size: 13px;
  1978 + color: #606266;
  1979 +
  1980 + i {
  1981 + margin-right: 6px;
  1982 + color: #909399;
  1983 + }
  1984 + }
  1985 +
  1986 + .preview-row {
  1987 + display: flex;
  1988 + flex-wrap: wrap;
  1989 + gap: 24px;
  1990 +
  1991 + .image-section {
  1992 + flex: 1;
  1993 + min-width: 280px;
  1994 + }
  1995 + }
  1996 +
  1997 + .image-section {
  1998 + padding: 16px;
  1999 + background: #fafbfc;
  2000 + border-radius: 8px;
  2001 + border: 1px solid #e4e7ed;
  2002 + }
  2003 +
  2004 + .image-section-title {
  2005 + font-size: 14px;
  2006 + font-weight: 600;
  2007 + color: #303133;
  2008 + margin-bottom: 12px;
  2009 + padding-bottom: 8px;
  2010 + border-bottom: 1px solid #e4e7ed;
  2011 +
  2012 + i {
  2013 + margin-right: 6px;
  2014 + color: #409EFF;
  2015 + }
  2016 + }
  2017 +
  2018 + .image-list {
  2019 + display: grid;
  2020 + grid-template-columns: repeat(3, 1fr);
  2021 + gap: 12px;
  2022 + }
  2023 +
  2024 + .preview-img {
  2025 + width: 100%;
  2026 + aspect-ratio: 1;
  2027 + max-height: 160px;
  2028 + border-radius: 8px;
  2029 + border: 1px solid #e4e7ed;
  2030 + cursor: pointer;
  2031 + transition: border-color 0.2s, box-shadow 0.2s;
  2032 +
  2033 + &:hover {
  2034 + border-color: #409EFF;
  2035 + box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
  2036 + }
  2037 + }
  2038 +
  2039 + .image-placeholder {
  2040 + grid-column: 1 / 2;
  2041 + display: flex;
  2042 + flex-direction: column;
  2043 + align-items: center;
  2044 + justify-content: center;
  2045 + gap: 8px;
  2046 + aspect-ratio: 1;
  2047 + max-height: 160px;
  2048 + background: #f5f7fa;
  2049 + border: 1px dashed #dcdfe6;
  2050 + border-radius: 8px;
  2051 + color: #c0c4cc;
  2052 + font-size: 13px;
  2053 +
  2054 + i {
  2055 + font-size: 40px;
  2056 + }
  2057 + }
  2058 +
  2059 + .preview-remark {
  2060 + margin-top: 16px;
  2061 + padding: 12px 16px;
  2062 + font-size: 13px;
  2063 + color: #606266;
  2064 + background: #fafbfc;
  2065 + border-radius: 8px;
  2066 + border: 1px solid #e4e7ed;
  2067 + line-height: 1.6;
  2068 +
  2069 + .remark-label {
  2070 + font-weight: 600;
  2071 + color: #303133;
  2072 + margin-right: 4px;
  2073 + }
  2074 + }
  2075 +}
  2076 +</style>
antis-ncc-admin/src/views/lqKhxx/index.vue
@@ -217,40 +217,28 @@ @@ -217,40 +217,28 @@
217 <div class="card-content"> 217 <div class="card-content">
218 <div class="data-grid"> 218 <div class="data-grid">
219 <div class="data-item"> 219 <div class="data-item">
220 - <span class="label">  
221 - <i class="el-icon-phone icon-inline"></i>手机号  
222 - </span>  
223 - <span class="value">{{ item.sjh || '-' }}</span> 220 + <span class="label"><i class="el-icon-phone icon-inline"></i>手机号</span>
  221 + <span class="value text-ellipsis" :title="item.sjh">{{ item.sjh || '无' }}</span>
224 </div> 222 </div>
225 <div class="data-item"> 223 <div class="data-item">
226 - <span class="label">  
227 - <i class="el-icon-shop icon-inline"></i>归属门店  
228 - </span>  
229 - <span class="value text-truncate" :title="item.gsmdName">{{  
230 - item.gsmdName ||  
231 - '-'  
232 - }}</span> 224 + <span class="label"><i class="el-icon-shop icon-inline"></i>归属门店</span>
  225 + <span class="value text-ellipsis" :title="item.gsmdName">{{ item.gsmdName || '无' }}</span>
233 </div> 226 </div>
234 <div class="data-item"> 227 <div class="data-item">
235 - <span class="label">  
236 - <i class="el-icon-user icon-inline"></i>客户类型  
237 - </span>  
238 - <span class="value">{{ item.khlxName || '-' }}</span> 228 + <span class="label"><i class="el-icon-user icon-inline"></i>客户类型</span>
  229 + <span class="value text-ellipsis">{{ item.khlxName || '无' }}</span>
239 </div> 230 </div>
240 <div class="data-item"> 231 <div class="data-item">
241 - <span class="label">  
242 - <i class="el-icon-time icon-inline"  
243 - :class="{ 'icon-warning': item.sleepDays > 0 }"></i>沉睡天数  
244 - </span>  
245 - <span class="value" :class="{ 'text-danger': item.sleepDays > 0 }">{{  
246 - item.sleepDays || 0  
247 - }}天</span> 232 + <span class="label"><i class="el-icon-time icon-inline" :class="{ 'icon-warning': item.sleepDays > 0 }"></i>沉睡</span>
  233 + <span class="value" :class="{ 'text-danger': item.sleepDays > 0 }">{{ (item.sleepDays != null ? item.sleepDays : 0) }}天</span>
248 </div> 234 </div>
249 - <div class="data-item full-width">  
250 - <span class="label">  
251 - <i class="el-icon-calendar icon-inline"></i>最后到店  
252 - </span>  
253 - <span class="value">{{ formatDate(item.lastVisitTime) }}</span> 235 + <div class="data-item">
  236 + <span class="label"><i class="el-icon-cake icon-inline"></i>生日</span>
  237 + <span class="value text-ellipsis">{{ formatBirthdayDisplay(item) }}</span>
  238 + </div>
  239 + <div class="data-item">
  240 + <span class="label"><i class="el-icon-calendar icon-inline"></i>最后到店</span>
  241 + <span class="value text-ellipsis">{{ formatDate(item.lastVisitTime) }}</span>
254 </div> 242 </div>
255 </div> 243 </div>
256 244
@@ -364,6 +352,16 @@ @@ -364,6 +352,16 @@
364 </el-tag> 352 </el-tag>
365 </template> 353 </template>
366 </el-table-column> 354 </el-table-column>
  355 + <!-- 会员生日 -->
  356 + <el-table-column align="center" v-if="colId === 'birthday' && tableColumnsVisible[colId] !== false" key="birthday" label="会员生日"
  357 + :min-width="tableColumnsMeta.birthday.width">
  358 + <template slot-scope="scope">
  359 + <div class="table-cell-with-icon">
  360 + <i class="el-icon-cake cell-icon warning"></i>
  361 + <span class="cell-text">{{ formatBirthdayDisplay(scope.row) }}</span>
  362 + </div>
  363 + </template>
  364 + </el-table-column>
367 <!-- 客户类型 --> 365 <!-- 客户类型 -->
368 <el-table-column align="center" v-if="colId === 'khlx' && tableColumnsVisible[colId] !== false" key="khlx" label="客户类型" 366 <el-table-column align="center" v-if="colId === 'khlx' && tableColumnsVisible[colId] !== false" key="khlx" label="客户类型"
369 :min-width="tableColumnsMeta.khlx.width" :sortable="tableColumnsMeta.khlx.sortable ? 'custom' : false" 367 :min-width="tableColumnsMeta.khlx.width" :sortable="tableColumnsMeta.khlx.sortable ? 'custom' : false"
@@ -517,7 +515,8 @@ @@ -517,7 +515,8 @@
517 <MemberRightsDialog v-if="memberRightsDialogVisible" ref="MemberRightsDialog" /> 515 <MemberRightsDialog v-if="memberRightsDialogVisible" ref="MemberRightsDialog" />
518 <DetailDialog v-if="detailDialogVisible" ref="DetailDialog" /> 516 <DetailDialog v-if="detailDialogVisible" ref="DetailDialog" />
519 <member-portrait-dialog :visible.sync="memberPortraitDialog.visible" 517 <member-portrait-dialog :visible.sync="memberPortraitDialog.visible"
520 - :member-id="memberPortraitDialog.memberId" /> 518 + :member-id="memberPortraitDialog.memberId"
  519 + @edit="handlePortraitEdit" />
521 </div> 520 </div>
522 </template> 521 </template>
523 <script> 522 <script>
@@ -588,13 +587,14 @@ export default { @@ -588,13 +587,14 @@ export default {
588 sidx: "id", 587 sidx: "id",
589 }, 588 },
590 // 列配置:顺序与显示(持久化到 localStorage) 589 // 列配置:顺序与显示(持久化到 localStorage)
591 - tableColumnsOrder: ['khmc', 'sjh', 'gsmd', 'xb', 'khlx', 'mainHealthUser', 'subHealthUser', 'jdqd', 'tjr', 'expandUser', 'memberType', 'consumeLevel', 'totalBillingAmount', 'remainingRightsAmount', 'firstVisitTime', 'lastVisitTime', 'visitDays', 'sleepDays'], 590 + tableColumnsOrder: ['khmc', 'sjh', 'gsmd', 'xb', 'birthday', 'khlx', 'mainHealthUser', 'subHealthUser', 'jdqd', 'tjr', 'expandUser', 'memberType', 'consumeLevel', 'totalBillingAmount', 'remainingRightsAmount', 'firstVisitTime', 'lastVisitTime', 'visitDays', 'sleepDays'],
592 tableColumnsVisible: {}, 591 tableColumnsVisible: {},
593 tableColumnsMeta: { 592 tableColumnsMeta: {
594 khmc: { label: '客户名称', width: '250px', sortable: true, sidx: 'khmc' }, 593 khmc: { label: '客户名称', width: '250px', sortable: true, sidx: 'khmc' },
595 sjh: { label: '手机号', width: '150px', sortable: true, sidx: 'sjh' }, 594 sjh: { label: '手机号', width: '150px', sortable: true, sidx: 'sjh' },
596 gsmd: { label: '归属门店', width: '160px', sortable: true, sidx: 'gsmdName' }, 595 gsmd: { label: '归属门店', width: '160px', sortable: true, sidx: 'gsmdName' },
597 xb: { label: '性别', width: '85px', sortable: true, sidx: 'xb' }, 596 xb: { label: '性别', width: '85px', sortable: true, sidx: 'xb' },
  597 + birthday: { label: '会员生日', width: '130px', sortable: false },
598 khlx: { label: '客户类型', width: '100px', sortable: true, sidx: 'khlx' }, 598 khlx: { label: '客户类型', width: '100px', sortable: true, sidx: 'khlx' },
599 mainHealthUser: { label: '健康师', width: '85px', sortable: false }, 599 mainHealthUser: { label: '健康师', width: '85px', sortable: false },
600 subHealthUser: { label: '负责顾问', width: '95px', sortable: false }, 600 subHealthUser: { label: '负责顾问', width: '95px', sortable: false },
@@ -1065,6 +1065,16 @@ export default { @@ -1065,6 +1065,16 @@ export default {
1065 this.initData() 1065 this.initData()
1066 }, 1066 },
1067 // 格式化日期 1067 // 格式化日期
  1068 + formatBirthdayDisplay(row) {
  1069 + if (!row) return '无'
  1070 + if (row.birthdayType === 1 && row.yinlsr) return `农历 ${row.yinlsr}`
  1071 + if (row.yanglsr) {
  1072 + const d = new Date(row.yanglsr)
  1073 + if (!isNaN(d.getTime())) return `阳历 ${d.getMonth() + 1}月${d.getDate()}日`
  1074 + }
  1075 + if (row.yinlsr) return `农历 ${row.yinlsr}`
  1076 + return '无'
  1077 + },
1068 formatDate(date) { 1078 formatDate(date) {
1069 if (!date) return '无' 1079 if (!date) return '无'
1070 const d = new Date(date) 1080 const d = new Date(date)
@@ -1115,6 +1125,13 @@ export default { @@ -1115,6 +1125,13 @@ export default {
1115 memberId: memberId 1125 memberId: memberId
1116 } 1126 }
1117 }, 1127 },
  1128 + // 会员画像内点击「修改资料」:关闭画像并打开编辑
  1129 + handlePortraitEdit(memberId) {
  1130 + this.memberPortraitDialog.visible = false
  1131 + if (memberId) {
  1132 + this.addOrUpdateHandle(memberId)
  1133 + }
  1134 + },
1118 // 显示详情 1135 // 显示详情
1119 showDetail(id) { 1136 showDetail(id) {
1120 this.detailDialogVisible = true 1137 this.detailDialogVisible = true
@@ -1178,7 +1195,14 @@ export default { @@ -1178,7 +1195,14 @@ export default {
1178 const saved = localStorage.getItem('lqKhxx_columnConfig') 1195 const saved = localStorage.getItem('lqKhxx_columnConfig')
1179 if (saved) { 1196 if (saved) {
1180 const { order, visible } = JSON.parse(saved) 1197 const { order, visible } = JSON.parse(saved)
1181 - if (Array.isArray(order)) this.tableColumnsOrder = order 1198 + if (Array.isArray(order)) {
  1199 + this.tableColumnsOrder = order
  1200 + // 若旧配置无 birthday,插入到 xb 后
  1201 + if (!this.tableColumnsOrder.includes('birthday')) {
  1202 + const xbIdx = this.tableColumnsOrder.indexOf('xb')
  1203 + this.tableColumnsOrder.splice(xbIdx >= 0 ? xbIdx + 1 : 0, 0, 'birthday')
  1204 + }
  1205 + }
1182 if (visible && typeof visible === 'object') this.tableColumnsVisible = { ...this.tableColumnsVisible, ...visible } 1206 if (visible && typeof visible === 'object') this.tableColumnsVisible = { ...this.tableColumnsVisible, ...visible }
1183 } 1207 }
1184 } catch (e) { console.warn('loadColumnConfig error:', e) } 1208 } catch (e) { console.warn('loadColumnConfig error:', e) }
@@ -2011,34 +2035,16 @@ export default { @@ -2011,34 +2035,16 @@ export default {
2011 } 2035 }
2012 2036
2013 .card-header-wrapper { 2037 .card-header-wrapper {
2014 - padding: 10px 12px 8px; // 缩小卡片头部内边距  
2015 - border-bottom: 1px solid rgba(255, 255, 255, 0.3);  
2016 - background: linear-gradient(180deg,  
2017 - rgba(255, 255, 255, 0.4) 0%,  
2018 - rgba(255, 255, 255, 0.2) 50%,  
2019 - rgba(255, 255, 255, 0) 100%);  
2020 - position: relative;  
2021 -  
2022 - // 添加微妙的光影效果  
2023 - &::after {  
2024 - content: '';  
2025 - position: absolute;  
2026 - bottom: 0;  
2027 - left: 16px;  
2028 - right: 16px;  
2029 - height: 1px;  
2030 - background: linear-gradient(90deg,  
2031 - transparent 0%,  
2032 - rgba(255, 255, 255, 0.5) 50%,  
2033 - transparent 100%);  
2034 - } 2038 + padding: 12px;
  2039 + border-bottom: 1px solid rgba(226, 232, 240, 0.8);
  2040 + background: linear-gradient(180deg, rgba(248, 250, 252, 0.6) 0%, rgba(241, 245, 249, 0.3) 100%);
2035 2041
2036 .header-top { 2042 .header-top {
2037 display: flex; 2043 display: flex;
2038 justify-content: space-between; 2044 justify-content: space-between;
2039 align-items: center; 2045 align-items: center;
2040 - margin-bottom: 6px; // 缩小间距  
2041 - gap: 4px; 2046 + gap: 8px;
  2047 + flex-wrap: nowrap;
2042 2048
2043 .info-left { 2049 .info-left {
2044 display: flex; 2050 display: flex;
@@ -2047,32 +2053,23 @@ export default { @@ -2047,32 +2053,23 @@ export default {
2047 min-width: 0; 2053 min-width: 0;
2048 2054
2049 .member-name { 2055 .member-name {
2050 - font-size: 16px; // 增大标题字体,匹配卡片大小  
2051 - font-weight: 700;  
2052 - color: #0F172A; 2056 + font-size: 15px;
  2057 + font-weight: 600;
  2058 + color: #1e293b;
2053 margin-right: 6px; 2059 margin-right: 6px;
2054 white-space: nowrap; 2060 white-space: nowrap;
2055 overflow: hidden; 2061 overflow: hidden;
2056 text-overflow: ellipsis; 2062 text-overflow: ellipsis;
2057 - letter-spacing: -0.02em;  
2058 - line-height: 1.4; // 合理的行高  
2059 - text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8); 2063 + line-height: 1.4;
2060 } 2064 }
2061 2065
2062 .gender-icon { 2066 .gender-icon {
2063 - font-size: 14px; // 增大图标  
2064 -  
2065 - &.gender-male {  
2066 - color: #409EFF;  
2067 - }  
2068 -  
2069 - &.gender-female {  
2070 - color: #F56C6C;  
2071 - } 2067 + font-size: 14px;
  2068 + flex-shrink: 0;
2072 2069
2073 - &.gender-unknown {  
2074 - color: #909399;  
2075 - } 2070 + &.gender-male { color: #409EFF; }
  2071 + &.gender-female { color: #F56C6C; }
  2072 + &.gender-unknown { color: #94a3b8; }
2076 } 2073 }
2077 } 2074 }
2078 } 2075 }
@@ -2080,98 +2077,62 @@ export default { @@ -2080,98 +2077,62 @@ export default {
2080 .header-tags { 2077 .header-tags {
2081 display: flex; 2078 display: flex;
2082 flex-wrap: wrap; 2079 flex-wrap: wrap;
2083 - gap: 6px;  
2084 - min-height: 24px; 2080 + gap: 4px;
  2081 + margin-top: 8px;
2085 2082
2086 .mini-tag { 2083 .mini-tag {
2087 - border-radius: 10px;  
2088 - padding: 4px 8px;  
2089 - height: 20px;  
2090 - line-height: 12px; 2084 + border-radius: 6px;
  2085 + padding: 2px 6px;
  2086 + height: 18px;
  2087 + line-height: 14px;
2091 font-size: 11px; 2088 font-size: 11px;
2092 - font-weight: 600;  
2093 - transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);  
2094 - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);  
2095 - will-change: box-shadow;  
2096 - transform: translateZ(0); // 启用硬件加速  
2097 -  
2098 - &:hover {  
2099 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);  
2100 - } 2089 + font-weight: 500;
2101 } 2090 }
2102 } 2091 }
2103 2092
2104 .level-tag { 2093 .level-tag {
2105 - font-weight: 700;  
2106 - border-radius: 12px;  
2107 - padding: 5px 12px;  
2108 - font-size: 13px;  
2109 - letter-spacing: 0.03em;  
2110 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);  
2111 - transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);  
2112 - will-change: box-shadow;  
2113 - transform: translateZ(0); // 启用硬件加速  
2114 -  
2115 - &:hover {  
2116 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);  
2117 - } 2094 + font-weight: 600;
  2095 + border-radius: 8px;
  2096 + padding: 4px 10px;
  2097 + font-size: 12px;
  2098 + flex-shrink: 0;
2118 } 2099 }
2119 } 2100 }
2120 2101
2121 .card-content { 2102 .card-content {
2122 flex: 1; 2103 flex: 1;
2123 - padding: 10px 12px; // 缩小内容区内边距 2104 + padding: 12px;
2124 display: flex; 2105 display: flex;
2125 flex-direction: column; 2106 flex-direction: column;
2126 - gap: 8px; // 缩小间距 2107 + gap: 10px;
  2108 + min-width: 0;
2127 2109
2128 .data-grid { 2110 .data-grid {
2129 display: grid; 2111 display: grid;
2130 grid-template-columns: 1fr 1fr; 2112 grid-template-columns: 1fr 1fr;
2131 - gap: 6px 8px; // 缩小网格间距 2113 + gap: 8px 12px;
2132 2114
2133 .data-item { 2115 .data-item {
2134 display: flex; 2116 display: flex;
2135 - flex-direction: column;  
2136 - padding: 3px 0; // 缩小内边距  
2137 -  
2138 - &.full-width {  
2139 - grid-column: span 2;  
2140 - flex-direction: row;  
2141 - justify-content: space-between;  
2142 - align-items: center;  
2143 - padding: 6px 0 3px; // 缩小内边距  
2144 - border-top: 1px dashed rgba(255, 255, 255, 0.3);  
2145 - margin-top: 3px;  
2146 -  
2147 - .value {  
2148 - text-align: right;  
2149 - font-weight: 600;  
2150 - }  
2151 - } 2117 + flex-direction: row;
  2118 + align-items: center;
  2119 + gap: 6px;
  2120 + min-width: 0;
2152 2121
2153 .label { 2122 .label {
2154 - font-size: 13px; // 增大标签字体,匹配卡片大小  
2155 - color: #475569;  
2156 - margin-bottom: 4px; // 合理的间距  
2157 - font-weight: 600;  
2158 - text-transform: uppercase;  
2159 - letter-spacing: 0.05em;  
2160 - line-height: 1.4; // 合理的行高 2123 + flex-shrink: 0;
  2124 + min-width: 56px;
  2125 + font-size: 12px;
  2126 + color: #64748b;
  2127 + font-weight: 500;
  2128 + line-height: 1.4;
2161 display: flex; 2129 display: flex;
2162 align-items: center; 2130 align-items: center;
2163 - gap: 5px; // 合理的图标间距 2131 + gap: 4px;
2164 2132
2165 .icon-inline { 2133 .icon-inline {
2166 - font-size: 13px; // 增大图标  
2167 - opacity: 0.75;  
2168 - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);  
2169 - filter: drop-shadow(0 1px 1px rgba(255, 255, 255, 0.5));  
2170 - }  
2171 -  
2172 - &:hover .icon-inline {  
2173 - opacity: 1;  
2174 - transform: scale(1.15) rotate(5deg); 2134 + font-size: 12px;
  2135 + opacity: 0.8;
2175 } 2136 }
2176 } 2137 }
2177 2138
@@ -2181,33 +2142,25 @@ export default { @@ -2181,33 +2142,25 @@ export default {
2181 } 2142 }
2182 2143
2183 .value { 2144 .value {
2184 - font-size: 14px; // 增大数据值字体,匹配卡片大小  
2185 - color: #0F172A;  
2186 - font-weight: 600;  
2187 - white-space: nowrap; 2145 + flex: 1;
  2146 + font-size: 13px;
  2147 + color: #1e293b;
  2148 + font-weight: 500;
  2149 + line-height: 1.4;
  2150 + min-width: 0;
2188 overflow: hidden; 2151 overflow: hidden;
2189 text-overflow: ellipsis; 2152 text-overflow: ellipsis;
2190 - line-height: 1.5; // 合理的行高  
2191 - letter-spacing: -0.01em; 2153 + white-space: nowrap;
2192 2154
2193 - &.text-truncate {  
2194 - max-width: 100%; 2155 + &.text-ellipsis {
  2156 + overflow: hidden;
  2157 + text-overflow: ellipsis;
  2158 + white-space: nowrap;
2195 } 2159 }
2196 2160
2197 &.text-danger { 2161 &.text-danger {
2198 color: #EF4444; 2162 color: #EF4444;
2199 - font-weight: 700;  
2200 - position: relative;  
2201 -  
2202 - &::after {  
2203 - content: '';  
2204 - position: absolute;  
2205 - bottom: -2px;  
2206 - left: 0;  
2207 - right: 0;  
2208 - height: 2px;  
2209 - background: linear-gradient(90deg, rgba(239, 68, 68, 0.3), transparent);  
2210 - } 2163 + font-weight: 600;
2211 } 2164 }
2212 } 2165 }
2213 } 2166 }
@@ -2215,79 +2168,41 @@ export default { @@ -2215,79 +2168,41 @@ export default {
2215 2168
2216 .amount-section { 2169 .amount-section {
2217 margin-top: auto; 2170 margin-top: auto;
2218 - // 更精致的渐变背景  
2219 - background: linear-gradient(135deg,  
2220 - rgba(37, 99, 235, 0.12) 0%,  
2221 - rgba(59, 130, 246, 0.08) 50%,  
2222 - rgba(96, 165, 250, 0.06) 100%);  
2223 - backdrop-filter: blur(12px) saturate(150%);  
2224 - -webkit-backdrop-filter: blur(12px) saturate(150%);  
2225 - border: 1px solid rgba(255, 255, 255, 0.5);  
2226 - border-radius: 10px; // 缩小圆角  
2227 - padding: 8px 10px; // 缩小内边距 2171 + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(96, 165, 250, 0.05) 100%);
  2172 + border: 1px solid rgba(59, 130, 246, 0.15);
  2173 + border-radius: 8px;
  2174 + padding: 10px 12px;
2228 display: flex; 2175 display: flex;
2229 justify-content: space-between; 2176 justify-content: space-between;
2230 align-items: center; 2177 align-items: center;
2231 - margin-top: 6px; // 缩小上边距  
2232 - position: relative;  
2233 - overflow: hidden;  
2234 - box-shadow:  
2235 - inset 0 1px 2px rgba(255, 255, 255, 0.6),  
2236 - inset 0 -1px 1px rgba(37, 99, 235, 0.1),  
2237 - 0 2px 8px rgba(37, 99, 235, 0.1);  
2238 -  
2239 - &::before {  
2240 - content: '';  
2241 - position: absolute;  
2242 - top: 0;  
2243 - left: 0;  
2244 - right: 0;  
2245 - height: 2px;  
2246 - background: linear-gradient(90deg,  
2247 - transparent 0%,  
2248 - rgba(37, 99, 235, 0.4) 50%,  
2249 - transparent 100%);  
2250 - box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2);  
2251 - } 2178 + gap: 12px;
2252 2179
2253 .amount-box { 2180 .amount-box {
2254 flex: 1; 2181 flex: 1;
2255 display: flex; 2182 display: flex;
2256 flex-direction: column; 2183 flex-direction: column;
2257 align-items: center; 2184 align-items: center;
2258 - gap: 3px; // 缩小间距 2185 + gap: 4px;
2259 2186
2260 .sub-label { 2187 .sub-label {
2261 - font-size: 13px; // 增大金额标签字体  
2262 - color: #475569;  
2263 - margin-bottom: 4px; // 合理的间距  
2264 - font-weight: 600;  
2265 - text-transform: uppercase;  
2266 - letter-spacing: 0.06em; 2188 + font-size: 12px;
  2189 + color: #64748b;
  2190 + font-weight: 500;
2267 display: flex; 2191 display: flex;
2268 align-items: center; 2192 align-items: center;
2269 justify-content: center; 2193 justify-content: center;
2270 - gap: 5px; // 合理的图标间距 2194 + gap: 4px;
2271 2195
2272 .icon-inline { 2196 .icon-inline {
2273 - font-size: 13px; // 增大图标 2197 + font-size: 12px;
2274 opacity: 0.8; 2198 opacity: 0.8;
2275 - transition: all 0.2s ease;  
2276 - }  
2277 -  
2278 - &:hover .icon-inline {  
2279 - opacity: 1;  
2280 - transform: scale(1.1);  
2281 } 2199 }
2282 } 2200 }
2283 2201
2284 .sub-value { 2202 .sub-value {
2285 - font-size: 18px; // 增大金额字体,匹配卡片大小  
2286 - font-weight: 700;  
2287 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', 'Helvetica Neue', 'Inter', sans-serif;  
2288 - letter-spacing: -0.03em;  
2289 - line-height: 1.3; // 合理的行高  
2290 - transition: all 0.2s ease; 2203 + font-size: 16px;
  2204 + font-weight: 600;
  2205 + line-height: 1.3;
2291 2206
2292 &.primary-color { 2207 &.primary-color {
2293 color: #2563EB; 2208 color: #2563EB;
@@ -2309,40 +2224,21 @@ export default { @@ -2309,40 +2224,21 @@ export default {
2309 2224
2310 .divider { 2225 .divider {
2311 width: 1px; 2226 width: 1px;
2312 - height: 32px; // 合理的高度  
2313 - background: linear-gradient(180deg, transparent, rgba(226, 232, 240, 0.8), transparent);  
2314 - margin: 0 12px; // 合理的边距 2227 + height: 28px;
  2228 + background: rgba(226, 232, 240, 0.8);
  2229 + flex-shrink: 0;
2315 } 2230 }
2316 } 2231 }
2317 } 2232 }
2318 2233
2319 .card-footer { 2234 .card-footer {
2320 - margin-top: 0;  
2321 - padding: 12px 14px; // 符合规范:12px内边距  
2322 - border-top: 1px solid rgba(255, 255, 255, 0.3); 2235 + padding: 10px 12px;
  2236 + border-top: 1px solid rgba(226, 232, 240, 0.8);
2323 display: flex; 2237 display: flex;
2324 - justify-content: space-between; 2238 + justify-content: flex-start;
2325 align-items: center; 2239 align-items: center;
2326 - background: linear-gradient(180deg,  
2327 - rgba(255, 255, 255, 0) 0%,  
2328 - rgba(255, 255, 255, 0.2) 50%,  
2329 - rgba(248, 250, 252, 0.4) 100%);  
2330 - gap: 8px; // 合理的间距  
2331 - position: relative;  
2332 -  
2333 - // 顶部高光线条  
2334 - &::before {  
2335 - content: '';  
2336 - position: absolute;  
2337 - top: 0;  
2338 - left: 16px;  
2339 - right: 16px;  
2340 - height: 1px;  
2341 - background: linear-gradient(90deg,  
2342 - transparent 0%,  
2343 - rgba(255, 255, 255, 0.6) 50%,  
2344 - transparent 100%);  
2345 - } 2240 + gap: 4px;
  2241 + background: rgba(248, 250, 252, 0.5);
2346 2242
2347 .action-btn { 2243 .action-btn {
2348 padding: 6px 12px; // 合理的内边距 2244 padding: 6px 12px; // 合理的内边距
antis-ncc-admin/src/views/lqKhxxBirthday/index.vue
@@ -14,6 +14,10 @@ @@ -14,6 +14,10 @@
14 </el-select> 14 </el-select>
15 </div> 15 </div>
16 <div class="filter-item"> 16 <div class="filter-item">
  17 + <el-checkbox v-model="showSolar">显示阳历生日会员</el-checkbox>
  18 + <el-checkbox v-model="showLunar">显示农历生日会员</el-checkbox>
  19 + </div>
  20 + <div class="filter-item">
17 <el-button type="primary" icon="el-icon-refresh" @click="loadBirthdayData">刷新</el-button> 21 <el-button type="primary" icon="el-icon-refresh" @click="loadBirthdayData">刷新</el-button>
18 </div> 22 </div>
19 23
@@ -58,7 +62,7 @@ @@ -58,7 +62,7 @@
58 <div class="day-number">{{ data.day.split('-')[2] }}</div> 62 <div class="day-number">{{ data.day.split('-')[2] }}</div>
59 <div class="birthday-list"> 63 <div class="birthday-list">
60 <el-tooltip v-for="member in getBirthdaysByDate(data.day)" :key="member.id" 64 <el-tooltip v-for="member in getBirthdaysByDate(data.day)" :key="member.id"
61 - :content="`${member.khmc} (${member.consumeLevelName})`" placement="top"> 65 + :content="`${member.khmc} (${member.consumeLevelName}) ${member.birthdayTypeName || ''}`" placement="top">
62 <span class="birthday-member" :class="`level-${member.consumeLevel}`" 66 <span class="birthday-member" :class="`level-${member.consumeLevel}`"
63 @click.stop="showMemberDetail(member)"> 67 @click.stop="showMemberDetail(member)">
64 <i class="el-icon-user"></i>{{ member.khmc }} 68 <i class="el-icon-user"></i>{{ member.khmc }}
@@ -111,10 +115,18 @@ @@ -111,10 +115,18 @@
111 <span class="value">{{ selectedMember.gsmdName || '无' }}</span> 115 <span class="value">{{ selectedMember.gsmdName || '无' }}</span>
112 </div> 116 </div>
113 <div class="info-row"> 117 <div class="info-row">
114 - <label>生日日期</label> 118 + <label>生日类型</label>
  119 + <span class="value">{{ selectedMember.birthdayTypeName || '无' }}</span>
  120 + </div>
  121 + <div class="info-row">
  122 + <label>阳历生日</label>
115 <span class="value">{{ formatBirthday(selectedMember.yanglsr) }}</span> 123 <span class="value">{{ formatBirthday(selectedMember.yanglsr) }}</span>
116 </div> 124 </div>
117 <div class="info-row"> 125 <div class="info-row">
  126 + <label>农历生日</label>
  127 + <span class="value">{{ selectedMember.yinlsr || '无' }}</span>
  128 + </div>
  129 + <div class="info-row">
118 <label>年龄</label> 130 <label>年龄</label>
119 <span class="value">{{ selectedMember.age }}岁</span> 131 <span class="value">{{ selectedMember.age }}岁</span>
120 </div> 132 </div>
@@ -150,6 +162,8 @@ export default { @@ -150,6 +162,8 @@ export default {
150 savedCalendarDate: null, // 保存日历的日期状态 162 savedCalendarDate: null, // 保存日历的日期状态
151 selectedStore: '', 163 selectedStore: '',
152 storeList: [], 164 storeList: [],
  165 + showSolar: true,
  166 + showLunar: true,
153 birthdayData: [], 167 birthdayData: [],
154 birthdayMap: {}, // 日期到会员的映射 168 birthdayMap: {}, // 日期到会员的映射
155 detailDialogVisible: false, 169 detailDialogVisible: false,
@@ -158,6 +172,21 @@ export default { @@ -158,6 +172,21 @@ export default {
158 } 172 }
159 }, 173 },
160 watch: { 174 watch: {
  175 + showSolar() {
  176 + this.loadBirthdayData()
  177 + },
  178 + showLunar() {
  179 + this.loadBirthdayData()
  180 + },
  181 + currentDate: {
  182 + handler(newVal, oldVal) {
  183 + if (!oldVal) return
  184 + const newMonth = newVal.getFullYear() * 12 + newVal.getMonth()
  185 + const oldMonth = oldVal.getFullYear() * 12 + oldVal.getMonth()
  186 + if (newMonth !== oldMonth) this.loadBirthdayData()
  187 + },
  188 + deep: true
  189 + },
161 detailDialogVisible(newVal, oldVal) { 190 detailDialogVisible(newVal, oldVal) {
162 if (newVal === true) { 191 if (newVal === true) {
163 // 弹窗打开时,保存当前日历日期 192 // 弹窗打开时,保存当前日历日期
@@ -196,16 +225,36 @@ export default { @@ -196,16 +225,36 @@ export default {
196 }, 225 },
197 226
198 /** 227 /**
  228 + * 获取当前日历可见月份的起止日期
  229 + */
  230 + getMonthRange() {
  231 + const d = new Date(this.currentDate)
  232 + const year = d.getFullYear()
  233 + const month = d.getMonth()
  234 + const start = new Date(year, month, 1)
  235 + const end = new Date(year, month + 1, 0)
  236 + return {
  237 + startDate: this.formatDate(start),
  238 + endDate: this.formatDate(end)
  239 + }
  240 + },
  241 +
  242 + /**
199 * 加载生日数据 243 * 加载生日数据
200 */ 244 */
201 async loadBirthdayData() { 245 async loadBirthdayData() {
202 this.loading = true 246 this.loading = true
203 try { 247 try {
  248 + const { startDate, endDate } = this.getMonthRange()
204 const res = await request({ 249 const res = await request({
205 url: '/api/Extend/LqKhxx/GetUpcomingBirthdays', 250 url: '/api/Extend/LqKhxx/GetUpcomingBirthdays',
206 method: 'get', 251 method: 'get',
207 data: { 252 data: {
208 - storeId: this.selectedStore 253 + storeId: this.selectedStore,
  254 + showSolar: this.showSolar,
  255 + showLunar: this.showLunar,
  256 + startDate,
  257 + endDate
209 } 258 }
210 }) 259 })
211 260
antis-ncc-admin/src/views/personalPerformanceStatistics/index.vue
@@ -53,6 +53,36 @@ @@ -53,6 +53,36 @@
53 {{ formatMoney(scope.row.TotalPerformance) }} 53 {{ formatMoney(scope.row.TotalPerformance) }}
54 </template> 54 </template>
55 </el-table-column> 55 </el-table-column>
  56 + <el-table-column prop="LifeBeautyPerformance" label="生美业绩" width="95" align="right">
  57 + <template slot-scope="scope">
  58 + {{ formatMoney(scope.row.LifeBeautyPerformance) }}
  59 + </template>
  60 + </el-table-column>
  61 + <el-table-column prop="MedicalBeautyPerformance" label="医美业绩" width="95" align="right">
  62 + <template slot-scope="scope">
  63 + {{ formatMoney(scope.row.MedicalBeautyPerformance) }}
  64 + </template>
  65 + </el-table-column>
  66 + <el-table-column prop="TechBeautyPerformance" label="科美业绩" width="95" align="right">
  67 + <template slot-scope="scope">
  68 + {{ formatMoney(scope.row.TechBeautyPerformance) }}
  69 + </template>
  70 + </el-table-column>
  71 + <el-table-column prop="CooperationCategoryPerformance" label="合作业绩" width="95" align="right">
  72 + <template slot-scope="scope">
  73 + {{ formatMoney(scope.row.CooperationCategoryPerformance) }}
  74 + </template>
  75 + </el-table-column>
  76 + <el-table-column prop="OtherPerformance" label="其他业绩" width="95" align="right">
  77 + <template slot-scope="scope">
  78 + {{ formatMoney(scope.row.OtherPerformance) }}
  79 + </template>
  80 + </el-table-column>
  81 + <el-table-column prop="ProductPerformance" label="产品业绩" width="95" align="right">
  82 + <template slot-scope="scope">
  83 + {{ formatMoney(scope.row.ProductPerformance) }}
  84 + </template>
  85 + </el-table-column>
56 <el-table-column prop="FirstOrderPerformance" label="首单业绩" width="100" align="right"> 86 <el-table-column prop="FirstOrderPerformance" label="首单业绩" width="100" align="right">
57 <template slot-scope="scope"> 87 <template slot-scope="scope">
58 {{ formatMoney(scope.row.FirstOrderPerformance) }} 88 {{ formatMoney(scope.row.FirstOrderPerformance) }}
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxBirthdayOutput.cs
@@ -58,6 +58,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx @@ -58,6 +58,11 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx
58 public int birthdayType { get; set; } 58 public int birthdayType { get; set; }
59 59
60 /// <summary> 60 /// <summary>
  61 + /// 生日类型名称(阳历生日/农历生日)
  62 + /// </summary>
  63 + public string birthdayTypeName { get; set; }
  64 +
  65 + /// <summary>
61 /// 生日日期(用于日历显示,格式:MM-DD) 66 /// 生日日期(用于日历显示,格式:MM-DD)
62 /// </summary> 67 /// </summary>
63 public string birthdayDate { get; set; } 68 public string birthdayDate { get; set; }
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatisticsPersonalPerformance/LqStatisticsPersonalPerformanceListOutput.cs
@@ -68,6 +68,36 @@ namespace NCC.Extend.Entitys.Dto.LqStatisticsPersonalPerformance @@ -68,6 +68,36 @@ namespace NCC.Extend.Entitys.Dto.LqStatisticsPersonalPerformance
68 public decimal CooperationPerformance { get; set; } 68 public decimal CooperationPerformance { get; set; }
69 69
70 /// <summary> 70 /// <summary>
  71 + /// 生美业绩(按品项分类 F_ItemCategory 统计)
  72 + /// </summary>
  73 + public decimal LifeBeautyPerformance { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 医美业绩(按品项分类 F_ItemCategory 统计)
  77 + /// </summary>
  78 + public decimal MedicalBeautyPerformance { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 科美业绩(按品项分类 F_ItemCategory 统计)
  82 + /// </summary>
  83 + public decimal TechBeautyPerformance { get; set; }
  84 +
  85 + /// <summary>
  86 + /// 合作业绩-分类(按品项分类 F_ItemCategory 统计,与 xmzl.fl3 合作业绩可能略有差异)
  87 + /// </summary>
  88 + public decimal CooperationCategoryPerformance { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 其他业绩(按品项分类 F_ItemCategory 统计,含空值)
  92 + /// </summary>
  93 + public decimal OtherPerformance { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 产品业绩(按品项分类 F_ItemCategory 统计)
  97 + /// </summary>
  98 + public decimal ProductPerformance { get; set; }
  99 +
  100 + /// <summary>
71 /// 订单数量 101 /// 订单数量
72 /// </summary> 102 /// </summary>
73 public int OrderCount { get; set; } 103 public int OrderCount { get; set; }
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStatisticsPersonalPerformance/LqStatisticsPersonalPerformanceListQueryInput.cs
@@ -42,5 +42,10 @@ namespace NCC.Extend.Entitys.Dto.LqStatisticsPersonalPerformance @@ -42,5 +42,10 @@ namespace NCC.Extend.Entitys.Dto.LqStatisticsPersonalPerformance
42 /// 岗位 42 /// 岗位
43 /// </summary> 43 /// </summary>
44 public string Position { get; set; } 44 public string Position { get; set; }
  45 +
  46 + /// <summary>
  47 + /// 当前页码(前端传 pageIndex 时使用,与 currentPage 二选一)
  48 + /// </summary>
  49 + public int pageIndex { get; set; }
45 } 50 }
46 } 51 }
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/MemberPortrait/MemberPortraitDtos.cs
@@ -97,6 +97,101 @@ namespace NCC.Extend.Entitys.Dto.MemberPortrait @@ -97,6 +97,101 @@ namespace NCC.Extend.Entitys.Dto.MemberPortrait
97 /// 会员类型列表(生美、医美、科技部、教育部) 97 /// 会员类型列表(生美、医美、科技部、教育部)
98 /// </summary> 98 /// </summary>
99 public List<MemberTypeInfo> MemberTypes { get; set; } = new List<MemberTypeInfo>(); 99 public List<MemberTypeInfo> MemberTypes { get; set; } = new List<MemberTypeInfo>();
  100 +
  101 + /// <summary>
  102 + /// 阳历生日
  103 + /// </summary>
  104 + public DateTime? Yanglsr { get; set; }
  105 +
  106 + /// <summary>
  107 + /// 农历生日(格式:腊月十九、四月廿一等)
  108 + /// </summary>
  109 + public string Yinlsr { get; set; }
  110 +
  111 + /// <summary>
  112 + /// 生日类型(0-阳历生日,1-农历生日)
  113 + /// </summary>
  114 + public int BirthdayType { get; set; }
  115 +
  116 + /// <summary>
  117 + /// 生日类型名称(阳历生日/农历生日)
  118 + /// </summary>
  119 + public string BirthdayTypeName { get; set; }
  120 +
  121 + /// <summary>
  122 + /// 年龄(根据阳历生日计算,无阳历生日时为 null)
  123 + /// </summary>
  124 + public int? Age { get; set; }
  125 +
  126 + /// <summary>
  127 + /// 性别
  128 + /// </summary>
  129 + public string Gender { get; set; }
  130 +
  131 + /// <summary>
  132 + /// 联系地址
  133 + /// </summary>
  134 + public string Address { get; set; }
  135 +
  136 + /// <summary>
  137 + /// 推荐人ID
  138 + /// </summary>
  139 + public string ReferrerId { get; set; }
  140 +
  141 + /// <summary>
  142 + /// 推荐人名称
  143 + /// </summary>
  144 + public string ReferrerName { get; set; }
  145 +
  146 + /// <summary>
  147 + /// 拓客人员ID
  148 + /// </summary>
  149 + public string ExpandUserId { get; set; }
  150 +
  151 + /// <summary>
  152 + /// 拓客人员名称
  153 + /// </summary>
  154 + public string ExpandUserName { get; set; }
  155 +
  156 + /// <summary>
  157 + /// 主健康师ID
  158 + /// </summary>
  159 + public string MainHealthUserId { get; set; }
  160 +
  161 + /// <summary>
  162 + /// 主健康师名称
  163 + /// </summary>
  164 + public string MainHealthUserName { get; set; }
  165 +
  166 + /// <summary>
  167 + /// 副健康师/负责顾问ID
  168 + /// </summary>
  169 + public string SubHealthUserId { get; set; }
  170 +
  171 + /// <summary>
  172 + /// 副健康师/负责顾问名称
  173 + /// </summary>
  174 + public string SubHealthUserName { get; set; }
  175 +
  176 + /// <summary>
  177 + /// 注册时间
  178 + /// </summary>
  179 + public DateTime? RegisterTime { get; set; }
  180 +
  181 + /// <summary>
  182 + /// 客户类型(khlx 枚举值)
  183 + /// </summary>
  184 + public string CustomerType { get; set; }
  185 +
  186 + /// <summary>
  187 + /// 客户类型名称(线索/新客/散客/会员/归档)
  188 + /// </summary>
  189 + public string CustomerTypeName { get; set; }
  190 +
  191 + /// <summary>
  192 + /// 备注
  193 + /// </summary>
  194 + public string Remark { get; set; }
100 } 195 }
101 196
102 /// <summary> 197 /// <summary>
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
@@ -144,7 +144,7 @@ namespace NCC.Extend.LqKhxx @@ -144,7 +144,7 @@ namespace NCC.Extend.LqKhxx
144 List<string> queryZcsj = input.zcsj != null ? input.zcsj.Split(',').ToObeject<List<string>>() : null; 144 List<string> queryZcsj = input.zcsj != null ? input.zcsj.Split(',').ToObeject<List<string>>() : null;
145 DateTime? startZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.First()) : null; 145 DateTime? startZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.First()) : null;
146 DateTime? endZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.Last()) : null; 146 DateTime? endZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.Last()) : null;
147 - 147 +
148 // 处理沉睡天数范围 148 // 处理沉睡天数范围
149 List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null; 149 List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null;
150 int? minSleepDays = null; 150 int? minSleepDays = null;
@@ -168,7 +168,7 @@ namespace NCC.Extend.LqKhxx @@ -168,7 +168,7 @@ namespace NCC.Extend.LqKhxx
168 minRemainingRights = min; 168 minRemainingRights = min;
169 maxRemainingRights = max; 169 maxRemainingRights = max;
170 } 170 }
171 - 171 +
172 var data = await _db.Queryable<LqKhxxEntity>() 172 var data = await _db.Queryable<LqKhxxEntity>()
173 .WhereIF(!string.IsNullOrEmpty(input.keyWord), p => p.Khmc.Contains(input.keyWord) || p.Sjh.Contains(input.keyWord) || p.Dah.Contains(input.keyWord)) 173 .WhereIF(!string.IsNullOrEmpty(input.keyWord), p => p.Khmc.Contains(input.keyWord) || p.Sjh.Contains(input.keyWord) || p.Dah.Contains(input.keyWord))
174 .WhereIF(input.IsTechMemberbh.HasValue, p => p.IsTechMember == input.IsTechMemberbh) 174 .WhereIF(input.IsTechMemberbh.HasValue, p => p.IsTechMember == input.IsTechMemberbh)
@@ -329,7 +329,7 @@ namespace NCC.Extend.LqKhxx @@ -329,7 +329,7 @@ namespace NCC.Extend.LqKhxx
329 minRemainingRights = min; 329 minRemainingRights = min;
330 maxRemainingRights = max; 330 maxRemainingRights = max;
331 } 331 }
332 - 332 +
333 var data = await _db.Queryable<LqKhxxEntity>() 333 var data = await _db.Queryable<LqKhxxEntity>()
334 .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id)) 334 .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
335 .WhereIF(!string.IsNullOrEmpty(input.khmc), p => p.Khmc.Contains(input.khmc)) 335 .WhereIF(!string.IsNullOrEmpty(input.khmc), p => p.Khmc.Contains(input.khmc))
@@ -711,7 +711,7 @@ namespace NCC.Extend.LqKhxx @@ -711,7 +711,7 @@ namespace NCC.Extend.LqKhxx
711 List<string> queryYanglsr = input.yanglsr != null ? input.yanglsr.Split(',').ToObeject<List<string>>() : null; 711 List<string> queryYanglsr = input.yanglsr != null ? input.yanglsr.Split(',').ToObeject<List<string>>() : null;
712 DateTime? startYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.First()) : null; 712 DateTime? startYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.First()) : null;
713 DateTime? endYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.Last()) : null; 713 DateTime? endYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.Last()) : null;
714 - 714 +
715 // 处理沉睡天数范围 715 // 处理沉睡天数范围
716 List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null; 716 List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null;
717 int? minSleepDays = null; 717 int? minSleepDays = null;
@@ -976,8 +976,8 @@ namespace NCC.Extend.LqKhxx @@ -976,8 +976,8 @@ namespace NCC.Extend.LqKhxx
976 // 根据消费等级编号获取消费等级名称 976 // 根据消费等级编号获取消费等级名称
977 string GetConsumeLevelNameFromLevel(int level) 977 string GetConsumeLevelNameFromLevel(int level)
978 { 978 {
979 - return MemberInfoUpdateConfig.ConsumeLevelNames.ContainsKey(level)  
980 - ? MemberInfoUpdateConfig.ConsumeLevelNames[level] 979 + return MemberInfoUpdateConfig.ConsumeLevelNames.ContainsKey(level)
  980 + ? MemberInfoUpdateConfig.ConsumeLevelNames[level]
981 : "D"; 981 : "D";
982 } 982 }
983 983
@@ -1133,8 +1133,8 @@ namespace NCC.Extend.LqKhxx @@ -1133,8 +1133,8 @@ namespace NCC.Extend.LqKhxx
1133 public async Task Update(string id, [FromBody] LqKhxxUpInput input) 1133 public async Task Update(string id, [FromBody] LqKhxxUpInput input)
1134 { 1134 {
1135 var entity = input.Adapt<LqKhxxEntity>(); 1135 var entity = input.Adapt<LqKhxxEntity>();
1136 - // 阳历生日转农历:若填写了 yanglsr,自动转换并保存到 yinlsr(格式:腊月十九、四月廿一等,便于直接阅读)  
1137 - if (input.yanglsr.HasValue) 1136 + // 阳历生日转农历:若填写了 yanglsr 且未显式填写 yinlsr,自动转换并保存到 yinlsr;若两者都填写则保留用户填写的值
  1137 + if (input.yanglsr.HasValue && string.IsNullOrEmpty(input.yinlsr))
1138 { 1138 {
1139 entity.Yinlsr = SolarToLunarString(input.yanglsr.Value); 1139 entity.Yinlsr = SolarToLunarString(input.yanglsr.Value);
1140 } 1140 }
@@ -2222,7 +2222,7 @@ namespace NCC.Extend.LqKhxx @@ -2222,7 +2222,7 @@ namespace NCC.Extend.LqKhxx
2222 // 查询会员的所有耗卡记录 2222 // 查询会员的所有耗卡记录
2223 var allConsumedItemsFromConsume = await _db.Queryable<LqXhPxmxEntity, LqXhHyhkEntity>( 2223 var allConsumedItemsFromConsume = await _db.Queryable<LqXhPxmxEntity, LqXhHyhkEntity>(
2224 (pxmx, hyhk) => new JoinQueryInfos(JoinType.Inner, pxmx.ConsumeInfoId == hyhk.Id)) 2224 (pxmx, hyhk) => new JoinQueryInfos(JoinType.Inner, pxmx.ConsumeInfoId == hyhk.Id))
2225 - .Where((pxmx, hyhk) => memberIdsList.Contains(hyhk.Hy) 2225 + .Where((pxmx, hyhk) => memberIdsList.Contains(hyhk.Hy)
2226 && pxmx.IsEffective == StatusEnum.有效.GetHashCode() 2226 && pxmx.IsEffective == StatusEnum.有效.GetHashCode()
2227 && hyhk.IsEffective == StatusEnum.有效.GetHashCode()) 2227 && hyhk.IsEffective == StatusEnum.有效.GetHashCode())
2228 .Where((pxmx, hyhk) => !string.IsNullOrEmpty(pxmx.BillingItemId)) 2228 .Where((pxmx, hyhk) => !string.IsNullOrEmpty(pxmx.BillingItemId))
@@ -3250,36 +3250,74 @@ WHERE kh.F_IsEffective = 1&quot;; @@ -3250,36 +3250,74 @@ WHERE kh.F_IsEffective = 1&quot;;
3250 #region 会员生日管理 3250 #region 会员生日管理
3251 3251
3252 /// <summary> 3252 /// <summary>
3253 - /// 获取门店未来30天过生日的会员列表 3253 + /// 获取门店指定日期范围内过生日的会员列表
3254 /// </summary> 3254 /// </summary>
3255 /// <remarks> 3255 /// <remarks>
3256 - /// 根据门店ID获取未来30天内过生日的会员信息,支持阳历生日和农历生日  
3257 - /// - 阳历生日:直接按公历月日计算今年/明年生日  
3258 - /// - 农历生日:将今年/明年农历月日转为公历日期后判断是否在未来30天内(便于按公历日期查询提醒) 3256 + /// 根据门店ID、日期范围、阳历/农历筛选条件获取过生日的会员信息
  3257 + /// - 按会员的生日类型(F_BirthdayType)决定使用阳历还是农历计算生日日期
  3258 + /// - 阳历生日:使用 yanglsr 按公历月日计算今年/明年生日
  3259 + /// - 农历生日:使用 yinlsr 将今年/明年农历月日转为公历后判断
3259 /// 3260 ///
3260 /// 会员等级说明:0=D,1=C,2=B,3=A,4=A+,5=A++ 3261 /// 会员等级说明:0=D,1=C,2=B,3=A,4=A+,5=A++
3261 /// 3262 ///
3262 /// 示例请求: 3263 /// 示例请求:
3263 /// ```http 3264 /// ```http
3264 - /// GET /api/Extend/LqKhxx/GetUpcomingBirthdays?storeId=门店ID 3265 + /// GET /api/Extend/LqKhxx/GetUpcomingBirthdays?storeId=门店ID&amp;showSolar=true&amp;showLunar=true&amp;startDate=2025-02-01&amp;endDate=2025-02-28
3265 /// ``` 3266 /// ```
3266 /// 3267 ///
3267 /// 参数说明: 3268 /// 参数说明:
3268 /// - storeId: 门店ID(可选,不传则查询所有门店) 3269 /// - storeId: 门店ID(可选,不传则查询所有门店)
  3270 + /// - showSolar: 是否显示阳历生日会员(默认 true)
  3271 + /// - showLunar: 是否显示农历生日会员(默认 true)
  3272 + /// - startDate: 查询开始日期 yyyy-MM-dd(可选,不传则从当天起)
  3273 + /// - endDate: 查询结束日期 yyyy-MM-dd(可选,不传则为 startDate 后 30 天)
3269 /// </remarks> 3274 /// </remarks>
3270 /// <param name="storeId">门店ID(可选)</param> 3275 /// <param name="storeId">门店ID(可选)</param>
  3276 + /// <param name="showSolar">是否显示阳历生日会员</param>
  3277 + /// <param name="showLunar">是否显示农历生日会员</param>
  3278 + /// <param name="startDate">查询开始日期</param>
  3279 + /// <param name="endDate">查询结束日期</param>
3271 /// <returns>会员生日信息列表,包含会员等级、生日日期、剩余权益等信息</returns> 3280 /// <returns>会员生日信息列表,包含会员等级、生日日期、剩余权益等信息</returns>
3272 /// <response code="200">成功返回会员生日列表</response> 3281 /// <response code="200">成功返回会员生日列表</response>
3273 /// <response code="500">服务器错误</response> 3282 /// <response code="500">服务器错误</response>
3274 [HttpGet("GetUpcomingBirthdays")] 3283 [HttpGet("GetUpcomingBirthdays")]
3275 - public async Task<dynamic> GetUpcomingBirthdays(string storeId = null) 3284 + public async Task<dynamic> GetUpcomingBirthdays(string storeId = null, bool showSolar = true, bool showLunar = true, string startDate = null, string endDate = null)
3276 { 3285 {
3277 try 3286 try
3278 { 3287 {
3279 var now = DateTime.Now; 3288 var now = DateTime.Now;
3280 - var endDate = now.AddDays(30); 3289 + DateTime queryStart;
  3290 + DateTime queryEnd;
  3291 +
  3292 + if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out var parsedStart))
  3293 + {
  3294 + queryStart = parsedStart.Date;
  3295 + if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out var parsedEnd))
  3296 + queryEnd = parsedEnd.Date;
  3297 + else
  3298 + queryEnd = queryStart.AddDays(30);
  3299 + }
  3300 + else
  3301 + {
  3302 + queryStart = now.Date;
  3303 + queryEnd = now.AddDays(30).Date;
  3304 + }
  3305 +
  3306 + if (queryStart > queryEnd)
  3307 + {
  3308 + var swap = queryStart;
  3309 + queryStart = queryEnd;
  3310 + queryEnd = swap;
  3311 + }
  3312 +
3281 var cal = new ChineseLunisolarCalendar(); 3313 var cal = new ChineseLunisolarCalendar();
3282 3314
  3315 + // 若两者都不勾选,直接返回空
  3316 + if (!showSolar && !showLunar)
  3317 + {
  3318 + return new { code = 200, msg = "获取成功", data = new List<LqKhxxBirthdayOutput>() };
  3319 + }
  3320 +
3283 // 构建查询:阳历或农历生日任一有值即可 3321 // 构建查询:阳历或农历生日任一有值即可
3284 var query = _db.Queryable<LqKhxxEntity>() 3322 var query = _db.Queryable<LqKhxxEntity>()
3285 .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) 3323 .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
@@ -3294,33 +3332,36 @@ WHERE kh.F_IsEffective = 1&quot;; @@ -3294,33 +3332,36 @@ WHERE kh.F_IsEffective = 1&quot;;
3294 // 获取所有会员数据 3332 // 获取所有会员数据
3295 var members = await query.ToListAsync(); 3333 var members = await query.ToListAsync();
3296 3334
3297 - // 在内存中筛选未来30天过生日的会员 3335 + // 在内存中筛选日期范围内过生日的会员
3298 var upcomingBirthdays = new List<LqKhxxBirthdayOutput>(); 3336 var upcomingBirthdays = new List<LqKhxxBirthdayOutput>();
3299 3337
3300 foreach (var member in members) 3338 foreach (var member in members)
3301 { 3339 {
  3340 + // 按用户勾选过滤:只显示勾选类型的会员
  3341 + if (member.BirthdayType == 0 && !showSolar) continue;
  3342 + if (member.BirthdayType == 1 && !showLunar) continue;
  3343 +
3302 DateTime? upcomingBirthday = null; 3344 DateTime? upcomingBirthday = null;
3303 string birthdayDateStr; 3345 string birthdayDateStr;
3304 int age; 3346 int age;
3305 DateTime? birthDateForAge = null; 3347 DateTime? birthDateForAge = null;
3306 3348
3307 - // 优先使用阳历生日  
3308 - if (member.Yanglsr.HasValue) 3349 + // 按会员生日类型决定使用阳历还是农历
  3350 + if (member.BirthdayType == 0 && member.Yanglsr.HasValue)
3309 { 3351 {
3310 var birthday = member.Yanglsr.Value; 3352 var birthday = member.Yanglsr.Value;
3311 birthDateForAge = birthday; 3353 birthDateForAge = birthday;
3312 var thisYearBirthday = new DateTime(now.Year, birthday.Month, birthday.Day); 3354 var thisYearBirthday = new DateTime(now.Year, birthday.Month, birthday.Day);
3313 var nextYearBirthday = new DateTime(now.Year + 1, birthday.Month, birthday.Day); 3355 var nextYearBirthday = new DateTime(now.Year + 1, birthday.Month, birthday.Day);
3314 3356
3315 - if (thisYearBirthday >= now.Date && thisYearBirthday <= endDate.Date) 3357 + if (thisYearBirthday >= queryStart && thisYearBirthday <= queryEnd)
3316 upcomingBirthday = thisYearBirthday; 3358 upcomingBirthday = thisYearBirthday;
3317 - else if (nextYearBirthday >= now.Date && nextYearBirthday <= endDate.Date) 3359 + else if (nextYearBirthday >= queryStart && nextYearBirthday <= queryEnd)
3318 upcomingBirthday = nextYearBirthday; 3360 upcomingBirthday = nextYearBirthday;
3319 3361
3320 birthdayDateStr = birthday.ToString("MM-dd"); 3362 birthdayDateStr = birthday.ToString("MM-dd");
3321 } 3363 }
3322 - // 无阳历有农历:将今年/明年农历生日转为公历后判断  
3323 - else if (!string.IsNullOrEmpty(member.Yinlsr)) 3364 + else if (member.BirthdayType == 1 && !string.IsNullOrEmpty(member.Yinlsr))
3324 { 3365 {
3325 int currentLunarYear = cal.GetYear(now); 3366 int currentLunarYear = cal.GetYear(now);
3326 int nextLunarYear = currentLunarYear + 1; 3367 int nextLunarYear = currentLunarYear + 1;
@@ -3333,9 +3374,9 @@ WHERE kh.F_IsEffective = 1&quot;; @@ -3333,9 +3374,9 @@ WHERE kh.F_IsEffective = 1&quot;;
3333 DateTime? thisYearSolar = LunarToSolar(cal, currentLunarYear, lunarMonth.Value, lunarDay.Value); 3374 DateTime? thisYearSolar = LunarToSolar(cal, currentLunarYear, lunarMonth.Value, lunarDay.Value);
3334 DateTime? nextYearSolar = LunarToSolar(cal, nextLunarYear, lunarMonth.Value, lunarDay.Value); 3375 DateTime? nextYearSolar = LunarToSolar(cal, nextLunarYear, lunarMonth.Value, lunarDay.Value);
3335 3376
3336 - if (thisYearSolar.HasValue && thisYearSolar.Value >= now.Date && thisYearSolar.Value <= endDate.Date) 3377 + if (thisYearSolar.HasValue && thisYearSolar.Value >= queryStart && thisYearSolar.Value <= queryEnd)
3337 upcomingBirthday = thisYearSolar; 3378 upcomingBirthday = thisYearSolar;
3338 - else if (nextYearSolar.HasValue && nextYearSolar.Value >= now.Date && nextYearSolar.Value <= endDate.Date) 3379 + else if (nextYearSolar.HasValue && nextYearSolar.Value >= queryStart && nextYearSolar.Value <= queryEnd)
3339 upcomingBirthday = nextYearSolar; 3380 upcomingBirthday = nextYearSolar;
3340 3381
3341 birthdayDateStr = member.Yinlsr; 3382 birthdayDateStr = member.Yinlsr;
@@ -3347,7 +3388,9 @@ WHERE kh.F_IsEffective = 1&quot;; @@ -3347,7 +3388,9 @@ WHERE kh.F_IsEffective = 1&quot;;
3347 3388
3348 if (!upcomingBirthday.HasValue) continue; 3389 if (!upcomingBirthday.HasValue) continue;
3349 3390
3350 - // 计算年龄(有阳历用阳历,否则用农历年近似) 3391 + // 计算年龄:有阳历生日用阳历,否则为 0(农历生日无阳历时无法精确计算年龄)
  3392 + if (!birthDateForAge.HasValue && member.Yanglsr.HasValue)
  3393 + birthDateForAge = member.Yanglsr.Value;
3351 age = birthDateForAge.HasValue ? now.Year - birthDateForAge.Value.Year : 0; 3394 age = birthDateForAge.HasValue ? now.Year - birthDateForAge.Value.Year : 0;
3352 if (age > 0 && birthDateForAge.HasValue && 3395 if (age > 0 && birthDateForAge.HasValue &&
3353 (now.Month < birthDateForAge.Value.Month || (now.Month == birthDateForAge.Value.Month && now.Day < birthDateForAge.Value.Day))) 3396 (now.Month < birthDateForAge.Value.Month || (now.Month == birthDateForAge.Value.Month && now.Day < birthDateForAge.Value.Day)))
@@ -3366,6 +3409,7 @@ WHERE kh.F_IsEffective = 1&quot;; @@ -3366,6 +3409,7 @@ WHERE kh.F_IsEffective = 1&quot;;
3366 yanglsr = member.Yanglsr, 3409 yanglsr = member.Yanglsr,
3367 yinlsr = member.Yinlsr, 3410 yinlsr = member.Yinlsr,
3368 birthdayType = member.BirthdayType, 3411 birthdayType = member.BirthdayType,
  3412 + birthdayTypeName = member.BirthdayType == 0 ? "阳历生日" : "农历生日",
3369 birthdayDate = birthdayDateStr, 3413 birthdayDate = birthdayDateStr,
3370 birthdayFullDate = upcomingBirthday.Value, 3414 birthdayFullDate = upcomingBirthday.Value,
3371 consumeLevel = member.ConsumeLevel, 3415 consumeLevel = member.ConsumeLevel,
@@ -3383,7 +3427,7 @@ WHERE kh.F_IsEffective = 1&quot;; @@ -3383,7 +3427,7 @@ WHERE kh.F_IsEffective = 1&quot;;
3383 { 3427 {
3384 var storeIds = upcomingBirthdays.Select(x => x.gsmd).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList(); 3428 var storeIds = upcomingBirthdays.Select(x => x.gsmd).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
3385 var stores = await _db.Queryable<LqMdxxEntity>().Where(s => storeIds.Contains(s.Id)).ToListAsync(); 3429 var stores = await _db.Queryable<LqMdxxEntity>().Where(s => storeIds.Contains(s.Id)).ToListAsync();
3386 - 3430 +
3387 foreach (var item in upcomingBirthdays) 3431 foreach (var item in upcomingBirthdays)
3388 { 3432 {
3389 if (!string.IsNullOrEmpty(item.gsmd)) 3433 if (!string.IsNullOrEmpty(item.gsmd))
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
@@ -1650,7 +1650,7 @@ namespace NCC.Extend.LqStatistics @@ -1650,7 +1650,7 @@ namespace NCC.Extend.LqStatistics
1650 var innerWhereClause = innerWhereConditions.Any() ? " AND " + string.Join(" AND ", innerWhereConditions) : ""; 1650 var innerWhereClause = innerWhereConditions.Any() ? " AND " + string.Join(" AND ", innerWhereConditions) : "";
1651 var outerWhereClause = outerWhereConditions.Any() ? "WHERE " + string.Join(" AND ", outerWhereConditions) : ""; 1651 var outerWhereClause = outerWhereConditions.Any() ? "WHERE " + string.Join(" AND ", outerWhereConditions) : "";
1652 1652
1653 - // 构建优化的主查询SQL - 合并查询减少扫描次数 1653 + // 构建优化的主查询SQL - 合并查询减少扫描次数,增加按品项分类的业绩统计
1654 var sql = $@" 1654 var sql = $@"
1655 SELECT 1655 SELECT
1656 order_stats.EmployeeId, 1656 order_stats.EmployeeId,
@@ -1671,7 +1671,13 @@ namespace NCC.Extend.LqStatistics @@ -1671,7 +1671,13 @@ namespace NCC.Extend.LqStatistics
1671 COALESCE(order_stats.TotalPerformance, 0) - COALESCE(coop_stats.CooperationPerformance, 0) AS BasePerformance, 1671 COALESCE(order_stats.TotalPerformance, 0) - COALESCE(coop_stats.CooperationPerformance, 0) AS BasePerformance,
1672 COALESCE(refund_stats.RefundPerformance, 0) AS RefundPerformance, 1672 COALESCE(refund_stats.RefundPerformance, 0) AS RefundPerformance,
1673 COALESCE(refund_stats.RefundCount, 0) AS RefundCount, 1673 COALESCE(refund_stats.RefundCount, 0) AS RefundCount,
1674 - order_stats.TotalPerformance 1674 + order_stats.TotalPerformance,
  1675 + COALESCE(cat_stats.LifeBeautyPerformance, 0) AS LifeBeautyPerformance,
  1676 + COALESCE(cat_stats.MedicalBeautyPerformance, 0) AS MedicalBeautyPerformance,
  1677 + COALESCE(cat_stats.TechBeautyPerformance, 0) AS TechBeautyPerformance,
  1678 + COALESCE(cat_stats.CooperationCategoryPerformance, 0) AS CooperationCategoryPerformance,
  1679 + COALESCE(cat_stats.OtherPerformance, 0) AS OtherPerformance,
  1680 + COALESCE(cat_stats.ProductPerformance, 0) AS ProductPerformance
1675 FROM ( 1681 FROM (
1676 SELECT 1682 SELECT
1677 order_base.jkszh AS EmployeeId, 1683 order_base.jkszh AS EmployeeId,
@@ -1763,6 +1769,31 @@ namespace NCC.Extend.LqStatistics @@ -1763,6 +1769,31 @@ namespace NCC.Extend.LqStatistics
1763 GROUP BY jksyj.jkszh 1769 GROUP BY jksyj.jkszh
1764 ) coop_stats ON order_stats.EmployeeId = coop_stats.EmployeeId 1770 ) coop_stats ON order_stats.EmployeeId = coop_stats.EmployeeId
1765 LEFT JOIN ( 1771 LEFT JOIN (
  1772 + -- 按品项分类统计业绩(生美、医美、科美、合作、其他、产品),与 order_base 使用相同 JOIN 确保分类之和=总业绩
  1773 + SELECT
  1774 + jksyj.jkszh AS EmployeeId,
  1775 + SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '生美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS LifeBeautyPerformance,
  1776 + SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '医美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS MedicalBeautyPerformance,
  1777 + SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '科美' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS TechBeautyPerformance,
  1778 + SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '合作' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS CooperationCategoryPerformance,
  1779 + SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '其他' OR jksyj.F_ItemCategory IS NULL OR jksyj.F_ItemCategory = '' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS OtherPerformance,
  1780 + SUM(CASE WHEN COALESCE(jksyj.F_ItemCategory, '') = '产品' THEN CAST(jksyj.jksyj AS DECIMAL(18,2)) ELSE 0 END) AS ProductPerformance
  1781 + FROM lq_kd_jksyj jksyj
  1782 + INNER JOIN lq_kd_pxmx pxmx ON jksyj.F_kdpxid = pxmx.F_Id AND pxmx.F_IsEffective = 1
  1783 + INNER JOIN lq_kd_kdjlb kd ON jksyj.glkdbh = CONVERT(kd.F_Id USING utf8mb4)
  1784 + WHERE jksyj.yjsj IS NOT NULL
  1785 + AND jksyj.jksyj IS NOT NULL
  1786 + AND jksyj.jksyj != ''
  1787 + AND jksyj.jksyj != '0'
  1788 + AND jksyj.F_kdpxid IS NOT NULL
  1789 + AND jksyj.F_kdpxid != ''
  1790 + AND jksyj.F_IsEffective = 1
  1791 + AND jksyj.yjsj >= @startDate
  1792 + AND jksyj.yjsj <= @endDate
  1793 + {innerWhereClause}
  1794 + GROUP BY jksyj.jkszh
  1795 + ) cat_stats ON order_stats.EmployeeId = cat_stats.EmployeeId
  1796 + LEFT JOIN (
1766 -- 退单业绩统计 1797 -- 退单业绩统计
1767 SELECT 1798 SELECT
1768 hytk_jksyj.jkszh AS EmployeeId, 1799 hytk_jksyj.jkszh AS EmployeeId,
@@ -1795,8 +1826,8 @@ namespace NCC.Extend.LqStatistics @@ -1795,8 +1826,8 @@ namespace NCC.Extend.LqStatistics
1795 var countSql = $"SELECT COUNT(*) FROM ({finalSql}) AS total_count"; 1826 var countSql = $"SELECT COUNT(*) FROM ({finalSql}) AS total_count";
1796 var totalCount = await _db.Ado.GetIntAsync(countSql, parameters); 1827 var totalCount = await _db.Ado.GetIntAsync(countSql, parameters);
1797 1828
1798 - // 分页查询  
1799 - var pageIndex = input.currentPage > 0 ? input.currentPage : 1; 1829 + // 分页查询(兼容前端传 pageIndex 或 currentPage)
  1830 + var pageIndex = input.pageIndex > 0 ? input.pageIndex : (input.currentPage > 0 ? input.currentPage : 1);
1800 var pageSize = input.pageSize > 0 ? input.pageSize : 20; 1831 var pageSize = input.pageSize > 0 ? input.pageSize : 20;
1801 var offset = (pageIndex - 1) * pageSize; 1832 var offset = (pageIndex - 1) * pageSize;
1802 var pagedSql = $"{finalSql} LIMIT {pageSize} OFFSET {offset}"; 1833 var pagedSql = $"{finalSql} LIMIT {pageSize} OFFSET {offset}";
@@ -1820,6 +1851,12 @@ namespace NCC.Extend.LqStatistics @@ -1820,6 +1851,12 @@ namespace NCC.Extend.LqStatistics
1820 TotalPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0), 1851 TotalPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0),
1821 BasePerformance = Convert.ToDecimal(stats.BasePerformance ?? 0), 1852 BasePerformance = Convert.ToDecimal(stats.BasePerformance ?? 0),
1822 CooperationPerformance = Convert.ToDecimal(stats.CooperationPerformance ?? 0), 1853 CooperationPerformance = Convert.ToDecimal(stats.CooperationPerformance ?? 0),
  1854 + LifeBeautyPerformance = Convert.ToDecimal(stats.LifeBeautyPerformance ?? 0),
  1855 + MedicalBeautyPerformance = Convert.ToDecimal(stats.MedicalBeautyPerformance ?? 0),
  1856 + TechBeautyPerformance = Convert.ToDecimal(stats.TechBeautyPerformance ?? 0),
  1857 + CooperationCategoryPerformance = Convert.ToDecimal(stats.CooperationCategoryPerformance ?? 0),
  1858 + OtherPerformance = Convert.ToDecimal(stats.OtherPerformance ?? 0),
  1859 + ProductPerformance = Convert.ToDecimal(stats.ProductPerformance ?? 0),
1823 RefundPerformance = Convert.ToDecimal(stats.RefundPerformance ?? 0), 1860 RefundPerformance = Convert.ToDecimal(stats.RefundPerformance ?? 0),
1824 RefundCount = Convert.ToInt32(stats.RefundCount ?? 0), 1861 RefundCount = Convert.ToInt32(stats.RefundCount ?? 0),
1825 ActualPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0), 1862 ActualPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0),
netcore/src/Modularity/Extend/NCC.Extend/MemberPortraitService.cs
@@ -9,6 +9,10 @@ using NCC.FriendlyException; @@ -9,6 +9,10 @@ using NCC.FriendlyException;
9 using NCC.Extend.Entitys.Dto.MemberPortrait; 9 using NCC.Extend.Entitys.Dto.MemberPortrait;
10 using NCC.Extend.Entitys.lq_kd_kdjlb; 10 using NCC.Extend.Entitys.lq_kd_kdjlb;
11 using NCC.Extend.Entitys.lq_kd_pxmx; 11 using NCC.Extend.Entitys.lq_kd_pxmx;
  12 +using NCC.Extend.Entitys.lq_kd_jksyj;
  13 +using NCC.Extend.Entitys.lq_kd_kjbsyj;
  14 +using NCC.Extend.Entitys.lq_xh_jksyj;
  15 +using NCC.Extend.Entitys.lq_xh_kjbsyj;
12 using NCC.Extend.Entitys.lq_khxx; 16 using NCC.Extend.Entitys.lq_khxx;
13 using NCC.Extend.Entitys.lq_xh_hyhk; 17 using NCC.Extend.Entitys.lq_xh_hyhk;
14 using NCC.Extend.Entitys.lq_xh_pxmx; 18 using NCC.Extend.Entitys.lq_xh_pxmx;
@@ -17,8 +21,14 @@ using NCC.Extend.Entitys.lq_hytk_mx; @@ -17,8 +21,14 @@ using NCC.Extend.Entitys.lq_hytk_mx;
17 using NCC.Extend.Entitys.lq_kd_deductinfo; 21 using NCC.Extend.Entitys.lq_kd_deductinfo;
18 using NCC.Extend.Entitys.lq_mdxx; 22 using NCC.Extend.Entitys.lq_mdxx;
19 using NCC.Extend.Entitys.lq_package_info; 23 using NCC.Extend.Entitys.lq_package_info;
  24 +using NCC.Extend.Entitys.lq_yyjl;
  25 +using NCC.Extend.Entitys.lq_yaoyjl;
  26 +using NCC.Extend.Entitys.lq_xh_feedback;
  27 +using NCC.Extend.Entitys.lq_order_records;
20 using NCC.Extend.Entitys.Enum; 28 using NCC.Extend.Entitys.Enum;
  29 +using NCC.Code;
21 using NCC.Dependency; 30 using NCC.Dependency;
  31 +using NCC.System.Entitys.Permission;
22 using SqlSugar; 32 using SqlSugar;
23 using SqlSugar.IOC; 33 using SqlSugar.IOC;
24 34
@@ -91,6 +101,55 @@ namespace NCC.Extend @@ -91,6 +101,55 @@ namespace NCC.Extend
91 .FirstAsync(); 101 .FirstAsync();
92 } 102 }
93 103
  104 + // 年龄:根据阳历生日计算,无阳历生日时为 null
  105 + int? age = null;
  106 + if (member.Yanglsr.HasValue)
  107 + {
  108 + var today = DateTime.Now;
  109 + var birth = member.Yanglsr.Value;
  110 + age = today.Year - birth.Year;
  111 + if (today.Month < birth.Month || (today.Month == birth.Month && today.Day < birth.Day))
  112 + {
  113 + age--;
  114 + }
  115 + }
  116 +
  117 + // 推荐人名称:Tjr 存客户ID,查 LqKhxxEntity.Khmc
  118 + string referrerName = null;
  119 + if (!string.IsNullOrEmpty(member.Tjr))
  120 + {
  121 + referrerName = await _db.Queryable<LqKhxxEntity>()
  122 + .Where(x => x.Id == member.Tjr)
  123 + .Select(x => x.Khmc)
  124 + .FirstAsync();
  125 + }
  126 +
  127 + // 拓客/健康师名称:从 UserEntity 查
  128 + var staffIds = new List<string>();
  129 + if (!string.IsNullOrEmpty(member.ExpandUser)) staffIds.Add(member.ExpandUser);
  130 + if (!string.IsNullOrEmpty(member.MainHealthUser)) staffIds.Add(member.MainHealthUser);
  131 + if (!string.IsNullOrEmpty(member.SubHealthUser)) staffIds.Add(member.SubHealthUser);
  132 + staffIds = staffIds.Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
  133 + var staffNameMap = new Dictionary<string, string>();
  134 + if (staffIds.Count > 0)
  135 + {
  136 + var staffs = await _db.Queryable<UserEntity>()
  137 + .Where(u => staffIds.Contains(u.Id))
  138 + .Select(u => new { u.Id, u.RealName })
  139 + .ToListAsync();
  140 + foreach (var s in staffs)
  141 + {
  142 + staffNameMap[s.Id] = s.RealName ?? "—";
  143 + }
  144 + }
  145 +
  146 + // 客户类型名称
  147 + string customerTypeName = null;
  148 + if (!string.IsNullOrEmpty(member.Khlx) && int.TryParse(member.Khlx, out var khlxVal))
  149 + {
  150 + customerTypeName = EnumHelper.GetEnumDesc<MemberTypeEnum>(khlxVal);
  151 + }
  152 +
94 // 构建会员类型列表 153 // 构建会员类型列表
95 var memberTypes = new List<MemberTypeInfo>(); 154 var memberTypes = new List<MemberTypeInfo>();
96 if (member.IsBeautyMember == StatusEnum.有效.GetHashCode()) 155 if (member.IsBeautyMember == StatusEnum.有效.GetHashCode())
@@ -141,7 +200,26 @@ namespace NCC.Extend @@ -141,7 +200,26 @@ namespace NCC.Extend
141 SleepStartTime = member.SleepStartTime, 200 SleepStartTime = member.SleepStartTime,
142 ConsumeLevel = member.ConsumeLevel, 201 ConsumeLevel = member.ConsumeLevel,
143 ConsumeLevelUpdateTime = member.ConsumeLevelUpdateTime, 202 ConsumeLevelUpdateTime = member.ConsumeLevelUpdateTime,
144 - MemberTypes = memberTypes 203 + MemberTypes = memberTypes,
  204 + Yanglsr = member.Yanglsr,
  205 + Yinlsr = member.Yinlsr,
  206 + BirthdayType = member.BirthdayType,
  207 + BirthdayTypeName = member.BirthdayType == 0 ? "阳历生日" : "农历生日",
  208 + Age = age,
  209 + Gender = member.Xb,
  210 + Address = member.Lxdz,
  211 + ReferrerId = member.Tjr,
  212 + ReferrerName = referrerName,
  213 + ExpandUserId = member.ExpandUser,
  214 + ExpandUserName = !string.IsNullOrEmpty(member.ExpandUser) && staffNameMap.TryGetValue(member.ExpandUser, out var eu) ? eu : null,
  215 + MainHealthUserId = member.MainHealthUser,
  216 + MainHealthUserName = !string.IsNullOrEmpty(member.MainHealthUser) && staffNameMap.TryGetValue(member.MainHealthUser, out var mhu) ? mhu : null,
  217 + SubHealthUserId = member.SubHealthUser,
  218 + SubHealthUserName = !string.IsNullOrEmpty(member.SubHealthUser) && staffNameMap.TryGetValue(member.SubHealthUser, out var shu) ? shu : null,
  219 + RegisterTime = member.Zcsj,
  220 + CustomerType = member.Khlx,
  221 + CustomerTypeName = customerTypeName,
  222 + Remark = member.Bz
145 }; 223 };
146 224
147 // 行为概要(使用个人累计字段 + 明细表兜底) 225 // 行为概要(使用个人累计字段 + 明细表兜底)
@@ -491,21 +569,76 @@ namespace NCC.Extend @@ -491,21 +569,76 @@ namespace NCC.Extend
491 569
492 try 570 try
493 { 571 {
494 - var query = _db.Queryable<LqKdKdjlbEntity>() 572 + var baseQuery = _db.Queryable<LqKdKdjlbEntity>()
495 .Where(x => x.Kdhy == memberId && x.IsEffective == StatusEnum.有效.GetHashCode()) 573 .Where(x => x.Kdhy == memberId && x.IsEffective == StatusEnum.有效.GetHashCode())
496 - .OrderBy(x => x.Kdrq, OrderByType.Desc)  
497 - .Select(x => new 574 + .OrderBy(x => x.Kdrq, OrderByType.Desc);
  575 +
  576 + var total = await baseQuery.CountAsync();
  577 + var billings = await baseQuery.Select(x => new
  578 + {
  579 + x.Id,
  580 + x.CreateUser,
  581 + BillingDate = x.Kdrq,
  582 + StoreName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(md => md.Id == x.Djmd).Select(md => md.Dm),
  583 + Amount = x.Sfyj,
  584 + DebtAmount = x.Qk,
  585 + ActivityName = SqlFunc.Subqueryable<LqPackageInfoEntity>().Where(pkg => pkg.Id == x.ActivityId).Select(pkg => pkg.ActivityName)
  586 + }).ToPageListAsync(pageIndex, pageSize);
  587 +
  588 + var billingIds = billings.Select(x => x.Id).ToList();
  589 + var pxmxList = await _db.Queryable<LqKdPxmxEntity>()
  590 + .Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode())
  591 + .Select(x => new { x.Glkdbh, x.Pxmc, x.ProjectNumber })
  592 + .ToListAsync();
  593 +
  594 + var jksyjList = await _db.Queryable<LqKdJksyjEntity>()
  595 + .Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode())
  596 + .Select(x => new { x.Glkdbh, x.Jksxm })
  597 + .ToListAsync();
  598 +
  599 + var kjbsyjList = await _db.Queryable<LqKdKjbsyjEntity>()
  600 + .Where(x => billingIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode())
  601 + .Select(x => new { x.Glkdbh, x.Kjblsxm })
  602 + .ToListAsync();
  603 +
  604 + var pxmxByBilling = pxmxList
  605 + .GroupBy(x => x.Glkdbh)
  606 + .ToDictionary(g => g.Key, g => string.Join("、", g.Select(p => string.IsNullOrEmpty(p.Pxmc) ? "—" : (p.ProjectNumber > 0 ? $"{p.Pxmc}×{p.ProjectNumber}" : p.Pxmc))));
  607 +
  608 + var healthCoachByBilling = jksyjList
  609 + .Where(x => !string.IsNullOrEmpty(x.Jksxm))
  610 + .GroupBy(x => x.Glkdbh)
  611 + .ToDictionary(g => g.Key, g => string.Join("、", g.Select(p => p.Jksxm).Distinct()));
  612 +
  613 + var techTeacherByBilling = kjbsyjList
  614 + .Where(x => !string.IsNullOrEmpty(x.Kjblsxm))
  615 + .GroupBy(x => x.Glkdbh)
  616 + .ToDictionary(g => g.Key, g => string.Join("、", g.Select(p => p.Kjblsxm).Distinct()));
  617 +
  618 + var createUserIds = billings.Where(x => !string.IsNullOrEmpty(x.CreateUser)).Select(x => x.CreateUser).Distinct().ToList();
  619 + var createUserNameMap = new Dictionary<string, string>();
  620 + if (createUserIds.Count > 0)
  621 + {
  622 + var users = await _db.Queryable<UserEntity>().Where(u => createUserIds.Contains(u.Id)).Select(u => new { u.Id, u.RealName }).ToListAsync();
  623 + foreach (var u in users)
498 { 624 {
499 - Id = x.Id,  
500 - BillingDate = x.Kdrq,  
501 - StoreName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(md => md.Id == x.Djmd).Select(md => md.Dm),  
502 - Amount = x.Sfyj,  
503 - DebtAmount = x.Qk,  
504 - ActivityName = SqlFunc.Subqueryable<LqPackageInfoEntity>().Where(pkg => pkg.Id == x.ActivityId).Select(pkg => pkg.ActivityName)  
505 - }); 625 + createUserNameMap[u.Id] = u.RealName ?? "—";
  626 + }
  627 + }
506 628
507 - var total = await query.CountAsync();  
508 - var result = await query.ToPageListAsync(pageIndex, pageSize); 629 + var result = billings.Select(x => new
  630 + {
  631 + x.Id,
  632 + x.BillingDate,
  633 + x.StoreName,
  634 + x.Amount,
  635 + x.DebtAmount,
  636 + x.ActivityName,
  637 + Items = pxmxByBilling.ContainsKey(x.Id) ? pxmxByBilling[x.Id] : "—",
  638 + CreatorName = !string.IsNullOrEmpty(x.CreateUser) && createUserNameMap.ContainsKey(x.CreateUser) ? createUserNameMap[x.CreateUser] : "—",
  639 + HealthCoachNames = healthCoachByBilling.ContainsKey(x.Id) ? healthCoachByBilling[x.Id] : "—",
  640 + TechTeacherNames = techTeacherByBilling.ContainsKey(x.Id) ? techTeacherByBilling[x.Id] : "—"
  641 + }).ToList();
509 642
510 return new 643 return new
511 { 644 {
@@ -537,20 +670,83 @@ namespace NCC.Extend @@ -537,20 +670,83 @@ namespace NCC.Extend
537 670
538 try 671 try
539 { 672 {
540 - var query = _db.Queryable<LqXhHyhkEntity>() 673 + var baseQuery = _db.Queryable<LqXhHyhkEntity>()
541 .Where(x => x.Hy == memberId && x.IsEffective == StatusEnum.有效.GetHashCode()) 674 .Where(x => x.Hy == memberId && x.IsEffective == StatusEnum.有效.GetHashCode())
542 - .OrderBy(x => x.Hksj, OrderByType.Desc)  
543 - .Select(x => new 675 + .OrderBy(x => x.Hksj, OrderByType.Desc);
  676 +
  677 + var total = await baseQuery.CountAsync();
  678 + var consumes = await baseQuery.Select(x => new
  679 + {
  680 + x.Id,
  681 + x.Czry,
  682 + ConsumeDate = x.Hksj,
  683 + StoreName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(md => md.Id == x.Md).Select(md => md.Dm),
  684 + Amount = x.Xfje,
  685 + LaborCost = x.Sgfy
  686 + }).ToPageListAsync(pageIndex, pageSize);
  687 +
  688 + var consumeIds = consumes.Select(x => x.Id).ToList();
  689 + var pxmxList = await _db.Queryable<LqXhPxmxEntity>()
  690 + .Where(x => consumeIds.Contains(x.ConsumeInfoId) && x.IsEffective == StatusEnum.有效.GetHashCode())
  691 + .Select(x => new { x.ConsumeInfoId, x.Pxmc, x.ProjectNumber })
  692 + .ToListAsync();
  693 +
  694 + var xhJksyjList = await _db.Queryable<LqXhJksyjEntity>()
  695 + .Where(x => consumeIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode())
  696 + .Select(x => new { x.Glkdbh, x.Jksxm })
  697 + .ToListAsync();
  698 +
  699 + var xhKjbsyjList = await _db.Queryable<LqXhKjbsyjEntity>()
  700 + .Where(x => consumeIds.Contains(x.Glkdbh) && x.IsEffective == StatusEnum.有效.GetHashCode())
  701 + .Select(x => new { x.Glkdbh, x.Kjblsxm })
  702 + .ToListAsync();
  703 +
  704 + // 查询有服务日志的耗卡ID(lq_xh_feedback.ConsumeId)
  705 + var consumeIdsWithFeedback = await _db.Queryable<LqXhFeedbackEntity>()
  706 + .Where(x => consumeIds.Contains(x.ConsumeId))
  707 + .Select(x => x.ConsumeId)
  708 + .Distinct()
  709 + .ToListAsync();
  710 + var hasFeedbackSet = consumeIdsWithFeedback.ToHashSet();
  711 +
  712 + var pxmxByConsume = pxmxList
  713 + .GroupBy(x => x.ConsumeInfoId)
  714 + .ToDictionary(g => g.Key, g => string.Join("、", g.Select(p => string.IsNullOrEmpty(p.Pxmc) ? "—" : (p.ProjectNumber > 0 ? $"{p.Pxmc}×{p.ProjectNumber}" : p.Pxmc))));
  715 +
  716 + var healthCoachByConsume = xhJksyjList
  717 + .Where(x => !string.IsNullOrEmpty(x.Jksxm))
  718 + .GroupBy(x => x.Glkdbh)
  719 + .ToDictionary(g => g.Key, g => string.Join("、", g.Select(p => p.Jksxm).Distinct()));
  720 +
  721 + var techTeacherByConsume = xhKjbsyjList
  722 + .Where(x => !string.IsNullOrEmpty(x.Kjblsxm))
  723 + .GroupBy(x => x.Glkdbh)
  724 + .ToDictionary(g => g.Key, g => string.Join("、", g.Select(p => p.Kjblsxm).Distinct()));
  725 +
  726 + var operatorIds = consumes.Where(x => !string.IsNullOrEmpty(x.Czry)).Select(x => x.Czry).Distinct().ToList();
  727 + var operatorNameMap = new Dictionary<string, string>();
  728 + if (operatorIds.Count > 0)
  729 + {
  730 + var users = await _db.Queryable<UserEntity>().Where(u => operatorIds.Contains(u.Id)).Select(u => new { u.Id, u.RealName }).ToListAsync();
  731 + foreach (var u in users)
544 { 732 {
545 - Id = x.Id,  
546 - ConsumeDate = x.Hksj,  
547 - StoreName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(md => md.Id == x.Md).Select(md => md.Dm),  
548 - Amount = x.Xfje,  
549 - LaborCost = x.Sgfy  
550 - }); 733 + operatorNameMap[u.Id] = u.RealName ?? "—";
  734 + }
  735 + }
551 736
552 - var total = await query.CountAsync();  
553 - var result = await query.ToPageListAsync(pageIndex, pageSize); 737 + var result = consumes.Select(x => new
  738 + {
  739 + x.Id,
  740 + x.ConsumeDate,
  741 + x.StoreName,
  742 + x.Amount,
  743 + x.LaborCost,
  744 + Items = pxmxByConsume.ContainsKey(x.Id) ? pxmxByConsume[x.Id] : "—",
  745 + OperatorName = !string.IsNullOrEmpty(x.Czry) && operatorNameMap.ContainsKey(x.Czry) ? operatorNameMap[x.Czry] : "—",
  746 + HealthCoachNames = healthCoachByConsume.ContainsKey(x.Id) ? healthCoachByConsume[x.Id] : "—",
  747 + TechTeacherNames = techTeacherByConsume.ContainsKey(x.Id) ? techTeacherByConsume[x.Id] : "—",
  748 + HasServiceLog = hasFeedbackSet.Contains(x.Id)
  749 + }).ToList();
554 750
555 return new 751 return new
556 { 752 {
@@ -610,6 +806,222 @@ namespace NCC.Extend @@ -610,6 +806,222 @@ namespace NCC.Extend
610 throw NCCException.Oh($"获取会员退卡列表失败: {ex.Message}"); 806 throw NCCException.Oh($"获取会员退卡列表失败: {ex.Message}");
611 } 807 }
612 } 808 }
  809 +
  810 + /// <summary>
  811 + /// 获取会员预约记录列表
  812 + /// </summary>
  813 + /// <remarks>
  814 + /// 根据会员ID查询预约记录(lq_yyjl.gk = memberId)
  815 + /// </remarks>
  816 + /// <param name="memberId">会员ID</param>
  817 + /// <param name="pageIndex">页码</param>
  818 + /// <param name="pageSize">每页数量</param>
  819 + /// <returns>预约记录列表</returns>
  820 + [HttpGet("appointment-list")]
  821 + public async Task<dynamic> GetAppointmentList(string memberId, int pageIndex = 1, int pageSize = 10)
  822 + {
  823 + if (string.IsNullOrEmpty(memberId))
  824 + {
  825 + throw NCCException.Oh("memberId 参数不能为空");
  826 + }
  827 +
  828 + try
  829 + {
  830 + var query = _db.Queryable<LqYyjlEntity>()
  831 + .Where(x => x.Gk == memberId)
  832 + .OrderBy(x => x.Yysj, OrderByType.Desc)
  833 + .Select(x => new
  834 + {
  835 + Id = x.Id,
  836 + AppointmentDate = x.Yysj,
  837 + EndDate = x.Yyjs,
  838 + StoreName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(md => md.Id == x.Djmd).Select(md => md.Dm),
  839 + InviterName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == x.Yyr).Select(u => u.RealName),
  840 + HealthCoachName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == x.Yyjks).Select(u => u.RealName),
  841 + ExperienceItem = x.Yytyxm,
  842 + Status = x.F_Status,
  843 + NoDealRemark = x.NoDealRemark
  844 + });
  845 +
  846 + var total = await query.CountAsync();
  847 + var list = await query.ToPageListAsync(pageIndex, pageSize);
  848 +
  849 + return new { Total = total, List = list };
  850 + }
  851 + catch (Exception ex)
  852 + {
  853 + _logger.LogError(ex, $"获取会员预约记录失败, memberId={memberId}");
  854 + throw NCCException.Oh($"获取会员预约记录失败: {ex.Message}");
  855 + }
  856 + }
  857 +
  858 + /// <summary>
  859 + /// 获取会员邀约记录列表
  860 + /// </summary>
  861 + /// <remarks>
  862 + /// 根据会员ID查询邀约记录(lq_yaoyjl.yykh = memberId)
  863 + /// </remarks>
  864 + /// <param name="memberId">会员ID</param>
  865 + /// <param name="pageIndex">页码</param>
  866 + /// <param name="pageSize">每页数量</param>
  867 + /// <returns>邀约记录列表</returns>
  868 + [HttpGet("invite-list")]
  869 + public async Task<dynamic> GetInviteList(string memberId, int pageIndex = 1, int pageSize = 10)
  870 + {
  871 + if (string.IsNullOrEmpty(memberId))
  872 + {
  873 + throw NCCException.Oh("memberId 参数不能为空");
  874 + }
  875 +
  876 + try
  877 + {
  878 + var query = _db.Queryable<LqYaoyjlEntity>()
  879 + .Where(x => x.Yykh == memberId)
  880 + .OrderBy(x => x.Yysj, OrderByType.Desc)
  881 + .Select(x => new
  882 + {
  883 + Id = x.Id,
  884 + InviteDate = x.Yysj,
  885 + StoreName = SqlFunc.Subqueryable<LqMdxxEntity>().Where(md => md.Id == x.StoreId).Select(md => md.Dm),
  886 + InviterName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == x.Yyr).Select(u => u.RealName),
  887 + ContactTime = x.Lxsj,
  888 + ContactRecord = x.Lxjl,
  889 + PhoneValid = x.Dhsfyx
  890 + });
  891 +
  892 + var total = await query.CountAsync();
  893 + var list = await query.ToPageListAsync(pageIndex, pageSize);
  894 +
  895 + return new { Total = total, List = list };
  896 + }
  897 + catch (Exception ex)
  898 + {
  899 + _logger.LogError(ex, $"获取会员邀约记录失败, memberId={memberId}");
  900 + throw NCCException.Oh($"获取会员邀约记录失败: {ex.Message}");
  901 + }
  902 + }
  903 +
  904 + /// <summary>
  905 + /// 获取会员服务日志列表
  906 + /// </summary>
  907 + /// <remarks>
  908 + /// 根据会员ID查询耗卡服务日志(lq_xh_feedback.F_MemberId = memberId)
  909 + /// </remarks>
  910 + /// <param name="memberId">会员ID</param>
  911 + /// <param name="pageIndex">页码</param>
  912 + /// <param name="pageSize">每页数量</param>
  913 + /// <returns>服务日志列表</returns>
  914 + [HttpGet("service-log-list")]
  915 + public async Task<dynamic> GetServiceLogList(string memberId, int pageIndex = 1, int pageSize = 10)
  916 + {
  917 + if (string.IsNullOrEmpty(memberId))
  918 + {
  919 + throw NCCException.Oh("memberId 参数不能为空");
  920 + }
  921 +
  922 + try
  923 + {
  924 + // 只查询关联耗卡为有效(IsEffective=1)的服务日志,作废耗卡的服务日志不展示
  925 + var query = _db.Queryable<LqXhFeedbackEntity, LqXhHyhkEntity>((f, h) => new JoinQueryInfos(
  926 + JoinType.Inner, f.ConsumeId == h.Id && h.IsEffective == StatusEnum.有效.GetHashCode()))
  927 + .Where((f, h) => f.MemberId == memberId)
  928 + .OrderBy((f, h) => f.CreateTime, OrderByType.Desc)
  929 + .Select((f, h) => new
  930 + {
  931 + f.Id,
  932 + CreateTime = f.CreateTime,
  933 + CreatorName = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == f.CreateUser).Select(u => u.RealName),
  934 + f.Remark,
  935 + f.KjbRemark,
  936 + f.BeforeImage,
  937 + f.AfterImage
  938 + });
  939 +
  940 + var total = await query.CountAsync();
  941 + var list = await query.ToPageListAsync(pageIndex, pageSize);
  942 +
  943 + return new { Total = total, List = list };
  944 + }
  945 + catch (Exception ex)
  946 + {
  947 + _logger.LogError(ex, $"获取会员服务日志失败, memberId={memberId}");
  948 + throw NCCException.Oh($"获取会员服务日志失败: {ex.Message}");
  949 + }
  950 + }
  951 +
  952 + /// <summary>
  953 + /// 获取会员旧日志列表(历史开单记录,来自 lq_order_records)
  954 + /// </summary>
  955 + /// <remarks>
  956 + /// 根据会员编号和手机号直接传入、精确匹配查询,非模糊检索。
  957 + /// 会员编号、手机号至少传一个。
  958 + ///
  959 + /// 示例请求:
  960 + /// ```http
  961 + /// GET /api/Extend/MemberPortrait/old-log-list?memberCode=GK2025101000046&amp;mobile=18615786320&amp;pageIndex=1&amp;pageSize=10
  962 + /// ```
  963 + ///
  964 + /// 参数说明:
  965 + /// - memberCode: 会员编号(lq_order_records.member_no)
  966 + /// - mobile: 手机号(lq_order_records.member_phone)
  967 + /// - pageIndex: 页码
  968 + /// - pageSize: 每页数量
  969 + /// </remarks>
  970 + /// <param name="memberCode">会员编号</param>
  971 + /// <param name="mobile">手机号</param>
  972 + /// <param name="pageIndex">页码</param>
  973 + /// <param name="pageSize">每页数量</param>
  974 + /// <returns>旧日志(历史开单记录)列表</returns>
  975 + [HttpGet("old-log-list")]
  976 + public async Task<dynamic> GetOldLogList(string memberCode, string mobile, int pageIndex = 1, int pageSize = 10)
  977 + {
  978 + if (string.IsNullOrEmpty(memberCode) && string.IsNullOrEmpty(mobile))
  979 + {
  980 + throw NCCException.Oh("会员编号和手机号至少传一个");
  981 + }
  982 +
  983 + try
  984 + {
  985 + var query = _db.Queryable<LqOrderRecordsEntity>();
  986 + if (!string.IsNullOrEmpty(memberCode) && !string.IsNullOrEmpty(mobile))
  987 + {
  988 + // 两个都传时用 OR:旧系统会员编号可能与新系统不一致,任一匹配即可
  989 + query = query.Where(x => x.MemberNo == memberCode || x.MemberPhone == mobile);
  990 + }
  991 + else if (!string.IsNullOrEmpty(memberCode))
  992 + {
  993 + query = query.Where(x => x.MemberNo == memberCode);
  994 + }
  995 + else
  996 + {
  997 + query = query.Where(x => x.MemberPhone == mobile);
  998 + }
  999 +
  1000 + var total = await query.CountAsync();
  1001 + var list = await query
  1002 + .OrderBy(x => x.CreatedAt, OrderByType.Desc)
  1003 + .Select(x => new
  1004 + {
  1005 + x.Id,
  1006 + x.OrderNo,
  1007 + x.ImageName,
  1008 + x.MemberNo,
  1009 + x.MemberPhone,
  1010 + x.MemberName,
  1011 + x.Remarks,
  1012 + CreateTime = x.CreatedAt,
  1013 + x.UpdatedAt
  1014 + })
  1015 + .ToPageListAsync(pageIndex, pageSize);
  1016 +
  1017 + return new { Total = total, List = list };
  1018 + }
  1019 + catch (Exception ex)
  1020 + {
  1021 + _logger.LogError(ex, $"获取会员旧日志失败, memberCode={memberCode}, mobile={mobile}");
  1022 + throw NCCException.Oh($"获取会员旧日志失败: {ex.Message}");
  1023 + }
  1024 + }
613 } 1025 }
614 } 1026 }
615 1027