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 2  
3 3 VUE_CLI_BABEL_TRANSPILE_MODULES = true
4 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 7 # VUE_APP_BASE_API = 'http://localhost:2011'
8 8 VUE_APP_IMG_API = ''
9 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 2 <el-dialog :visible.sync="visibleSync" title="会员画像" :width="'1500px'" append-to-body top="8vh"
3 3 custom-class="member-portrait-dialog" :close-on-click-modal="false" @closed="handleClosed">
4 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 9 <div class="avatar-circle">
9 10 <i class="el-icon-user-solid"></i>
10 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 19 </div>
  20 + <el-button type="primary" size="small" icon="el-icon-edit" class="btn-edit" @click="handleEdit">
  21 + 修改资料
  22 + </el-button>
37 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 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 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 56 </div>
71 57 </div>
72 58 </div>
73 59  
74 60 <!-- 选项卡内容 -->
75 61 <el-tabs v-model="activeTab" class="portrait-tabs">
76   - <!-- 概览 -->
  62 + <!-- 概览:消费行为、趋势、分析(个人档案已在顶部面板展示) -->
77 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 68 <i class="el-icon-shopping-cart-full"></i>
83   - <span class="card-title">消费行为</span>
  69 + <span>消费行为</span>
84 70 </div>
85   - <div class="card-body">
  71 + <div class="section-content">
86 72 <div class="behavior-grid">
87 73 <div class="behavior-item">
88 74 <div class="behavior-icon">
... ... @@ -169,24 +155,24 @@
169 155 </div>
170 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 161 <i class="el-icon-data-line"></i>
176   - <span class="card-title">近12个月消费趋势</span>
  162 + <span>近12个月消费趋势</span>
177 163 </div>
178   - <div class="card-body">
  164 + <div class="section-content">
179 165 <div ref="trendChart" class="trend-chart"></div>
180 166 </div>
181 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 172 <i class="el-icon-data-analysis"></i>
187   - <span class="card-title">消费分析</span>
  173 + <span>消费分析</span>
188 174 </div>
189   - <div class="card-body">
  175 + <div class="section-content">
190 176 <div class="analysis-layout">
191 177 <div class="analysis-item">
192 178 <div class="analysis-icon">
... ... @@ -227,132 +213,334 @@
227 213  
228 214 <!-- 权益明细 -->
229 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 221 <template slot-scope="scope">¥{{ formatMoney(scope.row.UnitPrice) }}</template>
242 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 229 <template slot-scope="scope">¥{{ formatMoney(scope.row.RemainingValue) }}</template>
250 230 </el-table-column>
251 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 255 </div>
254 256 </div>
255 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 286 <el-table-column prop="BillingDate" label="开单日期" width="160">
268 287 <template slot-scope="scope">{{ formatDateTime(scope.row.BillingDate) }}</template>
269 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 295 <template slot-scope="scope">¥{{ formatMoney(scope.row.Amount) }}</template>
273 296 </el-table-column>
274   - <el-table-column prop="DebtAmount" label="欠款金额" width="120">
  297 + <el-table-column prop="DebtAmount" label="欠款金额" width="90">
275 298 <template slot-scope="scope">¥{{ formatMoney(scope.row.DebtAmount) }}</template>
276 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 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 307 </div>
287 308 </div>
288 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 316 <template slot-scope="scope">{{ formatDateTime(scope.row.ConsumeDate) }}</template>
302 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 324 <template slot-scope="scope">¥{{ formatMoney(scope.row.Amount) }}</template>
306 325 </el-table-column>
307   - <el-table-column prop="LaborCost" label="手工费" >
  326 + <el-table-column prop="LaborCost" label="手工费" width="90">
308 327 <template slot-scope="scope">¥{{ formatMoney(scope.row.LaborCost) }}</template>
309 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 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 406 </div>
319 407 </div>
320 408 </el-tab-pane>
321 409  
322 410 <!-- 退卡列表 -->
323 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 414 <el-table-column prop="RefundDate" label="退卡日期" width="160">
333 415 <template slot-scope="scope">{{ formatDateTime(scope.row.RefundDate) }}</template>
334 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 419 <template slot-scope="scope">¥{{ formatMoney(scope.row.RefundAmount) }}</template>
338 420 </el-table-column>
339   - <el-table-column prop="ActualRefundAmount" label="实际退款" >
  421 + <el-table-column prop="ActualRefundAmount" label="实际退款" width="100">
340 422 <template slot-scope="scope">¥{{ formatMoney(scope.row.ActualRefundAmount) }}</template>
341 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 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 431 </div>
352 432 </div>
353 433 </el-tab-pane>
354 434 </el-tabs>
355 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 544 </el-dialog>
357 545 </template>
358 546  
... ... @@ -400,7 +588,33 @@ export default {
400 588 pageIndex: 1,
401 589 pageSize: 10,
402 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 620 watch: {
... ... @@ -427,6 +641,14 @@ export default {
427 641 this.fetchConsumeList()
428 642 } else if (newVal === 'refund' && this.refundList.length === 0) {
429 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 666 this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 }
445 667 this.refundList = []
446 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 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 701 async fetchData() {
450 702 if (!this.memberId) {
451 703 this.$message.warning('会员ID不能为空')
... ... @@ -459,13 +711,21 @@ export default {
459 711 this.consumePagination = { pageIndex: 1, pageSize: 10, total: 0 }
460 712 this.refundList = []
461 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 723 this.loading = true
464 724 try {
465 725 const res = await request({
466 726 url: '/api/Extend/MemberPortrait/overview',
467 727 method: 'GET',
468   - params: { memberId: this.memberId }
  728 + data: { memberId: this.memberId }
469 729 })
470 730  
471 731 if (res.code === 200 && res.data) {
... ... @@ -496,7 +756,7 @@ export default {
496 756 const res = await request({
497 757 url: '/api/Extend/MemberPortrait/billing-list',
498 758 method: 'GET',
499   - params: {
  759 + data: {
500 760 memberId: this.memberId,
501 761 pageIndex: this.billingPagination.pageIndex,
502 762 pageSize: this.billingPagination.pageSize
... ... @@ -524,7 +784,7 @@ export default {
524 784 const res = await request({
525 785 url: '/api/Extend/MemberPortrait/consume-list',
526 786 method: 'GET',
527   - params: {
  787 + data: {
528 788 memberId: this.memberId,
529 789 pageIndex: this.consumePagination.pageIndex,
530 790 pageSize: this.consumePagination.pageSize
... ... @@ -552,7 +812,7 @@ export default {
552 812 const res = await request({
553 813 url: '/api/Extend/MemberPortrait/refund-list',
554 814 method: 'GET',
555   - params: {
  815 + data: {
556 816 memberId: this.memberId,
557 817 pageIndex: this.refundPagination.pageIndex,
558 818 pageSize: this.refundPagination.pageSize
... ... @@ -599,6 +859,229 @@ export default {
599 859 this.refundPagination.pageIndex = 1
600 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 1085 renderTrendChart() {
603 1086 if (!this.$refs.trendChart) return
604 1087  
... ... @@ -677,10 +1160,17 @@ export default {
677 1160 if (amount === null || amount === undefined || amount === '') return '0.00'
678 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 1170 formatDateTime(timestamp) {
681 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 1174 return date.toLocaleString('zh-CN', {
685 1175 year: 'numeric',
686 1176 month: '2-digit',
... ... @@ -693,8 +1183,8 @@ export default {
693 1183 },
694 1184 formatDate(timestamp) {
695 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 1188 return date.toLocaleDateString('zh-CN', {
699 1189 year: 'numeric',
700 1190 month: '2-digit',
... ... @@ -703,15 +1193,27 @@ export default {
703 1193 }
704 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 1207 getConsumeLevelText(level) {
707 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 1218 getConsumeLevelTagType(level) {
717 1219 const typeMap = {
... ... @@ -719,7 +1221,8 @@ export default {
719 1221 1: '',
720 1222 2: 'warning',
721 1223 3: 'success',
722   - 4: 'danger'
  1224 + 4: 'success',
  1225 + 5: 'danger'
723 1226 }
724 1227 return typeMap[level] || 'info'
725 1228 },
... ... @@ -805,227 +1308,259 @@ export default {
805 1308 padding: 20px;
806 1309 background: #f5f7fa;
807 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 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 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 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 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 1348 .avatar-circle {
835   - width: 72px;
836   - height: 72px;
837   - border-radius: 8px;
  1349 + width: 64px;
  1350 + height: 64px;
  1351 + border-radius: 12px;
838 1352 background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
839 1353 display: flex;
840 1354 align-items: center;
841 1355 justify-content: center;
842 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 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 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 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 1476 display: flex;
976   - align-items: center;
977   - gap: 12px;
978 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 1482 align-items: center;
985 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 1531 display: flex;
996 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 1538 display: flex;
1001 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 1572  
1038 1573 // 选项卡样式
1039 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 1597 ::v-deep .el-tabs__header {
1041 1598 margin-bottom: 16px;
1042 1599 background: #ffffff;
... ... @@ -1075,8 +1632,23 @@ export default {
1075 1632 }
1076 1633  
1077 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 1652 .content-card {
1081 1653 background: #ffffff;
1082 1654 border-radius: 8px;
... ... @@ -1339,3 +1911,166 @@ export default {
1339 1911 }
1340 1912 }
1341 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 217 <div class="card-content">
218 218 <div class="data-grid">
219 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 222 </div>
225 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 226 </div>
234 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 230 </div>
240 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 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 242 </div>
255 243 </div>
256 244  
... ... @@ -364,6 +352,16 @@
364 352 </el-tag>
365 353 </template>
366 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 366 <el-table-column align="center" v-if="colId === 'khlx' && tableColumnsVisible[colId] !== false" key="khlx" label="客户类型"
369 367 :min-width="tableColumnsMeta.khlx.width" :sortable="tableColumnsMeta.khlx.sortable ? 'custom' : false"
... ... @@ -517,7 +515,8 @@
517 515 <MemberRightsDialog v-if="memberRightsDialogVisible" ref="MemberRightsDialog" />
518 516 <DetailDialog v-if="detailDialogVisible" ref="DetailDialog" />
519 517 <member-portrait-dialog :visible.sync="memberPortraitDialog.visible"
520   - :member-id="memberPortraitDialog.memberId" />
  518 + :member-id="memberPortraitDialog.memberId"
  519 + @edit="handlePortraitEdit" />
521 520 </div>
522 521 </template>
523 522 <script>
... ... @@ -588,13 +587,14 @@ export default {
588 587 sidx: "id",
589 588 },
590 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 591 tableColumnsVisible: {},
593 592 tableColumnsMeta: {
594 593 khmc: { label: '客户名称', width: '250px', sortable: true, sidx: 'khmc' },
595 594 sjh: { label: '手机号', width: '150px', sortable: true, sidx: 'sjh' },
596 595 gsmd: { label: '归属门店', width: '160px', sortable: true, sidx: 'gsmdName' },
597 596 xb: { label: '性别', width: '85px', sortable: true, sidx: 'xb' },
  597 + birthday: { label: '会员生日', width: '130px', sortable: false },
598 598 khlx: { label: '客户类型', width: '100px', sortable: true, sidx: 'khlx' },
599 599 mainHealthUser: { label: '健康师', width: '85px', sortable: false },
600 600 subHealthUser: { label: '负责顾问', width: '95px', sortable: false },
... ... @@ -1065,6 +1065,16 @@ export default {
1065 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 1078 formatDate(date) {
1069 1079 if (!date) return '无'
1070 1080 const d = new Date(date)
... ... @@ -1115,6 +1125,13 @@ export default {
1115 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 1136 showDetail(id) {
1120 1137 this.detailDialogVisible = true
... ... @@ -1178,7 +1195,14 @@ export default {
1178 1195 const saved = localStorage.getItem('lqKhxx_columnConfig')
1179 1196 if (saved) {
1180 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 1206 if (visible && typeof visible === 'object') this.tableColumnsVisible = { ...this.tableColumnsVisible, ...visible }
1183 1207 }
1184 1208 } catch (e) { console.warn('loadColumnConfig error:', e) }
... ... @@ -2011,34 +2035,16 @@ export default {
2011 2035 }
2012 2036  
2013 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 2042 .header-top {
2037 2043 display: flex;
2038 2044 justify-content: space-between;
2039 2045 align-items: center;
2040   - margin-bottom: 6px; // 缩小间距
2041   - gap: 4px;
  2046 + gap: 8px;
  2047 + flex-wrap: nowrap;
2042 2048  
2043 2049 .info-left {
2044 2050 display: flex;
... ... @@ -2047,32 +2053,23 @@ export default {
2047 2053 min-width: 0;
2048 2054  
2049 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 2059 margin-right: 6px;
2054 2060 white-space: nowrap;
2055 2061 overflow: hidden;
2056 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 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 2077 .header-tags {
2081 2078 display: flex;
2082 2079 flex-wrap: wrap;
2083   - gap: 6px;
2084   - min-height: 24px;
  2080 + gap: 4px;
  2081 + margin-top: 8px;
2085 2082  
2086 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 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 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 2102 .card-content {
2122 2103 flex: 1;
2123   - padding: 10px 12px; // 缩小内容区内边距
  2104 + padding: 12px;
2124 2105 display: flex;
2125 2106 flex-direction: column;
2126   - gap: 8px; // 缩小间距
  2107 + gap: 10px;
  2108 + min-width: 0;
2127 2109  
2128 2110 .data-grid {
2129 2111 display: grid;
2130 2112 grid-template-columns: 1fr 1fr;
2131   - gap: 6px 8px; // 缩小网格间距
  2113 + gap: 8px 12px;
2132 2114  
2133 2115 .data-item {
2134 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 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 2129 display: flex;
2162 2130 align-items: center;
2163   - gap: 5px; // 合理的图标间距
  2131 + gap: 4px;
2164 2132  
2165 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 2142 }
2182 2143  
2183 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 2151 overflow: hidden;
2189 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 2161 &.text-danger {
2198 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 2168  
2216 2169 .amount-section {
2217 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 2175 display: flex;
2229 2176 justify-content: space-between;
2230 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 2180 .amount-box {
2254 2181 flex: 1;
2255 2182 display: flex;
2256 2183 flex-direction: column;
2257 2184 align-items: center;
2258   - gap: 3px; // 缩小间距
  2185 + gap: 4px;
2259 2186  
2260 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 2191 display: flex;
2268 2192 align-items: center;
2269 2193 justify-content: center;
2270   - gap: 5px; // 合理的图标间距
  2194 + gap: 4px;
2271 2195  
2272 2196 .icon-inline {
2273   - font-size: 13px; // 增大图标
  2197 + font-size: 12px;
2274 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 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 2207 &.primary-color {
2293 2208 color: #2563EB;
... ... @@ -2309,40 +2224,21 @@ export default {
2309 2224  
2310 2225 .divider {
2311 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 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 2237 display: flex;
2324   - justify-content: space-between;
  2238 + justify-content: flex-start;
2325 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 2243 .action-btn {
2348 2244 padding: 6px 12px; // 合理的内边距
... ...
antis-ncc-admin/src/views/lqKhxxBirthday/index.vue
... ... @@ -14,6 +14,10 @@
14 14 </el-select>
15 15 </div>
16 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 21 <el-button type="primary" icon="el-icon-refresh" @click="loadBirthdayData">刷新</el-button>
18 22 </div>
19 23  
... ... @@ -58,7 +62,7 @@
58 62 <div class="day-number">{{ data.day.split('-')[2] }}</div>
59 63 <div class="birthday-list">
60 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 66 <span class="birthday-member" :class="`level-${member.consumeLevel}`"
63 67 @click.stop="showMemberDetail(member)">
64 68 <i class="el-icon-user"></i>{{ member.khmc }}
... ... @@ -111,10 +115,18 @@
111 115 <span class="value">{{ selectedMember.gsmdName || '无' }}</span>
112 116 </div>
113 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 123 <span class="value">{{ formatBirthday(selectedMember.yanglsr) }}</span>
116 124 </div>
117 125 <div class="info-row">
  126 + <label>农历生日</label>
  127 + <span class="value">{{ selectedMember.yinlsr || '无' }}</span>
  128 + </div>
  129 + <div class="info-row">
118 130 <label>年龄</label>
119 131 <span class="value">{{ selectedMember.age }}岁</span>
120 132 </div>
... ... @@ -150,6 +162,8 @@ export default {
150 162 savedCalendarDate: null, // 保存日历的日期状态
151 163 selectedStore: '',
152 164 storeList: [],
  165 + showSolar: true,
  166 + showLunar: true,
153 167 birthdayData: [],
154 168 birthdayMap: {}, // 日期到会员的映射
155 169 detailDialogVisible: false,
... ... @@ -158,6 +172,21 @@ export default {
158 172 }
159 173 },
160 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 190 detailDialogVisible(newVal, oldVal) {
162 191 if (newVal === true) {
163 192 // 弹窗打开时,保存当前日历日期
... ... @@ -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 245 async loadBirthdayData() {
202 246 this.loading = true
203 247 try {
  248 + const { startDate, endDate } = this.getMonthRange()
204 249 const res = await request({
205 250 url: '/api/Extend/LqKhxx/GetUpcomingBirthdays',
206 251 method: 'get',
207 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 53 {{ formatMoney(scope.row.TotalPerformance) }}
54 54 </template>
55 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 86 <el-table-column prop="FirstOrderPerformance" label="首单业绩" width="100" align="right">
57 87 <template slot-scope="scope">
58 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 58 public int birthdayType { get; set; }
59 59  
60 60 /// <summary>
  61 + /// 生日类型名称(阳历生日/农历生日)
  62 + /// </summary>
  63 + public string birthdayTypeName { get; set; }
  64 +
  65 + /// <summary>
61 66 /// 生日日期(用于日历显示,格式:MM-DD)
62 67 /// </summary>
63 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 68 public decimal CooperationPerformance { get; set; }
69 69  
70 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 102 /// </summary>
73 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 42 /// 岗位
43 43 /// </summary>
44 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 97 /// 会员类型列表(生美、医美、科技部、教育部)
98 98 /// </summary>
99 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 197 /// <summary>
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
... ... @@ -144,7 +144,7 @@ namespace NCC.Extend.LqKhxx
144 144 List<string> queryZcsj = input.zcsj != null ? input.zcsj.Split(',').ToObeject<List<string>>() : null;
145 145 DateTime? startZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.First()) : null;
146 146 DateTime? endZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.Last()) : null;
147   -
  147 +
148 148 // 处理沉睡天数范围
149 149 List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null;
150 150 int? minSleepDays = null;
... ... @@ -168,7 +168,7 @@ namespace NCC.Extend.LqKhxx
168 168 minRemainingRights = min;
169 169 maxRemainingRights = max;
170 170 }
171   -
  171 +
172 172 var data = await _db.Queryable<LqKhxxEntity>()
173 173 .WhereIF(!string.IsNullOrEmpty(input.keyWord), p => p.Khmc.Contains(input.keyWord) || p.Sjh.Contains(input.keyWord) || p.Dah.Contains(input.keyWord))
174 174 .WhereIF(input.IsTechMemberbh.HasValue, p => p.IsTechMember == input.IsTechMemberbh)
... ... @@ -329,7 +329,7 @@ namespace NCC.Extend.LqKhxx
329 329 minRemainingRights = min;
330 330 maxRemainingRights = max;
331 331 }
332   -
  332 +
333 333 var data = await _db.Queryable<LqKhxxEntity>()
334 334 .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
335 335 .WhereIF(!string.IsNullOrEmpty(input.khmc), p => p.Khmc.Contains(input.khmc))
... ... @@ -711,7 +711,7 @@ namespace NCC.Extend.LqKhxx
711 711 List<string> queryYanglsr = input.yanglsr != null ? input.yanglsr.Split(',').ToObeject<List<string>>() : null;
712 712 DateTime? startYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.First()) : null;
713 713 DateTime? endYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.Last()) : null;
714   -
  714 +
715 715 // 处理沉睡天数范围
716 716 List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null;
717 717 int? minSleepDays = null;
... ... @@ -976,8 +976,8 @@ namespace NCC.Extend.LqKhxx
976 976 // 根据消费等级编号获取消费等级名称
977 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 981 : "D";
982 982 }
983 983  
... ... @@ -1133,8 +1133,8 @@ namespace NCC.Extend.LqKhxx
1133 1133 public async Task Update(string id, [FromBody] LqKhxxUpInput input)
1134 1134 {
1135 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 1139 entity.Yinlsr = SolarToLunarString(input.yanglsr.Value);
1140 1140 }
... ... @@ -2222,7 +2222,7 @@ namespace NCC.Extend.LqKhxx
2222 2222 // 查询会员的所有耗卡记录
2223 2223 var allConsumedItemsFromConsume = await _db.Queryable<LqXhPxmxEntity, LqXhHyhkEntity>(
2224 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 2226 && pxmx.IsEffective == StatusEnum.有效.GetHashCode()
2227 2227 && hyhk.IsEffective == StatusEnum.有效.GetHashCode())
2228 2228 .Where((pxmx, hyhk) => !string.IsNullOrEmpty(pxmx.BillingItemId))
... ... @@ -3250,36 +3250,74 @@ WHERE kh.F_IsEffective = 1&quot;;
3250 3250 #region 会员生日管理
3251 3251  
3252 3252 /// <summary>
3253   - /// 获取门店未来30天过生日的会员列表
  3253 + /// 获取门店指定日期范围内过生日的会员列表
3254 3254 /// </summary>
3255 3255 /// <remarks>
3256   - /// 根据门店ID获取未来30天内过生日的会员信息,支持阳历生日和农历生日
3257   - /// - 阳历生日:直接按公历月日计算今年/明年生日
3258   - /// - 农历生日:将今年/明年农历月日转为公历日期后判断是否在未来30天内(便于按公历日期查询提醒)
  3256 + /// 根据门店ID、日期范围、阳历/农历筛选条件获取过生日的会员信息
  3257 + /// - 按会员的生日类型(F_BirthdayType)决定使用阳历还是农历计算生日日期
  3258 + /// - 阳历生日:使用 yanglsr 按公历月日计算今年/明年生日
  3259 + /// - 农历生日:使用 yinlsr 将今年/明年农历月日转为公历后判断
3259 3260 ///
3260 3261 /// 会员等级说明:0=D,1=C,2=B,3=A,4=A+,5=A++
3261 3262 ///
3262 3263 /// 示例请求:
3263 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 3269 /// - storeId: 门店ID(可选,不传则查询所有门店)
  3270 + /// - showSolar: 是否显示阳历生日会员(默认 true)
  3271 + /// - showLunar: 是否显示农历生日会员(默认 true)
  3272 + /// - startDate: 查询开始日期 yyyy-MM-dd(可选,不传则从当天起)
  3273 + /// - endDate: 查询结束日期 yyyy-MM-dd(可选,不传则为 startDate 后 30 天)
3269 3274 /// </remarks>
3270 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 3280 /// <returns>会员生日信息列表,包含会员等级、生日日期、剩余权益等信息</returns>
3272 3281 /// <response code="200">成功返回会员生日列表</response>
3273 3282 /// <response code="500">服务器错误</response>
3274 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 3286 try
3278 3287 {
3279 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 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 3322 var query = _db.Queryable<LqKhxxEntity>()
3285 3323 .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
... ... @@ -3294,33 +3332,36 @@ WHERE kh.F_IsEffective = 1&quot;;
3294 3332 // 获取所有会员数据
3295 3333 var members = await query.ToListAsync();
3296 3334  
3297   - // 在内存中筛选未来30天过生日的会员
  3335 + // 在内存中筛选日期范围内过生日的会员
3298 3336 var upcomingBirthdays = new List<LqKhxxBirthdayOutput>();
3299 3337  
3300 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 3344 DateTime? upcomingBirthday = null;
3303 3345 string birthdayDateStr;
3304 3346 int age;
3305 3347 DateTime? birthDateForAge = null;
3306 3348  
3307   - // 优先使用阳历生日
3308   - if (member.Yanglsr.HasValue)
  3349 + // 按会员生日类型决定使用阳历还是农历
  3350 + if (member.BirthdayType == 0 && member.Yanglsr.HasValue)
3309 3351 {
3310 3352 var birthday = member.Yanglsr.Value;
3311 3353 birthDateForAge = birthday;
3312 3354 var thisYearBirthday = new DateTime(now.Year, birthday.Month, birthday.Day);
3313 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 3358 upcomingBirthday = thisYearBirthday;
3317   - else if (nextYearBirthday >= now.Date && nextYearBirthday <= endDate.Date)
  3359 + else if (nextYearBirthday >= queryStart && nextYearBirthday <= queryEnd)
3318 3360 upcomingBirthday = nextYearBirthday;
3319 3361  
3320 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 3366 int currentLunarYear = cal.GetYear(now);
3326 3367 int nextLunarYear = currentLunarYear + 1;
... ... @@ -3333,9 +3374,9 @@ WHERE kh.F_IsEffective = 1&quot;;
3333 3374 DateTime? thisYearSolar = LunarToSolar(cal, currentLunarYear, lunarMonth.Value, lunarDay.Value);
3334 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 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 3380 upcomingBirthday = nextYearSolar;
3340 3381  
3341 3382 birthdayDateStr = member.Yinlsr;
... ... @@ -3347,7 +3388,9 @@ WHERE kh.F_IsEffective = 1&quot;;
3347 3388  
3348 3389 if (!upcomingBirthday.HasValue) continue;
3349 3390  
3350   - // 计算年龄(有阳历用阳历,否则用农历年近似)
  3391 + // 计算年龄:有阳历生日用阳历,否则为 0(农历生日无阳历时无法精确计算年龄)
  3392 + if (!birthDateForAge.HasValue && member.Yanglsr.HasValue)
  3393 + birthDateForAge = member.Yanglsr.Value;
3351 3394 age = birthDateForAge.HasValue ? now.Year - birthDateForAge.Value.Year : 0;
3352 3395 if (age > 0 && birthDateForAge.HasValue &&
3353 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 3409 yanglsr = member.Yanglsr,
3367 3410 yinlsr = member.Yinlsr,
3368 3411 birthdayType = member.BirthdayType,
  3412 + birthdayTypeName = member.BirthdayType == 0 ? "阳历生日" : "农历生日",
3369 3413 birthdayDate = birthdayDateStr,
3370 3414 birthdayFullDate = upcomingBirthday.Value,
3371 3415 consumeLevel = member.ConsumeLevel,
... ... @@ -3383,7 +3427,7 @@ WHERE kh.F_IsEffective = 1&quot;;
3383 3427 {
3384 3428 var storeIds = upcomingBirthdays.Select(x => x.gsmd).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
3385 3429 var stores = await _db.Queryable<LqMdxxEntity>().Where(s => storeIds.Contains(s.Id)).ToListAsync();
3386   -
  3430 +
3387 3431 foreach (var item in upcomingBirthdays)
3388 3432 {
3389 3433 if (!string.IsNullOrEmpty(item.gsmd))
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStatisticsService.cs
... ... @@ -1650,7 +1650,7 @@ namespace NCC.Extend.LqStatistics
1650 1650 var innerWhereClause = innerWhereConditions.Any() ? " AND " + string.Join(" AND ", innerWhereConditions) : "";
1651 1651 var outerWhereClause = outerWhereConditions.Any() ? "WHERE " + string.Join(" AND ", outerWhereConditions) : "";
1652 1652  
1653   - // 构建优化的主查询SQL - 合并查询减少扫描次数
  1653 + // 构建优化的主查询SQL - 合并查询减少扫描次数,增加按品项分类的业绩统计
1654 1654 var sql = $@"
1655 1655 SELECT
1656 1656 order_stats.EmployeeId,
... ... @@ -1671,7 +1671,13 @@ namespace NCC.Extend.LqStatistics
1671 1671 COALESCE(order_stats.TotalPerformance, 0) - COALESCE(coop_stats.CooperationPerformance, 0) AS BasePerformance,
1672 1672 COALESCE(refund_stats.RefundPerformance, 0) AS RefundPerformance,
1673 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 1681 FROM (
1676 1682 SELECT
1677 1683 order_base.jkszh AS EmployeeId,
... ... @@ -1763,6 +1769,31 @@ namespace NCC.Extend.LqStatistics
1763 1769 GROUP BY jksyj.jkszh
1764 1770 ) coop_stats ON order_stats.EmployeeId = coop_stats.EmployeeId
1765 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 1798 SELECT
1768 1799 hytk_jksyj.jkszh AS EmployeeId,
... ... @@ -1795,8 +1826,8 @@ namespace NCC.Extend.LqStatistics
1795 1826 var countSql = $"SELECT COUNT(*) FROM ({finalSql}) AS total_count";
1796 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 1831 var pageSize = input.pageSize > 0 ? input.pageSize : 20;
1801 1832 var offset = (pageIndex - 1) * pageSize;
1802 1833 var pagedSql = $"{finalSql} LIMIT {pageSize} OFFSET {offset}";
... ... @@ -1820,6 +1851,12 @@ namespace NCC.Extend.LqStatistics
1820 1851 TotalPerformance = Convert.ToDecimal(stats.TotalPerformance ?? 0) - Convert.ToDecimal(stats.RefundPerformance ?? 0),
1821 1852 BasePerformance = Convert.ToDecimal(stats.BasePerformance ?? 0),
1822 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 1860 RefundPerformance = Convert.ToDecimal(stats.RefundPerformance ?? 0),
1824 1861 RefundCount = Convert.ToInt32(stats.RefundCount ?? 0),
1825 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 9 using NCC.Extend.Entitys.Dto.MemberPortrait;
10 10 using NCC.Extend.Entitys.lq_kd_kdjlb;
11 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 16 using NCC.Extend.Entitys.lq_khxx;
13 17 using NCC.Extend.Entitys.lq_xh_hyhk;
14 18 using NCC.Extend.Entitys.lq_xh_pxmx;
... ... @@ -17,8 +21,14 @@ using NCC.Extend.Entitys.lq_hytk_mx;
17 21 using NCC.Extend.Entitys.lq_kd_deductinfo;
18 22 using NCC.Extend.Entitys.lq_mdxx;
19 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 28 using NCC.Extend.Entitys.Enum;
  29 +using NCC.Code;
21 30 using NCC.Dependency;
  31 +using NCC.System.Entitys.Permission;
22 32 using SqlSugar;
23 33 using SqlSugar.IOC;
24 34  
... ... @@ -91,6 +101,55 @@ namespace NCC.Extend
91 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 154 var memberTypes = new List<MemberTypeInfo>();
96 155 if (member.IsBeautyMember == StatusEnum.有效.GetHashCode())
... ... @@ -141,7 +200,26 @@ namespace NCC.Extend
141 200 SleepStartTime = member.SleepStartTime,
142 201 ConsumeLevel = member.ConsumeLevel,
143 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 569  
492 570 try
493 571 {
494   - var query = _db.Queryable<LqKdKdjlbEntity>()
  572 + var baseQuery = _db.Queryable<LqKdKdjlbEntity>()
495 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 643 return new
511 644 {
... ... @@ -537,20 +670,83 @@ namespace NCC.Extend
537 670  
538 671 try
539 672 {
540   - var query = _db.Queryable<LqXhHyhkEntity>()
  673 + var baseQuery = _db.Queryable<LqXhHyhkEntity>()
541 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 751 return new
556 752 {
... ... @@ -610,6 +806,222 @@ namespace NCC.Extend
610 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  
... ...