Commit 747ab6fbb33d3a362f15d5f37d08e3d40b98dc31
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.
Showing
12 changed files
with
1956 additions
and
618 deletions
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"; |
| 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&showSolar=true&showLunar=true&startDate=2025-02-01&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"; |
| 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"; |
| 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"; |
| 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"; |
| 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"; |
| 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&mobile=18615786320&pageIndex=1&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 | ... | ... |