logo

会员管家

会员收银管理系统

忘记密码?

注册即赠 7 天免费试用,30 天未续费将锁定

测试账号: store1 / Store1@2026

其他角色入口
👤
员工端
业绩查询 · 提成账单
📊
结算管理端
工资单 · 财务结算
🔍
会员查询端
会员自助 · 余额积分
我的店铺
欢迎回来
营业中 店员 💳 续费
0
会员总数
0
今日新增
¥0
本月充值
¥0
本月消费
经营分析
客单价
¥0
复购率
0%
活跃会员
0
本月新增
0
添加会员
充值
消费
购卡
扫码结账
待处理事项
0张卡券即将到期
0位会员余额不足
0位待跟进新会员
0位会员本月生日
0个今日预约
0款产品库存紧张

卡券管理

管理门店卡券与套餐

在售卡券
0
活跃持卡
0
本月售卡
0
全部
计次卡
计时卡
套餐卡

数据报表

门店经营数据分析

今日
本周
本月
本年
充值总额
¥0
消费总额
¥0
售卡总额
¥0
营收趋势
消费排行TOP5
售卡排行TOP5
新增会员
0
活跃会员
0
提成报表
本月
上月
全部
提成总额
¥0
笔数
0
已结算
¥0
未结算
¥0
服务
¥0
0笔
充值
¥0
0笔
购卡
¥0
0笔
产品
¥0
0笔
支持自定义周期 / 底薪 / 调整项 / Excel 导出
旧版「一键结算」已淘汰,请使用结算后台
明细查询
点击查看明细 · 支持筛选与导出
※ 默认时间跟随上方筛选(今日/本周/本月/本年),可在弹窗内调整
※ 提成明细默认按上方提成报表时间(本月/上月/全部),可在弹窗内调整

会员管理

会员档案 · 充值 · 消费

全部(0)
本月新增(0)
即将到期(0)

会员详情

余额
积分
调整
优惠券
消费
我的卡券 购买卡券 +
消费记录
充值记录
操作日志
↓ 最新优先

会员充值

充值金额
¥
充值1000元 赠送100元
付款方式
现金
微信
支付宝
备注

消费收银

已选服务
暂未添加服务
添加服务
已选产品
暂未添加产品
添加产品
支付方式
卡券扣次
会员余额
微信
支付宝
现金
折扣优惠
积分抵扣
备注
合计 ¥0

购买卡券

付款方式
会员余额
微信
支付宝
现金
选择服务项目
选择外卖产品

系统设置 v2.6.0

门店配置 · 版本与账号

基础设置
服务项目设置
外卖产品设置
卡券设置
计时计次卡设置
会员卡类型设置
员工管理
员工管理
系统
修改密码
店铺信息
通知设置
短信通知开关 计费
打印机管理
数据备份与恢复
交接班
标签管理
📅
预约管理
升级记录
提成设置
积分规则
关于我们
短信余额
一键清理测试数据 · 转入正式

服务项目管理

外卖产品管理

计时计次卡管理

员工管理

会员卡类型

卡券名称
计次卡 ¥0
有效期0个月 包含内容
0
已售张数
0
在用张数
启用
状态
持卡会员 共0人
暂无持卡会员
`); printWindow.document.close(); printWindow.print(); }, confirmShift() { if (!confirm('确认交接?交接后将记录本次交接班数据。')) return; // 保存交接记录到localStorage const summary = DataManager.getShiftSummary(); let shiftHistory = JSON.parse(localStorage.getItem('salon_shift_history') || '[]'); const _shiftItem = { id: Date.now(), ...summary, confirmedAt: new Date().toISOString() }; shiftHistory.push(_shiftItem); localStorage.setItem('salon_shift_history', JSON.stringify(shiftHistory)); // [SEC-AUDIT-D-SHIFT-SYNC] 双写后端 shift_records try { if (typeof ApiManager !== 'undefined' && ApiManager.post) { ApiManager.post('/shift-summary', { shiftData: _shiftItem }).catch(e => { console.warn('[SEC-AUDIT-D-SHIFT-SYNC] POST 失败(本地已写)', e && e.message); }); } } catch(e) { console.warn('[SEC-AUDIT-D-SHIFT-SYNC] 异常', e); } alert('交接确认成功!'); this.closeShiftModal(); }, // ==================== SEC-SHIFT-HISTORY-20260701 交接历史 ==================== openShiftHistory() { document.getElementById('shiftHistoryModal').classList.add('active'); document.body.style.overflow = 'hidden'; const box = document.getElementById('shiftHistoryContent'); if (box) box.innerHTML = '
加载中...
'; const self = this; // 优先后端拉取 const _fallbackToLocal = () => { const list = JSON.parse(localStorage.getItem('salon_shift_history') || '[]'); self.renderShiftHistoryList(list, 'local'); }; try { if (typeof ApiManager !== 'undefined' && ApiManager.get) { ApiManager.get('/shift-records', { limit: 200 }).then(resp => { const rows = (resp && resp.data) ? resp.data : (Array.isArray(resp) ? resp : []); // 后端返回结构:{id, store_id, user_id, shift_data, created_at} // shift_data 是 JSON(或已解析对象) const parsed = (rows || []).map(r => { let sd = r.shift_data; if (typeof sd === 'string') { try { sd = JSON.parse(sd); } catch(e) { sd = {}; } } return Object.assign({}, sd || {}, { id: r.id, confirmedAt: r.created_at || (sd && sd.confirmedAt) || '', operator: (sd && sd.operator) || r.user_id || '' }); }); // 按 id 倒序(后端已 DESC,兜底再排一次) parsed.sort((a,b) => (Number(b.id)||0) - (Number(a.id)||0)); self.renderShiftHistoryList(parsed, 'server'); }).catch(err => { console.warn('[SEC-SHIFT-HISTORY] 后端获取失败,降级本地', err && err.message); _fallbackToLocal(); }); } else { _fallbackToLocal(); } } catch(e) { console.warn('[SEC-SHIFT-HISTORY] 异常,降级本地', e); _fallbackToLocal(); } }, renderShiftHistoryList(list, source) { const box = document.getElementById('shiftHistoryContent'); if (!box) return; if (!list || list.length === 0) { box.innerHTML = '
暂无历史交接记录
'; return; } this._shiftHistoryCache = list; const fmt2 = (v) => (Number(v)||0).toFixed(2); const fmtTime = (t) => { if (!t) return '-'; try { const d = new Date(t); if (isNaN(d.getTime())) return String(t); const pad = (n) => String(n).padStart(2, '0'); return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); } catch(e) { return String(t); } }; const tag = source === 'server' ? '●在线' : '●本地'; const html = list.map(item => { const total = fmt2(item.totalRevenue != null ? item.totalRevenue : ((Number(item.rechargeTotal)||0)+(Number(item.consumeTotal)||0)+(Number(item.cardTotal)||0))); return '
' + '
' + '
' + fmtTime(item.confirmedAt) + '
' + '
¥' + total + '
' + '
' + '
' + '充值 ¥' + fmt2(item.rechargeTotal) + ' · 消费 ¥' + fmt2(item.consumeTotal) + ' · 购卡 ¥' + fmt2(item.cardTotal) + '
' + '
'; }).join(''); box.innerHTML = '
' + tag + ' 共 ' + list.length + ' 条
' + html; }, viewShiftHistoryDetail(id) { const list = this._shiftHistoryCache || []; const item = list.find(x => String(x.id) === String(id)); if (!item) { alert('记录不存在'); return; } const fmt2 = (v) => (Number(v)||0).toFixed(2); const fmtTime = (t) => { if (!t) return '-'; try { const d = new Date(t); if (isNaN(d.getTime())) return String(t); const pad = (n) => String(n).padStart(2, '0'); return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); } catch(e) { return String(t); } }; const lines = [ '交接时间:' + fmtTime(item.confirmedAt), '', '━━━ 营收汇总 ━━━', '充值:¥' + fmt2(item.rechargeTotal) + ' (' + (item.rechargeCount||0) + '笔)', '消费:¥' + fmt2(item.consumeTotal) + ' (' + (item.consumeCount||0) + '笔)', ' ├ 服务 ¥' + fmt2(item.consumeServiceTotal), ' └ 产品 ¥' + fmt2(item.consumeProductTotal), '购卡:¥' + fmt2(item.cardTotal) + ' (' + (item.cardCount||0) + '笔)', '实际收入:¥' + fmt2(item.totalRevenue), '员工提成合计:¥' + fmt2(item.commissionTotal), ]; alert(lines.join('\n')); }, closeShiftHistoryModal() { document.getElementById('shiftHistoryModal').classList.remove('active'); document.body.style.overflow = ''; }, // ==================== 标签管理 ==================== openTagManageModal() { this.renderTagManageList(); this.renderTagColorPicker(); document.getElementById('tagManageModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, tagSelectedColor: '#9B1B5E', tagPresetColors: ['#9B1B5E','#D4A843','#e74c3c','#27ae60','#3498db','#9b59b6','#1abc9c','#e67e22','#34495e','#f39c12'], renderTagColorPicker() { const container = document.getElementById('tagColorPicker'); if (!container) return; const self = this; container.innerHTML = this.tagPresetColors.map(c => '
' ).join(''); }, selectTagColor(color) { this.tagSelectedColor = color; this.renderTagColorPicker(); }, closeTagManageModal() { document.getElementById('tagManageModal').classList.remove('active'); document.body.style.overflow = ''; }, renderTagManageList() { const tags = DataManager.getMemberTags(); const listEl = document.getElementById('tagManageList'); if (!listEl) return; if (tags.length === 0) { listEl.innerHTML = '
暂无标签
'; return; } listEl.innerHTML = tags.map(tag => '
' + '
' + '' + '' + tag.name + '' + '
' + '' + '
' ).join(''); }, addTag() { const nameInput = document.getElementById('newTagName'); if (!nameInput) return; const name = nameInput.value.trim(); const color = this.tagSelectedColor || '#9B1B5E'; if (!name) { alert('请输入标签名称'); return; } DataManager.addMemberTag({ name, color }); nameInput.value = ''; this.tagSelectedColor = '#9B1B5E'; this.renderTagColorPicker(); this.renderTagManageList(); }, deleteTag(tagId) { if (!confirm('确定删除该标签?将从所有会员中移除')) return; DataManager.deleteMemberTag(tagId); this.renderTagManageList(); }, // ==================== 预约管理 ==================== appointmentFilter: 'all', // 预约管理 - 会员搜索选择 searchApptMembers(keyword) { keyword = keyword.trim().toLowerCase(); const resultsEl = document.getElementById('apptMemberResults'); const members = DataManager.getMembers(); let filtered = members; if (keyword) { filtered = members.filter(m => m.name.toLowerCase().includes(keyword) || (m.phone && m.phone.includes(keyword)) ); } if (filtered.length === 0) { resultsEl.innerHTML = '
未找到匹配会员
'; resultsEl.style.display = 'block'; return; } resultsEl.innerHTML = filtered.slice(0, 20).map(m => '
' + '
' + '' + m.name + '' + '' + (m.phone || '') + '' + '
' + '' + (m.cardType || '普通') + '' + '
' ).join(''); resultsEl.style.display = 'block'; }, selectApptMember(id) { const members = DataManager.getMembers(); const member = members.find(m => String(m.id) === String(id)); if (!member) return; document.getElementById('apptMember').value = member.id; document.getElementById('apptMemberSelectedText').textContent = member.name + ' (' + (member.phone || '') + ')'; document.getElementById('apptMemberSelected').style.display = 'block'; document.getElementById('apptMemberSearchArea').style.display = 'none'; document.getElementById('apptMemberResults').style.display = 'none'; document.getElementById('apptMemberSearch').value = ''; }, clearApptMember() { document.getElementById('apptMember').value = ''; document.getElementById('apptMemberSelected').style.display = 'none'; document.getElementById('apptMemberSearchArea').style.display = 'block'; document.getElementById('apptMemberResults').style.display = 'none'; document.getElementById('apptMemberSearch').value = ''; }, // 预约管理 - 服务项目搜索选择 searchApptServices(keyword) { keyword = keyword.trim().toLowerCase(); const resultsEl = document.getElementById('apptServiceResults'); const services = DataManager.getServiceItems(); let filtered = services; if (keyword) { filtered = services.filter(s => s.name.toLowerCase().includes(keyword) ); } if (filtered.length === 0) { resultsEl.innerHTML = '
未找到匹配服务项目
'; resultsEl.style.display = 'block'; return; } resultsEl.innerHTML = filtered.map(s => '
' + '
' + '' + s.name + '' + '
' + '¥' + (s.price || 0) + '' + '
' ).join(''); resultsEl.style.display = 'block'; }, selectApptService(id) { const services = DataManager.getServiceItems(); const service = services.find(s => String(s.id) === String(id)); if (!service) return; document.getElementById('apptService').value = service.id; document.getElementById('apptServiceSelectedText').textContent = service.name + ' (¥' + (service.price || 0) + ')'; document.getElementById('apptServiceSelected').style.display = 'block'; document.getElementById('apptServiceSearchArea').style.display = 'none'; document.getElementById('apptServiceResults').style.display = 'none'; document.getElementById('apptServiceSearch').value = ''; }, clearApptService() { document.getElementById('apptService').value = ''; document.getElementById('apptServiceSelected').style.display = 'none'; document.getElementById('apptServiceSearchArea').style.display = 'block'; document.getElementById('apptServiceResults').style.display = 'none'; document.getElementById('apptServiceSearch').value = ''; }, openAppointmentModal() { // 初始化会员搜索选择器 this.clearApptMember(); this.clearApptService(); // 填充员工下拉 const staffSelect = document.getElementById('apptStaff'); const employees = DataManager.getEmployeeItems().filter(e => e.status === 'active'); staffSelect.innerHTML = '' + employees.map(e => '').join(''); // 默认日期为今天 const today = new Date(); document.getElementById('apptDate').value = today.toISOString().split('T')[0]; this.appointmentFilter = 'all'; this.renderAppointmentList(); document.getElementById('appointmentModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeAppointmentModal() { document.getElementById('appointmentModal').classList.remove('active'); document.body.style.overflow = ''; }, addAppointment() { const memberId = document.getElementById('apptMember').value; const serviceId = document.getElementById('apptService').value; const staffId = document.getElementById('apptStaff').value; const date = document.getElementById('apptDate').value; const time = document.getElementById('apptTime').value; const remark = document.getElementById('apptRemark').value.trim(); if (!memberId || !serviceId || !date || !time) { alert('请填写会员、服务项目、日期和时间'); return; } const members = DataManager.getMembers(); const member = members.find(m => String(m.id) === String(memberId)); const services = DataManager.getServiceItems(); const service = services.find(s => String(s.id) === String(serviceId)); const employees = DataManager.getEmployeeItems(); const staff = staffId ? employees.find(e => String(e.id) === String(staffId)) : null; DataManager.addAppointment({ memberId, memberName: member ? member.name : '', memberPhone: member ? member.phone : '', serviceId, serviceName: service ? service.name : '', servicePrice: service ? service.price : 0, staffId: staffId || '', staffName: staff ? staff.name : '', date, time, remark }); // 清空表单 this.clearApptMember(); this.clearApptService(); document.getElementById('apptStaff').value = ''; document.getElementById('apptTime').value = ''; document.getElementById('apptRemark').value = ''; this.renderAppointmentList(); // 刷新仪表盘预约数 if (typeof DashboardModule !== 'undefined') DashboardModule.refreshTodos(); }, renderAppointmentList() { const list = DataManager.getAppointments(); const now = new Date(); const todayStr = now.toISOString().split('T')[0]; let filtered = list; if (this.appointmentFilter === 'today') { filtered = list.filter(a => a.date === todayStr && a.status !== 'cancelled'); } else if (this.appointmentFilter === 'upcoming') { filtered = list.filter(a => a.date > todayStr && a.status === 'pending'); } else if (this.appointmentFilter === 'done') { filtered = list.filter(a => a.status === 'done'); } // 按日期+时间排序 filtered.sort((a, b) => (a.date + a.time).localeCompare(b.date + b.time)); const listEl = document.getElementById('appointmentList'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
暂无预约记录
'; return; } const statusMap = { 'pending': { text: '待服务', color: '#D4A843', bg: '#fff8e8' }, 'done': { text: '已完成', color: '#27ae60', bg: '#eaf7ee' }, 'cancelled': { text: '已取消', color: '#999', bg: '#f5f5f5' } }; listEl.innerHTML = filtered.map(a => { const st = statusMap[a.status] || statusMap['pending']; return '
' + '
' + '
' + (a.memberName || '') + '
' + '' + st.text + '' + '
' + '
📅 ' + a.date + ' ' + a.time + '
' + '
💇 ' + (a.serviceName || '') + (a.servicePrice ? ' (¥' + a.servicePrice + ')' : '') + '
' + (a.staffName ? '
👤 ' + a.staffName + '
' : '') + (a.remark ? '
📝 ' + a.remark + '
' : '') + '
' + (a.status === 'pending' ? '' : '') + (a.status === 'pending' ? '' : '') + '' + '
' + '
'; }).join(''); }, filterAppointments(filter) { this.appointmentFilter = filter; // 更新按钮样式 ['All', 'Today', 'Upcoming', 'Done'].forEach(f => { const btn = document.getElementById('apptFilter' + f); if (btn) { btn.style.background = (f.toLowerCase() === filter) ? '#9B1B5E' : 'white'; btn.style.color = (f.toLowerCase() === filter) ? 'white' : '#333'; } }); this.renderAppointmentList(); }, _apptSelectedPayment: null, _completingApptId: null, _completingAppt: null, _completingMember: null, completeAppointment(id) { const appointments = DataManager.getAppointments(); const appt = appointments.find(a => a.id === id); if (!appt) return; // 检查会员是否仍存在 const members = DataManager.getMembers(); const member = members.find(m => String(m.id) === String(appt.memberId)); if (!member) { alert('该会员已不存在,无法完成服务'); return; } // 保存当前预约信息,供确认消费时使用 this._completingApptId = id; this._completingAppt = appt; this._completingMember = member; this._apptSelectedPayment = null; // 显示消费确认弹窗 this.showApptConsumeConfirm(appt, member); }, showApptConsumeConfirm(appt, member) { const servicePrice = appt.servicePrice || 0; // 获取会员折扣 const cardTypes = DataManager.getMemberCardItems() || []; const cardType = cardTypes.find(ct => String(ct.id) === String(member.cardTypeId)); const discount = cardType && cardType.serviceDiscount ? cardType.serviceDiscount : 10; const discountedPrice = discount < 10 ? Math.round(servicePrice * discount) / 10 : servicePrice; // 检查是否有匹配的卡券可扣次 const heldCards = DataManager.getAllMemberHeldCards().filter(c => String(c.memberId) === String(member.id) && c.status === 'active' ); const matchingCards = heldCards.filter(c => { if (c.cardType === 'count') { return c.remainingCount > 0; } else if (c.cardType === 'time') { return c.remainingHours > 0; } else if (c.cardType === 'package') { return (c.packageItems || []).some(pi => String(pi.serviceId) === String(appt.serviceId) && pi.remainingCount > 0 ); } return false; }); let cardOptionsHtml = ''; if (matchingCards.length > 0) { cardOptionsHtml = matchingCards.map(c => { let detail = ''; if (c.cardType === 'count') detail = '剩余' + c.remainingCount + '次'; else if (c.cardType === 'time') detail = '剩余' + c.remainingHours + 'h'; else { const pi = (c.packageItems || []).find(pi => String(pi.serviceId) === String(appt.serviceId)); detail = pi ? '剩余' + pi.remainingCount + '次' : ''; } return '
' + '
' + '
' + c.cardName + ' ' + detail + '
' + '
'; }).join(''); } const html = ''; document.body.insertAdjacentHTML('beforeend', html); document.body.style.overflow = 'hidden'; }, selectApptPayment(type, cardId) { this._apptSelectedPayment = { type, cardId }; // 清除所有选中状态 const allRadios = ['apptPayBalanceRadio', 'apptPayCashRadio', 'apptPayWechatRadio', 'apptPayAlipayRadio']; allRadios.forEach(id => { const el = document.getElementById(id); if (el) { el.style.borderColor = '#ddd'; el.style.background = 'white'; } }); // 清除卡券选中状态 const heldCards = DataManager.getAllMemberHeldCards(); heldCards.forEach(c => { const radioEl = document.getElementById('apptPayCardRadio_' + c.id); if (radioEl) { radioEl.style.borderColor = '#ddd'; radioEl.style.background = 'white'; } }); // 设置选中状态 if (type === 'balance') { const radioEl = document.getElementById('apptPayBalanceRadio'); if (radioEl) { radioEl.style.borderColor = '#9B1B5E'; radioEl.style.background = '#9B1B5E'; } } else if (type === 'cash') { const radioEl = document.getElementById('apptPayCashRadio'); if (radioEl) { radioEl.style.borderColor = '#9B1B5E'; radioEl.style.background = '#9B1B5E'; } } else if (type === 'wechat') { const radioEl = document.getElementById('apptPayWechatRadio'); if (radioEl) { radioEl.style.borderColor = '#9B1B5E'; radioEl.style.background = '#9B1B5E'; } } else if (type === 'alipay') { const radioEl = document.getElementById('apptPayAlipayRadio'); if (radioEl) { radioEl.style.borderColor = '#9B1B5E'; radioEl.style.background = '#9B1B5E'; } } else if (type === 'card' && cardId) { const radioEl = document.getElementById('apptPayCardRadio_' + cardId); if (radioEl) { radioEl.style.borderColor = '#9B1B5E'; radioEl.style.background = '#9B1B5E'; } } }, closeApptConsumeModal() { const modal = document.getElementById('apptConsumeModal'); if (modal) modal.remove(); document.body.style.overflow = ''; this._completingApptId = null; this._completingAppt = null; this._completingMember = null; this._apptSelectedPayment = null; }, confirmApptConsume() { if (!this._apptSelectedPayment) { alert('请选择付款方式'); return; } const appt = this._completingAppt; const member = this._completingMember; const payment = this._apptSelectedPayment; if (!appt || !member) { alert('预约信息丢失,请重新操作'); this.closeApptConsumeModal(); return; } const servicePrice = appt.servicePrice || 0; const cardTypes = DataManager.getMemberCardItems() || []; const cardType = cardTypes.find(ct => String(ct.id) === String(member.cardTypeId)); const discount = cardType && cardType.serviceDiscount ? cardType.serviceDiscount : 10; const finalPrice = discount < 10 ? Math.round(servicePrice * discount) / 10 : servicePrice; // 检查余额是否足够 if (payment.type === 'balance' && member.balance < finalPrice) { alert('会员余额不足,当前余额¥' + member.balance.toFixed(2) + ',消费金额¥' + finalPrice); return; } // 生成消费记录 const orderNo = DataManager.generateOrderNo(); let payMethodText = ''; if (payment.type === 'balance') { // 扣减余额 member.balance -= finalPrice; DataManager.updateMember(member.id, { balance: member.balance }); payMethodText = '会员余额'; } else if (payment.type === 'card') { // 卡券扣次 const heldCard = DataManager.getAllMemberHeldCards().find(c => c.id === payment.cardId); if (!heldCard) { alert('卡券信息丢失'); return; } if (heldCard.cardType === 'count') { heldCard.remainingCount -= 1; DataManager.updateMemberHeldCard(heldCard.id, heldCard); } else if (heldCard.cardType === 'time') { heldCard.remainingHours -= 1; DataManager.updateMemberHeldCard(heldCard.id, heldCard); } else if (heldCard.cardType === 'package') { const pi = (heldCard.packageItems || []).find(pi => String(pi.serviceId) === String(appt.serviceId)); if (pi) { pi.remainingCount -= 1; DataManager.updateMemberHeldCard(heldCard.id, heldCard); } } payMethodText = '卡券扣次(' + heldCard.cardName + ')'; } else if (payment.type === 'cash') { payMethodText = '现金支付'; } else if (payment.type === 'wechat') { payMethodText = '微信支付'; } else if (payment.type === 'alipay') { payMethodText = '支付宝'; } // 添加消费记录 const recordData = { memberId: member.id, memberName: member.name, type: '消费', itemName: appt.serviceName, amount: -finalPrice, date: new Date().toISOString(), orderNo: orderNo, payMethod: payMethodText, discount: discount }; DataManager.addMemberRecord(recordData); // 累计积分 if (payment.type !== 'card') { DataManager.updateMemberPoints(member.id, Math.floor(finalPrice)); } // 计算提成 const employees = DataManager.getEmployees(); const staff = employees.find(e => String(e.id) === String(appt.staffId)); if (staff && staff.commissionSettings && staff.commissionSettings.service) { const serviceItems = DataManager.getServiceItems(); const service = serviceItems.find(s => String(s.id) === String(appt.serviceId)); if (service) { // [SEC-COMM-APPT-FIX] 修复预约完成弹窗 3 参数错位 → 4 参数 (positionId, targetType, targetId, originalAmount) const commissionResult = DataManager.calculateCommission(staff.positionId, "service", service.id, finalPrice); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission(staff.id, member.id, '服务提成', service.name, finalPrice, commissionResult, payMethodText, orderNo); } } } // 更新预约状态 DataManager.updateAppointment(this._completingApptId, { status: 'done' }); // 更新会员消费次数 if (member.consumeCount) { DataManager.updateMember(member.id, { consumeCount: member.consumeCount + 1 }); } else { DataManager.updateMember(member.id, { consumeCount: 1 }); } // 关闭弹窗 this.closeApptConsumeModal(); // 刷新列表 this.renderAppointmentList(); if (typeof DashboardModule !== 'undefined') DashboardModule.refreshTodos(); if (typeof DashboardModule !== 'undefined') DashboardModule.refreshStats(); alert('服务完成!消费记录已生成'); }, cancelAppointment(id) { DataManager.updateAppointment(id, { status: 'cancelled' }); this.renderAppointmentList(); if (typeof DashboardModule !== 'undefined') DashboardModule.refreshTodos(); }, deleteAppointment(id) { if (confirm('确定删除该预约记录?')) { DataManager.deleteAppointment(id); this.renderAppointmentList(); if (typeof DashboardModule !== 'undefined') DashboardModule.refreshTodos(); } }, openUpgradeHistory() { // [SEC-LS-CLEANUP P1] salon_upgrade_records 是死键 (无写入), 改本地空数组占位, 待接 API const records = []; records.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); let content = ''; if (records.length === 0) { content = '
暂无升级记录
'; } else { content = records.map(r => '
' + '
' + '👤 ' + r.memberName + '' + '' + r.date + '' + '
' + '
' + '' + r.fromCardType + ' → ' + '' + r.toCardType + '' + '
' + '
累计充值 ¥' + (r.totalRecharge || 0).toFixed(0) + ' · 达到 ¥' + (r.threshold || 0) + '
' + '
' ).join(''); } const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.id = 'upgradeHistoryModal'; modal.style.cssText = 'background:rgba(0,0,0,0.5);'; modal.innerHTML = ''; modal.addEventListener('click', function(e) { if (e.target === modal) { modal.classList.remove('active'); setTimeout(function() { modal.remove(); }, 300); document.body.style.overflow = ''; } }); document.body.appendChild(modal); // 触发显示动画 requestAnimationFrame(() => { modal.classList.add('active'); }); document.body.style.overflow = 'hidden'; }, // ============== SEC-S1-Task1+2: 短信配额相关 ============== // 入口右侧 badge:刷新当前店剩余条数 async refreshSmsQuotaBadge() { try { var token = localStorage.getItem('saas_token'); if (!token) return; var resp = await fetch('https://myapi.cddaojia.top/api/store/sms-quota', { headers: { 'Authorization': 'Bearer ' + token } }); if (!resp.ok) return; var json = await resp.json(); if (json && json.code === 0 && json.data) { var el = document.getElementById('smsQuotaBadge'); if (el) el.textContent = '剩 ' + json.data.remaining + ' 条'; } } catch(e) { /* silent */ } }, async openSmsQuotaModal(refreshOnly) { var modal = document.getElementById('smsQuotaModal'); var loading = document.getElementById('smsQuotaLoading'); var content = document.getElementById('smsQuotaContent'); var errEl = document.getElementById('smsQuotaError'); if (!modal) return; if (!refreshOnly) modal.style.display = 'flex'; loading.style.display = 'block'; content.style.display = 'none'; errEl.style.display = 'none'; try { var token = localStorage.getItem('saas_token'); var resp = await fetch('https://myapi.cddaojia.top/api/store/sms-quota', { headers: { 'Authorization': 'Bearer ' + token } }); var json = await resp.json(); if (!resp.ok || !json || json.code !== 0) { throw new Error((json && json.message) || ('HTTP ' + resp.status)); } var d = json.data || {}; document.getElementById('smsQuotaTotal').textContent = d.total || 0; document.getElementById('smsQuotaUsed').textContent = d.used || 0; document.getElementById('smsQuotaRemaining').textContent = d.remaining || 0; var pct = d.total > 0 ? Math.min(100, Math.round((d.used / d.total) * 100)) : 0; document.getElementById('smsQuotaBarFill').style.width = pct + '%'; // 统计分组 var statsEl = document.getElementById('smsQuotaStats'); var stats = Array.isArray(d.stats) ? d.stats : []; if (stats.length === 0) { statsEl.innerHTML = '
近 30 天暂无发送记录
'; } else { var scenLabel = { card_buy:'购卡通知', card_use:'核销通知', consume:'消费通知', recharge:'充值通知', login:'登录验证', member_login:'会员登录', staff_login:'员工登录', test:'测试' }; function _labelScen(s){ if(scenLabel[s]) return scenLabel[s]; if(!s) return '其他'; return String(s).replace(/_/g,' ').replace(/\b\w/g,function(c){return c.toUpperCase();}); } var statusLabel = { success:'✅ 成功', failed:'❌ 失败', no_quota:'⚠️ 余额不足', sign_failed:'⚠️ 签名失败', blocked:'⚠️ 被拦截', emergency:'🆘 应急放行' }; // v1.0.20 verify-fallback: 若近30天存在 emergency 记录,顶部插入橙色横条 var _emergencyTotal = 0; stats.forEach(function(r){ if(r.status==='emergency') _emergencyTotal += (r.cnt||0); }); // 按 scenario 聚合 var agg = {}; stats.forEach(function(r) { var k = r.scenario || 'unknown'; if (!agg[k]) agg[k] = { _total: 0 }; agg[k][r.status || 'unknown'] = (agg[k][r.status || 'unknown'] || 0) + (r.cnt || 0); agg[k]._total += (r.cnt || 0); }); var html = ''; Object.keys(agg).forEach(function(scen) { var row = agg[scen]; var parts = []; Object.keys(row).forEach(function(st) { if (st === '_total') return; parts.push((statusLabel[st] || st) + ' ' + row[st]); }); html += '
' + _labelScen(scen) + '(' + row._total + ') · ' + parts.join(' / ') + '
'; }); if (_emergencyTotal > 0) { html = '
' + '🆘 近30天曾应急放行 ' + _emergencyTotal + ' 条验证码短信(额度耗尽,未扣额度)· 请尽快充值以恢复通知类短信发送' + '
' + html; } statsEl.innerHTML = html; } loading.style.display = 'none'; content.style.display = 'block'; // 同步 badge var bd = document.getElementById('smsQuotaBadge'); if (bd) bd.textContent = '剩 ' + (d.remaining || 0) + ' 条'; // v1.0.20 UI2: 拉取购买记录 this._loadSmsHistory(); // v1.0.20 UI3: 拉取发送记录(默认今天) this._loadSmsLogs(); } catch (e) { loading.style.display = 'none'; errEl.textContent = '加载失败:' + (e.message || e); errEl.style.display = 'block'; } }, async _loadSmsHistory() { var wrap = document.getElementById('smsHistoryWrap'); var listEl = document.getElementById('smsHistoryList'); var toggleEl = document.getElementById('smsHistoryToggle'); var countEl = document.getElementById('smsHistoryCount'); if (!wrap || !listEl) return; try { var token = localStorage.getItem('saas_token'); var resp = await fetch('https://myapi.cddaojia.top/api/store/sms-package/history?limit=20', { headers: { 'Authorization': 'Bearer ' + token } }); var json = await resp.json(); if (!resp.ok || !json || json.code !== 0) throw new Error((json&&json.message)||('HTTP '+resp.status)); var records = (json.data && json.data.records) || []; wrap.style.display = 'block'; if (!records.length) { listEl.innerHTML = '
暂无购买记录
'; if (toggleEl) toggleEl.style.display = 'none'; if (countEl) countEl.textContent = ''; return; } if (countEl) countEl.textContent = '共 ' + records.length + ' 条'; this._smsHistory = records; this._smsHistoryExpanded = false; this._renderSmsHistory(); } catch (e) { wrap.style.display = 'block'; listEl.innerHTML = '
购买记录加载失败:'+(e.message||e)+'
'; } }, _renderSmsHistory() { var records = this._smsHistory || []; var expanded = !!this._smsHistoryExpanded; var listEl = document.getElementById('smsHistoryList'); var toggleEl = document.getElementById('smsHistoryToggle'); if (!listEl) return; var DEFAULT_N = 2; var showList = expanded ? records : records.slice(0, DEFAULT_N); var statusStyle = { fulfilled: {t:'✅ 已完成', c:'#16a34a', bg:'#dcfce7'}, paid: {t:'⏳ 处理中', c:'#0369a1', bg:'#e0f2fe'}, pending: {t:'⏱ 待支付', c:'#a16207', bg:'#fef9c3'}, failed: {t:'❌ 失败', c:'#dc2626', bg:'#fee2e2'}, cancelled: {t:'⛔ 已取消', c:'#6b7280', bg:'#f3f4f6'}, refunded: {t:'↩️ 已退款', c:'#7c3aed', bg:'#ede9fe'} }; function fmt(dt) { if (!dt) return '-'; try { var d = new Date(dt); var p = function(n){return n<10?'0'+n:''+n;}; return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())+' '+p(d.getHours())+':'+p(d.getMinutes()); } catch(_) { return String(dt).slice(0,16); } } var html = ''; showList.forEach(function(r) { var st = statusStyle[r.status] || {t: r.status, c:'#374151', bg:'#f3f4f6'}; var time = fmt(r.paidAt || r.createdAt); html += '
' + '
' + '
'+r.planName+' +'+r.smsCount+' 条
' + '
'+time+'
' + '
' + '
' + '
¥'+r.amountYuan+'
' + ''+st.t+'' + '
' + '
'; }); // 去掉最后一条底线 html = html.replace(/border-bottom:1px dashed #e5e7eb;(?![\s\S]*border-bottom:1px dashed #e5e7eb;)/, ''); listEl.innerHTML = html; if (records.length > DEFAULT_N && toggleEl) { toggleEl.style.display = 'block'; toggleEl.textContent = expanded ? '▲ 收起(仅显示最近 '+DEFAULT_N+' 条)' : '▼ 展开更多(还有 '+(records.length - DEFAULT_N)+' 条)'; var self = this; toggleEl.onclick = function() { self._smsHistoryExpanded = !self._smsHistoryExpanded; self._renderSmsHistory(); }; } else if (toggleEl) { toggleEl.style.display = 'none'; } }, // -------- v1.0.20 UI3: 发送记录 -------- async _loadSmsLogs(range) { var r = range || this._currentSmsLogRange || 'today'; this._currentSmsLogRange = r; var wrap = document.getElementById('smsLogWrap'); var listEl = document.getElementById('smsLogList'); var countEl = document.getElementById('smsLogCount'); var rangeBox = document.getElementById('smsLogRange'); var toggleEl = document.getElementById('smsLogToggle'); if (!wrap || !listEl) return; wrap.style.display = 'block'; // 绑定筛选按钮点击(幂等) if (rangeBox && !rangeBox.dataset.bound) { rangeBox.dataset.bound = '1'; var self = this; rangeBox.addEventListener('click', function(e) { var t = e.target; var rr = t.getAttribute && t.getAttribute('data-range'); if (!rr) return; self._loadSmsLogs(rr); }); } // 切换 active 样式 if (rangeBox) { var btns = rangeBox.querySelectorAll('[data-range]'); for (var i=0;i'; if (toggleEl) toggleEl.style.display = 'none'; } }, _renderSmsLogs() { var records = this._smsLogs || []; var expanded = !!this._smsLogExpanded; var listEl = document.getElementById('smsLogList'); var toggleEl = document.getElementById('smsLogToggle'); if (!listEl) return; if (!records.length) { listEl.innerHTML = '
该时段暂无发送记录
'; if (toggleEl) toggleEl.style.display = 'none'; return; } var DEFAULT_N = 3; var showList = expanded ? records : records.slice(0, DEFAULT_N); var scenLabel = { card_buy:'购卡通知', card_use:'核销通知', consume:'消费通知', recharge:'充值通知', login:'登录验证', member_login:'会员登录', staff_login:'员工登录', staff_reset_password:'员工找回密码', admin_reset_password:'超管找回密码', test:'测试' }; function labelScen(s){ if(scenLabel[s]) return scenLabel[s]; if(!s) return '其他'; return String(s).replace(/_/g,' ').replace(/\b\w/g,function(c){return c.toUpperCase();}); } var statusStyle = { success: {t:'✅ 成功', c:'#16a34a', bg:'#dcfce7'}, failed: {t:'❌ 失败', c:'#dc2626', bg:'#fee2e2'}, no_quota: {t:'⚠️ 余额不足', c:'#a16207', bg:'#fef9c3'}, sign_failed:{t:'⚠️ 签名失败', c:'#a16207', bg:'#fef9c3'}, blocked: {t:'⚠️ 被拦截', c:'#a16207', bg:'#fef9c3'}, emergency: {t:'🆘 应急放行', c:'#c2410c', bg:'#fff7ed'} }; function fmt(dt){ if (!dt) return '-'; try { var d = new Date(dt); var p = function(n){return n<10?'0'+n:''+n;}; return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())+' '+p(d.getHours())+':'+p(d.getMinutes()); } catch(_) { return String(dt).slice(0,16); } } var html = ''; showList.forEach(function(r) { var st = statusStyle[r.status] || {t:r.status||'-', c:'#374151', bg:'#f3f4f6'}; var reasonHtml = (r.status !== 'success' && r.failReason) ? '
' + r.failReason + '
' : ''; html += '
' + '
' + '' + labelScen(r.scenario) + '' + ''+st.t+'' + '
' + '
' + '📱 ' + (r.phone || '-') + '  ·  🏪 ' + (r.storeName || '-') + '
' + '
🕒 ' + fmt(r.createdAt) + '
' + reasonHtml + '
'; }); // 去掉最后一条底线 html = html.replace(/border-bottom:1px dashed #e5e7eb;(?![\s\S]*border-bottom:1px dashed #e5e7eb;)/, ''); listEl.innerHTML = html; if (records.length > DEFAULT_N && toggleEl) { toggleEl.style.display = 'block'; toggleEl.textContent = expanded ? '▲ 收起(仅显示最近 '+DEFAULT_N+' 条)' : '▼ 展开更多(还有 '+(records.length - DEFAULT_N)+' 条)'; var self = this; toggleEl.onclick = function() { self._smsLogExpanded = !self._smsLogExpanded; self._renderSmsLogs(); }; } else if (toggleEl) { toggleEl.style.display = 'none'; } }, closeSmsQuotaModal() { var m = document.getElementById('smsQuotaModal'); if (m) m.style.display = 'none'; }, // -------- Admin 配额管理 -------- openAdminSmsQuotaModal() { var modal = document.getElementById('adminSmsQuotaModal'); if (!modal) return; modal.style.display = 'flex'; this.refreshAdminSmsQuotaList(); }, closeAdminSmsQuotaModal() { var m = document.getElementById('adminSmsQuotaModal'); if (m) m.style.display = 'none'; }, async refreshAdminSmsQuotaList() { var loading = document.getElementById('adminQuotaListLoading'); var listEl = document.getElementById('adminQuotaList'); var emptyEl = document.getElementById('adminQuotaListEmpty'); var errEl = document.getElementById('adminQuotaListError'); loading.style.display = 'block'; listEl.style.display = 'none'; emptyEl.style.display = 'none'; errEl.style.display = 'none'; try { var token = localStorage.getItem('saas_token'); var resp = await fetch('https://myapi.cddaojia.top/api/admin/sms-quotas', { headers: { 'Authorization': 'Bearer ' + token } }); var json = await resp.json(); if (!resp.ok || !json || json.code !== 0) { throw new Error((json && json.message) || ('HTTP ' + resp.status)); } var list = (json.data && json.data.list) || []; var summary = (json.data && json.data.summary) || {}; document.getElementById('adminQuotaSumTotal').textContent = summary.total || 0; document.getElementById('adminQuotaSumUsed').textContent = summary.used || 0; document.getElementById('adminQuotaSumRemaining').textContent = summary.remaining || 0; if (list.length === 0) { loading.style.display = 'none'; emptyEl.style.display = 'block'; return; } var html = '' + '' + '' /* FIX10_QUOTA_MODAL */ + '' + '' + '' + '' + ''; list.forEach(function(it) { var sid = it.storeId; html += '' + '' + '' + '' + '' + '' + ''; }); html += '
门店总额已用剩余操作
' + '
' + (it.storeName || '-') + '
' + '
' + (it.storeCode || '') + ' · ID ' + sid + '
' + '
' + (it.total || 0) + '' + (it.used || 0) + '' + (it.remaining || 0) + '' + '
' + '' + '' + '' + '
' + '
'; listEl.innerHTML = html; loading.style.display = 'none'; listEl.style.display = 'block'; } catch (e) { loading.style.display = 'none'; errEl.textContent = '加载失败:' + (e.message || e); errEl.style.display = 'block'; } }, async applyStoreQuota(storeId) { // SEC-S1-Task2-InputV2: 输入框统一应用——正数=add,负数=sub var input = document.getElementById('qt-input-' + storeId); if (!input) return; var raw = String(input.value || '').trim(); if (!raw) { alert('请输入数量(默认 10,支持正负数)'); input.focus(); return; } if (!/^[+-]?\d+$/.test(raw)) { alert('请输入整数:正数=增加,负数=减少(如 -100)'); input.focus(); return; } var num = parseInt(raw, 10); if (num === 0) { alert('数量不能为 0'); input.focus(); return; } var action = num > 0 ? 'add' : 'sub'; var delta = Math.abs(num); if (!confirm('确定 ' + (action === 'add' ? '增加' : '减少') + ' ' + delta + ' 条短信额度?')) return; try { var token = localStorage.getItem('saas_token'); var resp = await fetch('https://myapi.cddaojia.top/api/admin/sms-quotas/' + storeId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ action: action, delta: delta }) }); var json = await resp.json(); if (!resp.ok || !json || json.code !== 0) { throw new Error((json && json.message) || ('HTTP ' + resp.status)); } this.refreshAdminSmsQuotaList(); } catch (e) { alert('操作失败:' + (e.message || e)); } }, async adjustStoreQuota(storeId, action, delta) { if (!confirm('确定 ' + (action === 'add' ? '增加' : '减少') + ' ' + delta + ' 条短信额度?')) return; try { var token = localStorage.getItem('saas_token'); var resp = await fetch('https://myapi.cddaojia.top/api/admin/sms-quotas/' + storeId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ action: action, delta: delta }) }); var json = await resp.json(); if (!resp.ok || !json || json.code !== 0) { throw new Error((json && json.message) || ('HTTP ' + resp.status)); } var d = json.data || {}; var elT = document.getElementById('qt-total-' + storeId); var elU = document.getElementById('qt-used-' + storeId); var elR = document.getElementById('qt-rem-' + storeId); if (elT) elT.textContent = d.total || 0; if (elU) elU.textContent = d.used || 0; if (elR) elR.textContent = d.remaining || 0; this.refreshAdminSmsQuotaList(); } catch (e) { alert('操作失败:' + (e.message || e)); } }, async setStoreQuota(storeId) { var v = prompt('请输入门店 #' + storeId + ' 的短信总额度(数字,覆盖现有总额):'); if (v == null) return; v = String(v).trim(); if (!/^\d+$/.test(v)) { alert('请输入非负整数'); return; } var total = parseInt(v, 10); try { var token = localStorage.getItem('saas_token'); var resp = await fetch('https://myapi.cddaojia.top/api/admin/sms-quotas/' + storeId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ total_quota: total }) }); var json = await resp.json(); if (!resp.ok || !json || json.code !== 0) { throw new Error((json && json.message) || ('HTTP ' + resp.status)); } this.refreshAdminSmsQuotaList(); } catch (e) { alert('操作失败:' + (e.message || e)); } } }; // ==================== 修改密码模块 ==================== const ChangePasswordModule = { open() { document.getElementById('cpOldPassword').value = ''; document.getElementById('cpNewPassword').value = ''; document.getElementById('cpNewPassword2').value = ''; document.getElementById('changePasswordModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, close(event) { if (event && event.target && !event.target.classList.contains('modal-overlay')) return; document.getElementById('changePasswordModal').classList.remove('active'); document.body.style.overflow = ''; }, async submit() { const oldPwd = document.getElementById('cpOldPassword').value || ''; const newPwd = document.getElementById('cpNewPassword').value || ''; const newPwd2 = document.getElementById('cpNewPassword2').value || ''; if (!oldPwd) { alert('请输入当前密码'); return; } if (!newPwd || newPwd.length < 8 || !/[A-Za-z]/.test(newPwd) || !/[0-9]/.test(newPwd)) { alert('新密码须至少 8 位且同时包含字母和数字'); return; } if (newPwd !== newPwd2) { alert('两次输入的新密码不一致'); return; } if (oldPwd === newPwd) { alert('新密码不能与当前密码相同'); return; } const btn = document.getElementById('cpSubmitBtn'); btn.disabled = true; const oldText = btn.textContent; btn.textContent = '提交中...'; try { const resp = await fetch(API_BASE + '/auth/password', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (authToken || '') }, body: JSON.stringify({ oldPassword: oldPwd, newPassword: newPwd }), }); const data = await resp.json(); if (data.code !== 0) { alert(data.message || '修改失败'); return; } alert('密码修改成功,将自动退出登录'); this.close(); setTimeout(() => { if (typeof doLogout === 'function') doLogout(); else location.reload(); }, 300); } catch (e) { alert('网络异常:' + (e.message || e)); } finally { btn.disabled = false; btn.textContent = oldText; } } }; // ==================== 忘记密码模块(三步流程) ==================== const ForgotPasswordModule = { step: 1, data: { username: '', phone: '' }, _resendTimer: null, open() { this.step = 1; this.data = { username: '', phone: '' }; document.getElementById('fpUsername').value = ''; document.getElementById('fpPhone').value = ''; document.getElementById('fpCode').value = ''; document.getElementById('fpNewPassword').value = ''; document.getElementById('fpNewPassword2').value = ''; this._renderStep(); document.getElementById('forgotPasswordModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, close(event) { if (event && event.target && !event.target.classList.contains('modal-overlay')) return; document.getElementById('forgotPasswordModal').classList.remove('active'); document.body.style.overflow = ''; if (this._resendTimer) { clearInterval(this._resendTimer); this._resendTimer = null; } }, _renderStep() { // 标题 document.getElementById('fpTitle').textContent = this.step === 1 ? '找回密码' : (this.step === 2 ? '验证身份' : '设置新密码'); // 步骤点 [1, 2, 3].forEach(i => { const dot = document.getElementById('fpStepDot' + i); if (i <= this.step) { dot.style.background = '#9B1B5E'; dot.style.color = '#fff'; } else { dot.style.background = '#e8e4df'; dot.style.color = '#999'; } }); // 显示对应 step document.getElementById('fpStep1').style.display = this.step === 1 ? '' : 'none'; document.getElementById('fpStep2').style.display = this.step === 2 ? '' : 'none'; document.getElementById('fpStep3').style.display = this.step === 3 ? '' : 'none'; // 按钮 const nextBtn = document.getElementById('fpNextBtn'); const backBtn = document.getElementById('fpBackBtn'); const cancelBtn = document.getElementById('fpCancelBtn'); if (this.step === 1) { nextBtn.textContent = '获取验证码'; backBtn.style.display = 'none'; cancelBtn.style.display = ''; } else if (this.step === 2) { nextBtn.textContent = '下一步'; backBtn.style.display = ''; cancelBtn.style.display = 'none'; } else { nextBtn.textContent = '确认重置'; backBtn.style.display = ''; cancelBtn.style.display = 'none'; } }, async next() { const nextBtn = document.getElementById('fpNextBtn'); if (this.step === 1) { const username = (document.getElementById('fpUsername').value || '').trim(); const phone = (document.getElementById('fpPhone').value || '').trim(); if (!username) { alert('请输入账号'); return; } if (!/^1[3-9]\d{9}$/.test(phone)) { alert('请输入正确的 11 位手机号'); return; } nextBtn.disabled = true; const oldText = nextBtn.textContent; nextBtn.textContent = '发送中...'; try { const resp = await fetch(API_BASE + '/auth/forgot-password/send-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, phone }), }); const data = await resp.json(); if (data.code !== 0) { alert(data.message || '发送失败'); return; } this.data = { username, phone }; this.step = 2; document.getElementById('fpPhoneMask').textContent = phone.slice(0, 3) + '****' + phone.slice(7); this._renderStep(); this._startResendCountdown(60); } catch (e) { alert('网络异常:' + (e.message || e)); } finally { nextBtn.disabled = false; // 仅当 step 未切换时恢复文字,避免覆盖 _renderStep() 设置的新按钮文字 if (this.step === 1) nextBtn.textContent = oldText; } } else if (this.step === 2) { const code = (document.getElementById('fpCode').value || '').trim(); if (!/^\d{6}$/.test(code)) { alert('请输入 6 位验证码'); return; } this.step = 3; this._renderStep(); } else if (this.step === 3) { const code = (document.getElementById('fpCode').value || '').trim(); const newPwd = document.getElementById('fpNewPassword').value || ''; const newPwd2 = document.getElementById('fpNewPassword2').value || ''; if (!newPwd || newPwd.length < 8 || !/[A-Za-z]/.test(newPwd) || !/[0-9]/.test(newPwd)) { alert('新密码须至少 8 位且同时包含字母和数字'); return; } if (newPwd !== newPwd2) { alert('两次输入的新密码不一致'); return; } nextBtn.disabled = true; const oldText = nextBtn.textContent; nextBtn.textContent = '重置中...'; try { const resp = await fetch(API_BASE + '/auth/forgot-password/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: this.data.username, phone: this.data.phone, code, newPassword: newPwd, }), }); const data = await resp.json(); if (data.code !== 0) { alert(data.message || '重置失败'); // 若是验证码相关错误,回到第 2 步 if (data.message && (data.message.indexOf('验证码') >= 0)) { this.step = 2; this._renderStep(); } return; } alert('密码已重置,请使用新密码登录'); this.close(); // 自动填充账号,方便登录 try { document.getElementById('loginUser').value = this.data.username; document.getElementById('loginPass').value = ''; document.getElementById('loginPass').focus(); } catch (e) {} } catch (e) { alert('网络异常:' + (e.message || e)); } finally { nextBtn.disabled = false; // 仅当 step 未切换(验证码错误未回退到 2)时恢复文字 if (this.step === 3) nextBtn.textContent = oldText; } } }, back() { if (this.step === 2) { this.step = 1; this._renderStep(); } else if (this.step === 3) { this.step = 2; this._renderStep(); } }, async resendCode() { const btn = document.getElementById('fpResendBtn'); if (btn.dataset.cooling === '1') return; try { const resp = await fetch(API_BASE + '/auth/forgot-password/send-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: this.data.username, phone: this.data.phone }), }); const data = await resp.json(); if (data.code !== 0) { alert(data.message || '发送失败'); return; } alert('验证码已重新发送'); this._startResendCountdown(60); } catch (e) { alert('网络异常:' + (e.message || e)); } }, _startResendCountdown(seconds) { const btn = document.getElementById('fpResendBtn'); if (!btn) return; if (this._resendTimer) { clearInterval(this._resendTimer); } let s = seconds; btn.dataset.cooling = '1'; btn.style.color = '#999'; btn.textContent = s + 's 后可重发'; this._resendTimer = setInterval(() => { s -= 1; if (s <= 0) { clearInterval(this._resendTimer); this._resendTimer = null; btn.dataset.cooling = '0'; btn.style.color = '#9B1B5E'; btn.textContent = '重新发送'; } else { btn.textContent = s + 's 后可重发'; } }, 1000); } }; // ==================== 仪表盘模块 ==================== const DashboardModule = { init() { this.refreshStats(); this.refreshTodos(); }, refreshStats() { const members = DataManager.getMembers(); const records = DataManager.getRecords(); const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); // 会员总数 document.getElementById('dashMemberTotal').textContent = members.length; // 今日新增会员 const todayNew = members.filter(m => new Date(m.createdAt || m.createDate) >= todayStart).length; document.getElementById('dashTodayNew').textContent = todayNew; // 本月充值金额 const monthRecharge = records.filter(r => { const d = new Date(r.date); return d >= monthStart && r.type === '充值'; }).reduce((sum, r) => sum + parseFloat(r.amount || 0), 0); document.getElementById('dashMonthRecharge').textContent = monthRecharge.toFixed(0); // 本月消费金额 const monthConsume = records.filter(r => { const d = new Date(r.date); return d >= monthStart && r.type === '消费'; }).reduce((sum, r) => sum + parseFloat(r.amount || 0), 0); document.getElementById('dashMonthConsume').textContent = Math.abs(monthConsume).toFixed(0); // 客单价 = 本月消费总额 / 本月消费次数 const monthConsumeRecords = records.filter(r => { const d = new Date(r.date); return d >= monthStart && r.type === '消费' && !r.cancelled; }); const avgOrder = monthConsumeRecords.length > 0 ? Math.abs(monthConsumeRecords.reduce((s,r) => s + parseFloat(r.amount||0), 0)) / monthConsumeRecords.length : 0; document.getElementById('dashAvgOrder').textContent = '¥' + avgOrder.toFixed(0); // 复购率 = 消费2次以上的会员 / 有消费记录的会员 × 100% const memberConsumeCount = {}; monthConsumeRecords.forEach(r => { memberConsumeCount[r.memberId] = (memberConsumeCount[r.memberId] || 0) + 1; }); const totalConsumeMembers = Object.keys(memberConsumeCount).length; const repeatMembers = Object.values(memberConsumeCount).filter(c => c >= 2).length; const repurchaseRate = totalConsumeMembers > 0 ? (repeatMembers / totalConsumeMembers * 100) : 0; document.getElementById('dashRepurchaseRate').textContent = repurchaseRate.toFixed(0) + '%'; // 活跃会员 = 30天内有消费记录的会员数 const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); const activeMembers = new Set(); records.forEach(r => { if (r.type === '消费' && !r.cancelled && new Date(r.date) >= thirtyDaysAgo) { activeMembers.add(r.memberId); } }); document.getElementById('dashActiveMembers').textContent = activeMembers.size; // 本月新增会员 const monthNew = members.filter(m => new Date(m.createdAt || m.createDate) >= monthStart).length; document.getElementById('dashMonthNew').textContent = monthNew; }, refreshTodos() { const members = DataManager.getMembers(); const heldCards = DataManager.getAllMemberHeldCards ? DataManager.getAllMemberHeldCards() : (JSON.parse(localStorage.getItem('salon_member_held_cards') || '[]')); const records = DataManager.getAllMemberRecords ? DataManager.getAllMemberRecords() : DataManager.getMemberRecords(); const settings = DataManager.getNotifySettings(); const now = new Date(); const expiryThreshold = settings.cardExpiryDays || 7; const expiryDate = new Date(now.getTime() + expiryThreshold * 24 * 60 * 60 * 1000); const lowThreshold = settings.lowBalanceThreshold || 50; // 即将过期/耗尽卡券数量 let expiringCards = 0; heldCards.forEach(c => { if (c.status !== 'active') return; // [C] 兜底:已用完的卡(理论应已 used_up),整张跳过避免误报 if (c.cardType === 'count' && (c.remainingCount || 0) <= 0) return; if (c.cardType === 'time' && (c.remainingHours || 0) <= 0) return; if (c.cardType === 'package') { const totalLeft = (c.packageItems || []).reduce((s, p) => s + (p.remainingCount || 0), 0); if (totalLeft <= 0) return; } // 检查到期日期 if (c.expireDate) { const expireDate = new Date(c.expireDate); if (expireDate <= expiryDate && expireDate >= now) expiringCards++; } // 检查次卡即将耗尽(剩余 1~2 次) if (c.cardType === 'count' && (c.remainingCount || 0) > 0 && (c.remainingCount || 0) <= 2) expiringCards++; // 检查计时卡即将耗尽(剩余 >0 且 ≤2 小时) if (c.cardType === 'time' && (c.remainingHours || 0) > 0 && (c.remainingHours || 0) <= 2) expiringCards++; // [B] 套餐卡:所有项目剩余次数总和 1~2 才算即将耗尽(与次卡阈值一致,避免新卡误报) if (c.cardType === 'package') { const totalLeft = (c.packageItems || []).reduce((s, p) => s + (p.remainingCount || 0), 0); if (totalLeft > 0 && totalLeft <= 2) expiringCards++; } }); document.getElementById('dashTodoExpiry').textContent = expiringCards + '张卡券即将到期'; // 余额不足会员数量 const lowBalanceMembers = members.filter(m => { const balance = parseFloat(m.balance || 0); return balance < lowThreshold; }).length; document.getElementById('dashTodoLowBalance').textContent = lowBalanceMembers + '位会员余额不足'; // 待跟进新会员数量(7天内新增且无消费) const weekStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const newMembersToFollow = members.filter(m => { const created = new Date(m.createDate || m.createdAt); if (created < weekStart) return false; const consumeRecords = records.filter(r => String(r.memberId) === String(m.id) && r.type === '消费'); return consumeRecords.length === 0; }).length; document.getElementById('dashTodoNewMember').textContent = newMembersToFollow + '位待跟进新会员'; // 本月生日会员数量 const thisMonth = now.getMonth() + 1; const birthdayMembers = members.filter(m => { if (!m.birthday) return false; const month = parseInt(m.birthday.split('-')[1]); return month === thisMonth; }); document.getElementById('dashTodoBirthday').textContent = birthdayMembers.length + '位会员本月生日'; // 今日预约数量 const todayStr = now.toISOString().split('T')[0]; const appointments = DataManager.getAppointments ? DataManager.getAppointments() : []; if (settings.appointment !== false) { const todayAppointments = appointments.filter(a => a.date === todayStr && a.status === 'pending'); document.getElementById('dashTodoAppointment').textContent = todayAppointments.length + '个今日预约'; } else { document.getElementById('dashTodoAppointment').textContent = '预约提醒已关闭'; } // 低库存预警 (库存 <= 5 视为预警) const productItems = DataManager.getProductItems().filter(p => p.enabled); const lowStockProducts = productItems.filter(p => (parseInt(p.stock) || 0) <= 5); const outOfStockCount = productItems.filter(p => (parseInt(p.stock) || 0) <= 0).length; const lowStockEl = document.getElementById('dashTodoLowStock'); if (lowStockEl) { let text = lowStockProducts.length + '款产品库存紧张'; if (outOfStockCount > 0) text += '(' + outOfStockCount + '款已售罄)'; lowStockEl.textContent = text; } }, quickAddMember() { MemberModule.openAddModal(); }, // ==================== SEC-I 快捷选择会员弹层 ==================== _pickerAction: null, _visiblePickerMembers: 50, // [Fix8-B 20260709] 初始渲染 50 条 _PICKER_BATCH: 50, // [Fix8-B 20260709] 每次追加 50 条 _pickerDebounceTimer: null, // [Fix8-B 20260709] 搜索 debounce 定时器 quickOpenAction(action) { this._pickerAction = action; this._visiblePickerMembers = 50; /* [Fix8-B 20260709] 每次打开弹层复位 */ var titleMap = { recharge: '选择充值会员', consume: '选择消费会员', purchaseCard: '选择购卡会员' }; var elTitle = document.getElementById('quickPickerTitle'); if (elTitle) elTitle.textContent = titleMap[action] || '选择会员'; var elSearch = document.getElementById('quickPickerSearch'); if (elSearch) elSearch.value = ''; this.renderPickerList(''); document.getElementById('quickMemberPickerModal').classList.add('active'); document.body.style.overflow = 'hidden'; // 自动聚焦搜索框 (移动端可能弹键盘, 延迟一点点) setTimeout(function(){ if (elSearch) elSearch.focus(); }, 280); }, closePicker() { document.getElementById('quickMemberPickerModal').classList.remove('active'); document.body.style.overflow = ''; this._pickerAction = null; this._visiblePickerMembers = 50; /* [Fix8-B 20260709] 关闭复位 */ }, onPickerSearchInput() { // [Fix8-B 20260709] debounce 250ms,避免快速打字时每次 keyup 都触发 4000 会员全量 filter+sort+render var self = this; var kw = (document.getElementById('quickPickerSearch').value || '').trim(); if (self._pickerDebounceTimer) { clearTimeout(self._pickerDebounceTimer); self._pickerDebounceTimer = null; } self._pickerDebounceTimer = setTimeout(function(){ self._pickerDebounceTimer = null; self.renderPickerList(kw); }, 250); }, renderPickerList(filter) { var listEl = document.getElementById('quickPickerList'); if (!listEl) return; var members = []; try { if (typeof DataManager !== 'undefined' && DataManager.getMembers) members = DataManager.getMembers() || []; } catch(e){} var kw = (filter || '').toLowerCase(); var filtered = kw ? members.filter(function(m){ return (String(m.name||'')).toLowerCase().indexOf(kw) >= 0 || (String(m.phone||'')).indexOf(kw) >= 0; }) : members.slice(); // 按创建时间倒序 (最近添加的会员优先) filtered.sort(function(a,b){ var ta = new Date(a.createDate || a.createdAt || a.createTime || 0).getTime(); var tb = new Date(b.createDate || b.createdAt || b.createTime || 0).getTime(); return tb - ta; }); if (filtered.length === 0) { var emptyHint = kw ? '试试其他关键词' : '点首页"添加会员"先创建一位'; listEl.innerHTML = '
👥
未找到会员
'+emptyHint+'
'+ (kw ? '' : '')+ '
'; return; } // [Fix8-B 20260709] 分批展开:初始 50,每次追加 50;硬上限仍 200 var vis = this._visiblePickerMembers || 50; var batch = this._PICKER_BATCH || 50; var cap = Math.min(vis, 200, filtered.length); var capped = filtered.slice(0, cap); var totalHint = filtered.length > 200 ? ('共 '+filtered.length+' 位 · 最近 200 位中显示 '+cap) : ('共 '+filtered.length+' 位'); var html = '
'+totalHint+'
'; for (var i=0; i'+ '
'+escapeHtml(firstChar)+'
'+ '
'+ '
'+escapeHtml(name)+'
'+ '
'+escapeHtml(phone)+'
'+ '
'+ '
'+ '
'+balText+'
'+ '
余额
'+ '
'+ ''; } // [Fix8-B 20260709] 追加 展开/收起 按钮 var poolSize = Math.min(filtered.length, 200); if (poolSize > cap) { var remaining = poolSize - cap; var nextBatch = Math.min(batch, remaining); html += '
'+ ''+ '
'; } else if (cap > 50 && cap > batch) { html += '
'+ ''+ '
'; } listEl.innerHTML = html; }, // [Fix8-B 20260709] 追加展开 50 位 togglePickerMembers() { this._visiblePickerMembers = Math.min((this._visiblePickerMembers || 50) + (this._PICKER_BATCH || 50), 200); var kw = ''; try { kw = (document.getElementById('quickPickerSearch').value || '').trim(); } catch(e){} this.renderPickerList(kw); }, // [Fix8-B 20260709] 独立收起入口 collapsePickerMembers() { this._visiblePickerMembers = 50; var kw = ''; try { kw = (document.getElementById('quickPickerSearch').value || '').trim(); } catch(e){} this.renderPickerList(kw); }, pickMember(memberId) { var action = this._pickerAction; if (!action || !memberId) { this.closePicker(); return; } try { if (typeof MemberModule !== 'undefined') { MemberModule._fromQuickAction = true; MemberModule.currentDetailId = memberId; } } catch(e){} this.closePicker(); try { document.querySelectorAll('.page-section.active').forEach(function(p){ p.classList.remove('active'); }); } catch(e){} try { if (typeof DataManager !== 'undefined' && DataManager.getMembers) MemberModule.members = DataManager.getMembers() || []; } catch(e){} try { if (action === 'recharge' && MemberModule.openRecharge) { MemberModule.openRecharge(memberId); } else if (action === 'consume' && MemberModule.openConsume) { MemberModule.openConsume(memberId); } else if (action === 'purchaseCard' && MemberModule.openPurchaseCard) { MemberModule.openPurchaseCard(); } } catch(e){ console.warn('pickMember failed', e); } }, showBirthdayList() { const members = DataManager.getMembers(); const now = new Date(); const thisMonth = now.getMonth() + 1; const birthdayMembers = members.filter(m => { if (!m.birthday) return false; const month = parseInt(m.birthday.split('-')[1]); return month === thisMonth; }).sort((a, b) => { const dayA = parseInt(a.birthday.split('-')[2]); const dayB = parseInt(b.birthday.split('-')[2]); return dayA - dayB; }); const container = document.getElementById('notificationList'); if (birthdayMembers.length === 0) { container.innerHTML = '
本月无生日会员
'; } else { let html = ''; birthdayMembers.forEach(m => { html += '
' + '
' + '🎂' + '
' + '
' + m.name + '
' + '
生日:' + m.birthday + '
' + '
' + '
' + (m.phone || '') + '
' + '
' + '
'; }); container.innerHTML = html; } document.getElementById('notificationModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, showTodayAppointments() { // 打开设置页的预约管理弹窗,筛选今日 switchPage('settings'); setTimeout(function() { if (typeof SettingsModule !== 'undefined') { SettingsModule.openAppointmentModal(); SettingsModule.filterAppointments('today'); } }, 100); }, showNotifications() { const notifications = this.generateNotifications(); const container = document.getElementById('notificationList'); if (notifications.length === 0) { container.innerHTML = '
暂无通知
'; } else { let html = ''; notifications.forEach(n => { html += `
${n.type === 'expiry' ? '' : n.type === 'lowBalance' ? '' : ''}
${escapeHtml(n.title)}
${escapeHtml(n.desc)}
${escapeHtml(n.time)}
`; }); container.innerHTML = html; } document.getElementById('notificationModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, generateNotifications() { const notifications = []; const members = DataManager.getMembers(); const heldCards = DataManager.getAllMemberHeldCards ? DataManager.getAllMemberHeldCards() : (JSON.parse(localStorage.getItem('salon_member_held_cards') || '[]')); const settings = DataManager.getNotifySettings(); const now = new Date(); // 卡券即将过期/耗尽通知 if (settings.cardExpiry) { const expiryDays = settings.cardExpiryDays || 7; const expiryThreshold = new Date(now.getTime() + expiryDays * 24 * 60 * 60 * 1000); heldCards.forEach(c => { if (c.status !== 'active') return; const member = members.find(m => String(m.id) === String(c.memberId)); // 到期日期检查 if (c.expireDate) { const expiry = new Date(c.expireDate); if (expiry <= expiryThreshold && expiry >= now) { const daysLeft = Math.ceil((expiry - now) / (24 * 60 * 60 * 1000)); notifications.push({ type: 'expiry', title: '卡券即将过期', desc: `${member ? member.name : '未知会员'}的${c.cardName || '卡券'}将在${daysLeft}天后过期`, time: this.formatTime(now) }); } } // 次卡即将耗尽(剩余≤2次) if (c.cardType === 'count' && (c.remainingCount || 0) <= 2) { notifications.push({ type: 'lowCount', title: '次卡即将耗尽', desc: `${member ? member.name : '未知会员'}的${c.cardName || '次卡'}仅剩${c.remainingCount || 0}次`, time: this.formatTime(now) }); } // 计时卡即将耗尽(剩余≤2小时) if (c.cardType === 'time' && (c.remainingHours || 0) <= 2) { notifications.push({ type: 'lowTime', title: '计时卡即将耗尽', desc: `${member ? member.name : '未知会员'}的${c.cardName || '计时卡'}仅剩${c.remainingHours || 0}小时`, time: this.formatTime(now) }); } // 套餐卡即将耗尽(剩余≤3项) if (c.cardType === 'package') { const remainingItems = (c.packageItems || []).filter(p => (p.remainingCount || 0) > 0).length; if (remainingItems <= 3) { notifications.push({ type: 'lowPackage', title: '套餐卡即将耗尽', desc: `${member ? member.name : '未知会员'}的${c.cardName || '套餐卡'}仅剩${remainingItems}项可用`, time: this.formatTime(now) }); } } }); } // 余额不足通知 if (settings.lowBalance) { const threshold = settings.lowBalanceThreshold || 50; members.forEach(m => { if (parseFloat(m.balance || 0) < threshold) { notifications.push({ type: 'lowBalance', title: '会员余额不足', desc: `${m.name}余额仅剩¥${parseFloat(m.balance || 0).toFixed(2)},建议提醒充值`, time: this.formatTime(now) }); } }); } // 生日提醒 if (settings.birthday) { members.forEach(m => { if (!m.birthday) return; const birthday = new Date(m.birthday); const thisYearBirthday = new Date(now.getFullYear(), birthday.getMonth(), birthday.getDate()); const diffDays = Math.ceil((thisYearBirthday - now) / (24 * 60 * 60 * 1000)); if (diffDays >= 0 && diffDays <= 7) { notifications.push({ type: 'birthday', title: '会员生日提醒', desc: `${m.name}的生日即将到来(${diffDays === 0 ? '今天' : diffDays + '天后'}),可发送祝福`, time: this.formatTime(now) }); } }); } // 预约提醒(当天 + 提前 N 天窗口) if (settings.appointment) { const apptAheadDays = (settings.appointmentDays != null ? settings.appointmentDays : 1); const apptAppts = DataManager.getAppointments ? DataManager.getAppointments() : []; const apptTodayZero = new Date(now.getFullYear(), now.getMonth(), now.getDate()); apptAppts.forEach(a => { if (!a || a.status !== 'pending' || !a.date) return; const parts = String(a.date).split('-'); if (parts.length < 3) return; const aDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); const diffDays = Math.floor((aDate - apptTodayZero) / (24 * 60 * 60 * 1000)); if (diffDays < 0 || diffDays > apptAheadDays) return; const member = members.find(m => String(m.id) === String(a.memberId)); const whenLabel = diffDays === 0 ? '今日' : (diffDays === 1 ? '明日' : diffDays + '天后'); const memberName = member ? member.name : (a.memberName || '未知会员'); notifications.push({ type: 'appointment', title: whenLabel + '预约提醒', desc: `${memberName}${a.time ? ' ' + a.time : ''} 有预约${a.service ? ':' + a.service : ''}`, time: this.formatTime(now) }); }); } return notifications; }, formatTime(date) { const now = new Date(); const diff = now - date; if (diff < 60000) return '刚刚'; if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'; if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'; return date.toLocaleDateString(); }, closeNotificationModal() { document.getElementById('notificationModal').classList.remove('active'); document.body.style.overflow = ''; }, goToExpiryCards() { this.closeNotificationModal(); goToCardManage(); }, goToLowBalanceMembers() { this.closeNotificationModal(); goToMemberList(); }, goToNewMembers() { this.closeNotificationModal(); goToMemberList(); } }; // ==================== 提成设置模块 ==================== const CommissionModule = { currentPositionId: null, currentTab: 'service', batchCommissionType: 'percentage', // [SEC-COMM-BATCH-FILTER] 批量设置状态 batchSelectedIds: null, batchCurrentCategoryId: null, batchSearchKeyword: '', init() {}, openModal() { document.getElementById('commissionModal').classList.add('active'); document.body.style.overflow = 'hidden'; // 填充职位下拉 const select = document.getElementById('commissionPositionSelect'); const positions = DataManager.getEmployeePositions(); select.innerHTML = '' + positions.map(p => ``).join(''); this.currentPositionId = null; this.currentTab = 'service'; this.renderTabs(); this.syncPositionVisibility(); this.renderList(); }, // 切 tab / 打开时统一控制职位选择器可见性 syncPositionVisibility() { const wrap = document.querySelector('.commission-position-select'); if (wrap) wrap.style.display = (this.currentTab === 'service') ? '' : 'none'; if (this.currentTab !== 'service') { // 非 service tab 一律全员(positionId=0) this.currentPositionId = 0; } }, closeModal() { document.getElementById('commissionModal').classList.remove('active'); document.body.style.overflow = ''; }, onPositionChange() { this.currentPositionId = document.getElementById('commissionPositionSelect').value; this.renderList(); }, switchTab(tab) { this.currentTab = tab; this.renderTabs(); this.syncPositionVisibility(); this.renderList(); // 服务和产品都支持批量设置(批量逻辑复用现有 confirmBatch) document.getElementById('commissionBatchBtn').style.display = (tab === 'service' || tab === 'product') ? 'flex' : 'none'; }, renderTabs() { const tabLabel = this.currentTab === 'service' ? '服务' : this.currentTab === 'recharge' ? '充值' : this.currentTab === 'card' ? '购卡' : this.currentTab === 'product' ? '产品' : ''; document.querySelectorAll('.commission-tab').forEach(t => { t.classList.toggle('active', t.textContent.includes(tabLabel)); }); }, renderList() { const container = document.getElementById('commissionRuleList'); if (this.currentTab === 'service' && !this.currentPositionId) { container.innerHTML = '
请先选择职位
'; return; } const rules = DataManager.getCommissionRules(); const targetType = this.currentTab; const items = this.getTargetItems(targetType); if (items.length === 0) { container.innerHTML = '
暂无项目
'; return; } let html = ''; items.forEach(item => { const existingRule = rules.find(r => String(r.positionId) === String(this.currentPositionId) && r.targetType === targetType && String(r.targetId) === String(item.id) ); const commissionType = existingRule ? existingRule.commissionType : 'percentage'; const commissionValue = existingRule ? existingRule.commissionValue : ''; const performanceAmount = existingRule ? existingRule.performanceAmount : ''; const enabled = existingRule ? existingRule.enabled : false; html += `
${escapeHtml(item.name)}
比例
固定
`; }); container.innerHTML = html; }, getTargetItems(targetType) { if (targetType === 'service') { return DataManager.getServiceItems().filter(s => s.enabled); } else if (targetType === 'recharge') { // 充值提成按会员卡类型设置 return DataManager.getMemberCardItems().filter(c => c.enabled); } else if (targetType === 'card') { return DataManager.getCardItems().filter(c => c.enabled); } else if (targetType === 'product') { return DataManager.getProductItems().filter(p => p.enabled); } return []; }, setCommissionType(targetId, type) { const rules = DataManager.getCommissionRules(); const existingRule = rules.find(r => String(r.positionId) === String(this.currentPositionId) && r.targetType === this.currentTab && String(r.targetId) === String(targetId) ); if (existingRule) { DataManager.updateCommissionRule(existingRule.id, { commissionType: type }); } else { const items = this.getTargetItems(this.currentTab); const item = items.find(i => String(i.id) === String(targetId)); DataManager.addCommissionRule({ positionId: this.currentPositionId, positionName: (this.currentTab === 'service' ? (DataManager.getEmployeePositions().find(p => String(p.id) === String(this.currentPositionId))?.name || '') : '全员'), targetType: this.currentTab, targetId: targetId, targetName: item ? item.name : '', commissionType: type, commissionValue: 0, performanceAmount: 0, enabled: false }); } this.renderList(); }, saveRule(targetId, targetType, value, commissionType) { const rules = DataManager.getCommissionRules(); const existingRule = rules.find(r => String(r.positionId) === String(this.currentPositionId) && r.targetType === targetType && String(r.targetId) === String(targetId) ); if (existingRule) { DataManager.updateCommissionRule(existingRule.id, { commissionValue: parseFloat(value) || 0 }); } else { const items = this.getTargetItems(targetType); const item = items.find(i => String(i.id) === String(targetId)); DataManager.addCommissionRule({ positionId: this.currentPositionId, positionName: (this.currentTab === 'service' ? (DataManager.getEmployeePositions().find(p => String(p.id) === String(this.currentPositionId))?.name || '') : '全员'), targetType: targetType, targetId: targetId, targetName: item ? item.name : '', commissionType: commissionType, commissionValue: parseFloat(value) || 0, performanceAmount: 0, enabled: false }); } }, savePerformance(targetId, value) { const rules = DataManager.getCommissionRules(); const existingRule = rules.find(r => String(r.positionId) === String(this.currentPositionId) && r.targetType === this.currentTab && String(r.targetId) === String(targetId) ); if (existingRule) { DataManager.updateCommissionRule(existingRule.id, { performanceAmount: parseFloat(value) || 0 }); } }, toggleEnabled(targetId) { const rules = DataManager.getCommissionRules(); const existingRule = rules.find(r => String(r.positionId) === String(this.currentPositionId) && r.targetType === this.currentTab && String(r.targetId) === String(targetId) ); if (existingRule) { // 如果要启用,先检查提成值是否为0 const newEnabled = !existingRule.enabled; if (newEnabled && (existingRule.commissionValue === 0 || existingRule.commissionValue === '' || existingRule.commissionValue === null)) { alert('请先设置提成值'); return; } DataManager.updateCommissionRule(existingRule.id, { enabled: newEnabled }); } else { // 新建规则时默认启用,但提成值为0,提示用户设置 const items = this.getTargetItems(this.currentTab); const item = items.find(i => String(i.id) === String(targetId)); DataManager.addCommissionRule({ positionId: this.currentPositionId, positionName: (this.currentTab === 'service' ? (DataManager.getEmployeePositions().find(p => String(p.id) === String(this.currentPositionId))?.name || '') : '全员'), targetType: this.currentTab, targetId: targetId, targetName: item ? item.name : '', commissionType: 'percentage', commissionValue: 0, performanceAmount: 0, enabled: true }); alert('请先设置提成值'); } this.renderList(); }, openBatchModal() { if (this.currentTab === 'service' && !this.currentPositionId) { alert('请先选择职位'); return; } // [SEC-COMM-BATCH-FILTER] 切标题 + 重置状态(默认全不选) const titleLabel = this.currentTab === 'service' ? '服务' : '产品'; const titleEl = document.getElementById('batchModalTitle'); if (titleEl) titleEl.textContent = `批量设置${titleLabel}提成`; this.batchSelectedIds = new Set(); this.batchCurrentCategoryId = null; this.batchSearchKeyword = ''; this.batchCommissionType = 'percentage'; this.renderBatchType(); this.renderBatchCategoryTabs(); this.renderBatchItemList(); const searchEl = document.getElementById('batchSearchInput'); if (searchEl) searchEl.value = ''; document.getElementById('batchCommissionValue').value = ''; document.getElementById('batchPerformanceAmount').value = ''; document.getElementById('commissionBatchModal').classList.add('active'); }, closeBatchModal() { document.getElementById('commissionBatchModal').classList.remove('active'); }, setBatchCommissionType(type) { this.batchCommissionType = type; this.renderBatchType(); }, renderBatchType() { document.getElementById('batchCommissionTypePercent').classList.toggle('active', this.batchCommissionType === 'percentage'); document.getElementById('batchCommissionTypeFixed').classList.toggle('active', this.batchCommissionType === 'fixed'); }, confirmBatch() { const value = parseFloat(document.getElementById('batchCommissionValue').value); const performance = parseFloat(document.getElementById('batchPerformanceAmount').value); if (!value && !performance) { alert('请输入提成值或业绩比例'); return; } // [SEC-COMM-BATCH-FILTER] 必须至少勾选 1 项 if (!this.batchSelectedIds || this.batchSelectedIds.size === 0) { alert('请至少勾选一个项目'); return; } const targetType = this.currentTab; const allItems = this.getTargetItems(targetType); // 仅对已勾选的应用 const items = allItems.filter(it => this.batchSelectedIds.has(String(it.id))); const rules = DataManager.getCommissionRules(); items.forEach(item => { const existingRule = rules.find(r => String(r.positionId) === String(this.currentPositionId) && r.targetType === targetType && String(r.targetId) === String(item.id) ); if (existingRule) { const updates = {}; if (value) { updates.commissionType = this.batchCommissionType; updates.commissionValue = value; } if (performance) updates.performanceAmount = performance; // [SEC-COMM-BATCH-FILTER] 顺便同步真实 targetName,自动修复历史脏数据 updates.targetName = item.name; DataManager.updateCommissionRule(existingRule.id, updates); } else { DataManager.addCommissionRule({ positionId: this.currentPositionId, positionName: (this.currentTab === 'service' ? (DataManager.getEmployeePositions().find(p => String(p.id) === String(this.currentPositionId))?.name || '') : '全员'), targetType: targetType, targetId: item.id, targetName: item.name, commissionType: this.batchCommissionType, commissionValue: value || 0, performanceAmount: performance || 0, enabled: false }); } }); this.closeBatchModal(); this.renderList(); alert(`批量设置成功,已应用 ${items.length} 个项目`); }, // ==================== [SEC-COMM-BATCH-FILTER] 批量筛选/勾选辅助方法 ==================== getBatchCategories() { if (this.currentTab === 'service') return DataManager.getServiceCategories(); if (this.currentTab === 'product') return DataManager.getProductCategories(); return []; }, getBatchFilteredItems() { let items = this.getTargetItems(this.currentTab); if (this.batchCurrentCategoryId !== null) { items = items.filter(i => String(i.categoryId) === String(this.batchCurrentCategoryId)); } if (this.batchSearchKeyword) { const kw = this.batchSearchKeyword.toLowerCase(); items = items.filter(i => (i.name || '').toLowerCase().includes(kw)); } return items; }, renderBatchCategoryTabs() { const cats = this.getBatchCategories(); const container = document.getElementById('batchCategoryTabs'); if (!container) return; const baseStyle = 'flex-shrink:0;padding:5px 12px;font-size:12px;color:#666;background:white;border:1px solid #e0d8d0;border-radius:14px;cursor:pointer;'; const activeStyle = 'color:#fff;background:#9B1B5E;border-color:#9B1B5E;font-weight:600;'; const isAll = this.batchCurrentCategoryId === null; let html = `
全部
`; cats.forEach(cat => { const a = String(this.batchCurrentCategoryId) === String(cat.id); html += `
${escapeHtml(cat.name)}
`; }); container.innerHTML = html; }, renderBatchItemList() { const filteredItems = this.getBatchFilteredItems(); const allItems = this.getTargetItems(this.currentTab); const cats = this.getBatchCategories(); const container = document.getElementById('batchItemList'); if (!container) return; // 顶部计数 const countEl = document.getElementById('batchSelectedCount'); if (countEl) countEl.textContent = `已选 ${this.batchSelectedIds ? this.batchSelectedIds.size : 0} / ${allItems.length} 项`; // 全选 checkbox 状态 const allCb = document.getElementById('batchSelectAllCheckbox'); if (allCb) { const allFilteredSelected = filteredItems.length > 0 && filteredItems.every(i => this.batchSelectedIds.has(String(i.id))); allCb.checked = allFilteredSelected; } if (filteredItems.length === 0) { container.innerHTML = '
暂无符合条件的项目
'; return; } let html = ''; filteredItems.forEach(item => { const cat = cats.find(c => String(c.id) === String(item.categoryId)); const checked = this.batchSelectedIds.has(String(item.id)); html += ``; }); container.innerHTML = html; }, onBatchCategorySelect(catId) { this.batchCurrentCategoryId = catId; this.renderBatchCategoryTabs(); this.renderBatchItemList(); }, onBatchSearchInput(value) { this.batchSearchKeyword = (value || '').trim(); this.renderBatchItemList(); }, toggleBatchItem(id) { const sid = String(id); if (this.batchSelectedIds.has(sid)) { this.batchSelectedIds.delete(sid); } else { this.batchSelectedIds.add(sid); } this.renderBatchItemList(); }, toggleSelectAllFiltered() { const items = this.getBatchFilteredItems(); if (items.length === 0) return; const allSelected = items.every(i => this.batchSelectedIds.has(String(i.id))); if (allSelected) { items.forEach(i => this.batchSelectedIds.delete(String(i.id))); } else { items.forEach(i => this.batchSelectedIds.add(String(i.id))); } this.renderBatchItemList(); }, clearAllBatch() { if (this.batchSelectedIds) this.batchSelectedIds.clear(); this.renderBatchItemList(); } }; // ==================== 计次卡管理模块 ==================== const CardModule = { /* [Fix9 20260709] paginated expand */ currentCategoryId: null, deleteTargetId: null, deleteCategoryTargetId: null, editingCategoryId: null, currentCardType: 'count', tempPackageItems: [], showAllCards: false, _visibleCards: 20, _initialized: false, init() { if (this._initialized) return; this._initialized = true; this.bindEvents(); this.refresh(); }, refresh() { this.renderCategoryTabs(); this.renderList(); }, renderCategoryTabs() { const c = DataManager.getCardCategories(); const container = document.getElementById('cardCategoryTabs'); let html = `
全部
`; c.forEach(cat => { html += `
${escapeHtml(cat.name)}
`; }); html += `
添加分类
`; container.innerHTML = html; }, renderList() { const items = DataManager.getCardItems(); const cats = DataManager.getCardCategories(); const container = document.getElementById('cardList'); const emptyState = document.getElementById('cardEmptyState'); let filtered = this.currentCategoryId ? items.filter(i => String(i.categoryId) === String(this.currentCategoryId)) : items; if (this._keyword) { const _kw = String(this._keyword).toLowerCase().trim(); if (_kw) filtered = filtered.filter(i => (i.name||'').toLowerCase().includes(_kw)); } if (filtered.length === 0) { container.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); /* [Fix9 20260709] paginated expand: 20 initial, +20 each */ const __cardLimit = 20; const __cardVisible = Math.max(__cardLimit, this._visibleCards || __cardLimit); const __cardDisplay = filtered.slice(0, __cardVisible); const __cardOlder = Math.max(0, filtered.length - __cardVisible); let html = ''; __cardDisplay.forEach(item => { const cat = cats.find(c => String(c.id) === String(item.categoryId)); const typeClass = item.type === 'count' ? 'type-count' : item.type === 'time' ? 'type-time' : 'type-package'; const typeName = item.type === 'count' ? '次卡' : item.type === 'time' ? '计时卡' : '套餐卡'; html += `
${escapeHtml(item.name)}
¥${item.price}
${typeName}有效期${item.validity}个月
${item.enabled ? '启用' : '禁用'}
`; }); /* [Fix9 20260709] expand/collapse button */ if (__cardOlder > 0) { html += '
'; } else if (__cardVisible > __cardLimit && filtered.length > __cardLimit) { html += '
'; } container.innerHTML = html; }, renderCategoryManageList() { const cats = DataManager.getCardCategories(); const container = document.getElementById('cardCategoryManageList'); if (cats.length === 0) { container.innerHTML = '

暂无分类

'; return; } let html = ''; cats.forEach(cat => { if (String(this.editingCategoryId) === String(cat.id)) { html += `
`; } else { html += `
${escapeHtml(cat.name)}
`; } }); container.innerHTML = html; if (this.editingCategoryId) { const input = document.getElementById(`cardEditCatInput_${this.editingCategoryId}`); if (input) { input.focus(); input.select(); } } }, renderCategoryOptions() { const cats = DataManager.getCardCategories(); const select = document.getElementById('cardCategory'); let html = ''; cats.forEach(cat => { html += ``; }); select.innerHTML = html; }, renderPackageItems() { const container = document.getElementById('packageItemsList'); if (this.tempPackageItems.length === 0) { container.innerHTML = ``; return; } const services = DataManager.getServiceItems(); let html = ''; this.tempPackageItems.forEach((pi, idx) => { const svc = services.find(s => s.id === pi.serviceId); if (svc) { html += `
${escapeHtml(svc.name)}×${pi.count}
`; } }); html += ``; container.innerHTML = html; }, renderServiceSelectList() { const services = DataManager.getServiceItems(); const container = document.getElementById('serviceSelectList'); if (services.length === 0) { container.innerHTML = '

暂无服务项目

'; return; } let html = ''; services.forEach(svc => { const existing = this.tempPackageItems.find(pi => pi.serviceId === svc.id); const checked = existing ? 'checked' : ''; const count = existing ? existing.count : 1; html += `
${escapeHtml(svc.name)}¥${svc.price}
`; }); container.innerHTML = html; }, selectCategory(id) { /* [Fix9 20260709] */ this.currentCategoryId = id; this.showAllCards = false; this._visibleCards = 20; this.renderCategoryTabs(); this.renderList(); }, toggleShowAllCards() { this.showAllCards = !this.showAllCards; this.renderList(); }, selectCardType(type) { this.currentCardType = type; document.querySelectorAll('.card-type-tab').forEach(tab => tab.classList.remove('active')); document.querySelector(`.card-type-tab:nth-child(${type === 'count' ? 1 : type === 'time' ? 2 : 3})`).classList.add('active'); document.getElementById('countConfig').classList.add('hidden'); document.getElementById('timeConfig').classList.add('hidden'); document.getElementById('packageConfig').classList.add('hidden'); if (type === 'count') document.getElementById('countConfig').classList.remove('hidden'); else if (type === 'time') document.getElementById('timeConfig').classList.remove('hidden'); else document.getElementById('packageConfig').classList.remove('hidden'); }, toggleServiceSelect(id) { const checkbox = document.getElementById(`svcSelect_${id}`); const itemDiv = checkbox.closest('.service-select-item'); if (checkbox.checked) { itemDiv.classList.add('selected'); } else { itemDiv.classList.remove('selected'); } }, removePackageItem(idx) { this.tempPackageItems.splice(idx, 1); this.renderPackageItems(); }, openModal(id = null) { this.renderCategoryOptions(); const title = document.getElementById('cardModalTitle'); this.tempPackageItems = []; if (id) { const item = DataManager.getCardItems().find(i => i.id === id); if (item) { title.textContent = '编辑卡项'; document.getElementById('cardId').value = item.id; document.getElementById('cardName').value = item.name; document.getElementById('cardPrice').value = item.price; document.getElementById('cardValidity').value = item.validity; document.getElementById('cardCategory').value = item.categoryId; this.currentCardType = item.type; this.selectCardType(item.type); if (item.type === 'count') document.getElementById('cardCount').value = item.count || 10; else if (item.type === 'time') document.getElementById('cardDuration').value = item.duration || 10; else { this.tempPackageItems = [...(item.packageItems || [])]; this.renderPackageItems(); } } } else { title.textContent = '添加卡项'; document.getElementById('cardForm').reset(); document.getElementById('cardId').value = ''; this.currentCardType = 'count'; this.selectCardType('count'); this.renderPackageItems(); if (this.currentCategoryId) document.getElementById('cardCategory').value = this.currentCategoryId; } document.getElementById('cardModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeModal() { document.getElementById('cardModal').classList.remove('active'); document.body.style.overflow = ''; this.tempPackageItems = []; }, openServiceSelectModal() { this.renderServiceSelectList(); document.getElementById('serviceSelectModal').classList.add('active'); }, closeServiceSelectModal() { document.getElementById('serviceSelectModal').classList.remove('active'); }, confirmServiceSelect() { const services = DataManager.getServiceItems(); services.forEach(svc => { const checkbox = document.getElementById(`svcSelect_${svc.id}`); if (checkbox && checkbox.checked) { const countInput = document.getElementById(`svcCount_${svc.id}`); const count = parseInt(countInput.value) || 1; const existingIdx = this.tempPackageItems.findIndex(pi => pi.serviceId === svc.id); if (existingIdx >= 0) this.tempPackageItems[existingIdx].count = count; else this.tempPackageItems.push({ serviceId: svc.id, count: count }); } else { this.tempPackageItems = this.tempPackageItems.filter(pi => pi.serviceId !== svc.id); } }); this.renderPackageItems(); this.closeServiceSelectModal(); }, toggleEnabled(id) { DataManager.toggleCardItemEnabled(id); this.renderList(); }, openDeleteModal(id) { const __it = DataManager.getCardItems().find(x => String(x.id) === String(id)); const name = __it ? __it.name : ''; this.deleteTargetId = id; document.getElementById('cardDeleteMessage').textContent = `确定删除"${name}"?删除后不可恢复,已售出的卡项不受影响`; document.getElementById('cardDeleteModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeDeleteModal() { document.getElementById('cardDeleteModal').classList.remove('active'); this.deleteTargetId = null; document.body.style.overflow = ''; }, confirmDelete() { if (this.deleteTargetId) { DataManager.deleteCardItem(this.deleteTargetId); this.renderList(); this.closeDeleteModal(); } }, openCategoryManage() { this.editingCategoryId = null; this.hideAddCategoryInput(); this.renderCategoryManageList(); document.getElementById('cardCategoryManageModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeCategoryManage() { document.getElementById('cardCategoryManageModal').classList.remove('active'); document.body.style.overflow = ''; this.renderCategoryTabs(); this.renderCategoryOptions(); const cats = DataManager.getCardCategories(); if (this.currentCategoryId && !cats.find(c => String(c.id) === String(this.currentCategoryId))) { this.currentCategoryId = null; this.renderCategoryTabs(); this.renderList(); } }, showAddCategoryInput() { document.getElementById('cardAddCategoryArea').classList.add('hidden'); document.getElementById('cardNewCategoryInputArea').classList.remove('hidden'); document.getElementById('cardNewCategoryName').value = ''; document.getElementById('cardNewCategoryName').focus(); }, hideAddCategoryInput() { document.getElementById('cardAddCategoryArea').classList.remove('hidden'); document.getElementById('cardNewCategoryInputArea').classList.add('hidden'); }, saveNewCategory() { const name = document.getElementById('cardNewCategoryName').value.trim(); if (!name) { alert('请输入分类名称'); return; } DataManager.addCardCategory(name); this.renderCategoryManageList(); this.hideAddCategoryInput(); this.renderCategoryTabs(); this.renderCategoryOptions(); }, startEditCategory(id) { this.editingCategoryId = id; this.renderCategoryManageList(); }, cancelEditCategory() { this.editingCategoryId = null; this.renderCategoryManageList(); }, saveEditCategory(id) { const input = document.getElementById(`cardEditCatInput_${id}`); const name = input.value.trim(); if (!name) { alert('分类名称不能为空'); return; } DataManager.updateCardCategory(id, name); this.editingCategoryId = null; this.renderCategoryManageList(); this.renderCategoryTabs(); this.renderCategoryOptions(); }, handleEditKeydown(event, id) { if (event.key === 'Enter') this.saveEditCategory(id); else if (event.key === 'Escape') this.cancelEditCategory(); }, /* [Fix9 20260709] expand/collapse methods */ toggleShowAllCards() { const items = DataManager.getCardItems(); let filtered = this.currentCategoryId ? items.filter(i => String(i.categoryId) === String(this.currentCategoryId)) : items; if (this._keyword) { const _kw = String(this._keyword).toLowerCase().trim(); if (_kw) filtered = filtered.filter(i => (i.name||'').toLowerCase().includes(_kw)); } const cur = this._visibleCards || 20; const next = Math.min(filtered.length, cur + 20); this._visibleCards = next; this.showAllCards = next >= filtered.length; this.renderList(); }, collapseItems() { this._visibleCards = 20; this.showAllCards = false; this.renderList(); }, openCategoryDeleteModal(id) { this.deleteCategoryTargetId = id; document.getElementById('cardCategoryDeleteModal').classList.add('active'); }, closeCategoryDeleteModal() { document.getElementById('cardCategoryDeleteModal').classList.remove('active'); this.deleteCategoryTargetId = null; }, confirmCategoryDelete() { if (this.deleteCategoryTargetId) { DataManager.deleteCardCategory(this.deleteCategoryTargetId); this.renderCategoryManageList(); this.closeCategoryDeleteModal(); this.renderCategoryTabs(); this.renderCategoryOptions(); } }, bindEvents() { document.getElementById('cardForm').addEventListener('submit', (e) => { e.preventDefault(); const id = document.getElementById('cardId').value; const name = document.getElementById('cardName').value.trim(); const price = document.getElementById('cardPrice').value; const validity = document.getElementById('cardValidity').value; const categoryId = document.getElementById('cardCategory').value; if (!name || !price || !validity || !categoryId) { alert('请填写完整信息'); return; } let data = { name, price: parseFloat(price), validity: parseInt(validity), categoryId, type: this.currentCardType }; if (this.currentCardType === 'count') { const count = document.getElementById('cardCount').value; if (!count) { alert('请输入次数'); return; } data.count = parseInt(count); data.duration = 0; data.packageItems = []; } else if (this.currentCardType === 'time') { const duration = document.getElementById('cardDuration').value; if (!duration) { alert('请输入时长'); return; } data.duration = parseFloat(duration); data.count = 0; data.packageItems = []; } else { if (this.tempPackageItems.length === 0) { alert('请添加服务项目'); return; } data.packageItems = this.tempPackageItems; data.count = 0; data.duration = 0; } if (id) DataManager.updateCardItem(id, data); else DataManager.addCardItem(data); this.renderList(); this.closeModal(); }); document.getElementById('cardModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeModal(); }); document.getElementById('serviceSelectModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeServiceSelectModal(); }); document.getElementById('cardCategoryManageModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeCategoryManage(); }); document.getElementById('cardDeleteModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeDeleteModal(); }); document.getElementById('cardCategoryDeleteModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeCategoryDeleteModal(); }); } }; // ==================== 员工管理模块 ==================== const EmployeeModule = { currentPositionId: null, deleteTargetId: null, deletePositionTargetId: null, deleteLevelTargetId: null, editingPositionId: null, editingLevelId: null, _initialized: false, init() { if (this._initialized) return; this._initialized = true; this.bindEvents(); this.refresh(); }, refresh() { this.renderPositionTabs(); this.renderList(); }, getPositionClass(positionName) { const name = positionName.toLowerCase(); if (name.includes('发型师') || name.includes('理发')) return 'pos-hairdresser'; if (name.includes('技师')) return 'pos-technician'; if (name.includes('助理')) return 'pos-assistant'; if (name.includes('前台')) return 'pos-receptionist'; if (name.includes('店长') || name.includes('经理')) return 'pos-manager'; return 'pos-hairdresser'; }, renderPositionTabs() { const positions = DataManager.getEmployeePositions(); const container = document.getElementById('employeePositionTabs'); let html = `
全部
`; positions.forEach(pos => { html += `
${escapeHtml(pos.name)}
`; }); container.innerHTML = html; }, renderList() { const items = DataManager.getEmployeeItems(); const positions = DataManager.getEmployeePositions(); const levels = DataManager.getEmployeeLevels(); const container = document.getElementById('employeeList'); const emptyState = document.getElementById('employeeEmptyState'); let filtered = this.currentPositionId ? items.filter(i => String(i.positionId) === String(this.currentPositionId)) : items; if (this._keyword) { const _kw = String(this._keyword).toLowerCase().trim(); if (_kw) filtered = filtered.filter(i => ((i.name||'').toLowerCase().includes(_kw)) || ((i.phone||'').includes(_kw))); } if (filtered.length === 0) { container.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); let html = ''; filtered.forEach(item => { const pos = positions.find(p => String(p.id) === String(item.positionId)); const lvl = levels.find(l => String(l.id) === String(item.levelId)); const posClass = pos ? this.getPositionClass(pos.name) : 'pos-hairdresser'; const statusClass = item.status === 'active' ? 'active' : 'inactive'; const statusText = item.status === 'active' ? '在职' : '离职'; html += `
${escapeHtml(item.name)}
${escapeHtml(pos ? pos.name : '未设置')}${escapeHtml(lvl ? lvl.name : '未设置')}
${escapeHtml(maskPhone(item.phone))}
${statusText}
`; }); container.innerHTML = html; }, renderPositionManageList() { const positions = DataManager.getEmployeePositions(); const container = document.getElementById('positionManageList'); if (positions.length === 0) { container.innerHTML = '

暂无职位

'; return; } let html = ''; positions.forEach(pos => { if (String(this.editingPositionId) === String(pos.id)) { html += `
`; } else { html += `
${escapeHtml(pos.name)}
`; } }); container.innerHTML = html; if (this.editingPositionId) { const input = document.getElementById(`positionEditInput_${this.editingPositionId}`); if (input) { input.focus(); input.select(); } } }, renderLevelManageList() { const levels = DataManager.getEmployeeLevels(); const container = document.getElementById('levelManageList'); if (levels.length === 0) { container.innerHTML = '

暂无职级

'; return; } let html = ''; levels.forEach(lvl => { if (String(this.editingLevelId) === String(lvl.id)) { html += `
`; } else { html += `
${escapeHtml(lvl.name)}
`; } }); container.innerHTML = html; if (this.editingLevelId) { const input = document.getElementById(`levelEditInput_${this.editingLevelId}`); if (input) { input.focus(); input.select(); } } }, renderPositionOptions() { const positions = DataManager.getEmployeePositions(); const select = document.getElementById('employeePosition'); let html = ''; positions.forEach(pos => { html += ``; }); select.innerHTML = html; }, renderLevelOptions() { const levels = DataManager.getEmployeeLevels(); const select = document.getElementById('employeeLevel'); let html = ''; levels.forEach(lvl => { html += ``; }); select.innerHTML = html; }, selectPosition(id) { this.currentPositionId = id; this.renderPositionTabs(); this.renderList(); }, openModal(id = null) { this.renderPositionOptions(); this.renderLevelOptions(); const title = document.getElementById('employeeModalTitle'); if (id) { const item = DataManager.getEmployeeItems().find(i => i.id === id); if (item) { title.textContent = '编辑员工'; document.getElementById('employeeId').value = item.id; document.getElementById('employeeName').value = item.name; document.getElementById('employeePhone').value = item.phone; document.getElementById('employeePosition').value = item.positionId; document.getElementById('employeeLevel').value = item.levelId; document.getElementById('employeeJoinDate').value = item.joinDate; document.querySelector(`input[name="employeeStatus"][value="${item.status}"]`).checked = true; } } else { title.textContent = '添加员工'; document.getElementById('employeeForm').reset(); document.getElementById('employeeId').value = ''; document.querySelector('input[name="employeeStatus"][value="active"]').checked = true; if (this.currentPositionId) document.getElementById('employeePosition').value = this.currentPositionId; } document.getElementById('employeeModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeModal() { document.getElementById('employeeModal').classList.remove('active'); document.body.style.overflow = ''; }, openDeleteModal(id) { const __it = DataManager.getEmployeeItems().find(x => String(x.id) === String(id)); const name = __it ? __it.name : ''; this.deleteTargetId = id; document.getElementById('employeeDeleteMessage').textContent = `确定删除"${name}"?删除后不可恢复,该员工的历史业绩数据将保留`; document.getElementById('employeeDeleteModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeDeleteModal() { document.getElementById('employeeDeleteModal').classList.remove('active'); this.deleteTargetId = null; document.body.style.overflow = ''; }, confirmDelete() { if (this.deleteTargetId) { DataManager.deleteEmployeeItem(this.deleteTargetId); this.renderList(); this.closeDeleteModal(); } }, openPositionManage() { this.editingPositionId = null; this.hideAddPositionInput(); this.renderPositionManageList(); document.getElementById('positionManageModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closePositionManage() { document.getElementById('positionManageModal').classList.remove('active'); document.body.style.overflow = ''; this.renderPositionTabs(); this.renderPositionOptions(); const positions = DataManager.getEmployeePositions(); if (this.currentPositionId && !positions.find(p => String(p.id) === String(this.currentPositionId))) { this.currentPositionId = null; this.renderPositionTabs(); this.renderList(); } }, showAddPositionInput() { document.getElementById('positionAddArea').classList.add('hidden'); document.getElementById('positionNewInputArea').classList.remove('hidden'); document.getElementById('positionNewName').value = ''; document.getElementById('positionNewName').focus(); }, hideAddPositionInput() { document.getElementById('positionAddArea').classList.remove('hidden'); document.getElementById('positionNewInputArea').classList.add('hidden'); }, saveNewPosition() { const name = document.getElementById('positionNewName').value.trim(); if (!name) { alert('请输入职位名称'); return; } DataManager.addEmployeePosition(name); this.renderPositionManageList(); this.hideAddPositionInput(); this.renderPositionTabs(); this.renderPositionOptions(); this.renderList(); }, startEditPosition(id) { this.editingPositionId = id; this.renderPositionManageList(); }, cancelEditPosition() { this.editingPositionId = null; this.renderPositionManageList(); }, saveEditPosition(id) { const input = document.getElementById(`positionEditInput_${id}`); const name = input.value.trim(); if (!name) { alert('职位名称不能为空'); return; } DataManager.updateEmployeePosition(id, name); this.editingPositionId = null; this.renderPositionManageList(); this.renderPositionTabs(); this.renderPositionOptions(); }, handlePositionEditKeydown(event, id) { if (event.key === 'Enter') this.saveEditPosition(id); else if (event.key === 'Escape') this.cancelEditPosition(); }, openPositionDeleteModal(id) { this.deletePositionTargetId = id; document.getElementById('positionDeleteModal').classList.add('active'); }, closePositionDeleteModal() { document.getElementById('positionDeleteModal').classList.remove('active'); this.deletePositionTargetId = null; }, confirmPositionDelete() { if (this.deletePositionTargetId) { DataManager.deleteEmployeePosition(this.deletePositionTargetId); this.renderPositionManageList(); this.closePositionDeleteModal(); this.renderPositionTabs(); this.renderPositionOptions(); this.renderList(); } }, openLevelManage() { this.editingLevelId = null; this.hideAddLevelInput(); this.renderLevelManageList(); document.getElementById('levelManageModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeLevelManage() { document.getElementById('levelManageModal').classList.remove('active'); document.body.style.overflow = ''; this.renderLevelOptions(); }, showAddLevelInput() { document.getElementById('levelAddArea').classList.add('hidden'); document.getElementById('levelNewInputArea').classList.remove('hidden'); document.getElementById('levelNewName').value = ''; document.getElementById('levelNewName').focus(); }, hideAddLevelInput() { document.getElementById('levelAddArea').classList.remove('hidden'); document.getElementById('levelNewInputArea').classList.add('hidden'); }, saveNewLevel() { const name = document.getElementById('levelNewName').value.trim(); if (!name) { alert('请输入职级名称'); return; } DataManager.addEmployeeLevel(name); this.renderLevelManageList(); this.hideAddLevelInput(); this.renderLevelOptions(); this.renderList(); }, startEditLevel(id) { this.editingLevelId = id; this.renderLevelManageList(); }, cancelEditLevel() { this.editingLevelId = null; this.renderLevelManageList(); }, saveEditLevel(id) { const input = document.getElementById(`levelEditInput_${id}`); const name = input.value.trim(); if (!name) { alert('职级名称不能为空'); return; } DataManager.updateEmployeeLevel(id, name); this.editingLevelId = null; this.renderLevelManageList(); this.renderLevelOptions(); }, handleLevelEditKeydown(event, id) { if (event.key === 'Enter') this.saveEditLevel(id); else if (event.key === 'Escape') this.cancelEditLevel(); }, openLevelDeleteModal(id) { this.deleteLevelTargetId = id; document.getElementById('levelDeleteModal').classList.add('active'); }, closeLevelDeleteModal() { document.getElementById('levelDeleteModal').classList.remove('active'); this.deleteLevelTargetId = null; }, confirmLevelDelete() { if (this.deleteLevelTargetId) { DataManager.deleteEmployeeLevel(this.deleteLevelTargetId); this.renderLevelManageList(); this.closeLevelDeleteModal(); this.renderLevelOptions(); this.renderList(); } }, bindEvents() { document.getElementById('employeeForm').addEventListener('submit', (e) => { e.preventDefault(); const id = document.getElementById('employeeId').value; const name = document.getElementById('employeeName').value.trim(); const phone = document.getElementById('employeePhone').value.trim(); const positionId = document.getElementById('employeePosition').value; const levelId = document.getElementById('employeeLevel').value; const joinDate = document.getElementById('employeeJoinDate').value; const status = document.querySelector('input[name="employeeStatus"]:checked').value; if (!name || !phone || !positionId || !levelId || !joinDate) { alert('请填写完整信息'); return; } const data = { name, phone, positionId, levelId, joinDate, status }; if (id) DataManager.updateEmployeeItem(id, data); else DataManager.addEmployeeItem(data); this.renderList(); this.closeModal(); }); document.getElementById('employeeModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeModal(); }); document.getElementById('positionManageModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closePositionManage(); }); document.getElementById('levelManageModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeLevelManage(); }); document.getElementById('employeeDeleteModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeDeleteModal(); }); document.getElementById('positionDeleteModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closePositionDeleteModal(); }); document.getElementById('levelDeleteModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeLevelDeleteModal(); }); } }; // ==================== 会员卡类型管理模块 ==================== const MemberCardModule = { deleteTargetId: null, init() { this.renderList(); this.bindEvents(); }, refresh() { this.renderList(); }, renderList() { const _rawItems = DataManager.getMemberCardItems(); const items = this._keyword ? _rawItems.filter(i => (i.name||'').toLowerCase().includes(String(this._keyword).toLowerCase().trim())) : _rawItems; const container = document.getElementById('memberCardList'); const emptyState = document.getElementById('memberCardEmptyState'); if (items.length === 0) { container.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); let html = ''; items.forEach(item => { const serviceDiscountText = Math.round(item.serviceDiscount / 10) + '折'; const productDiscountText = Math.round(item.productDiscount / 10) + '折'; html += `
${escapeHtml(item.name)}
储值¥${item.storedAmount}
服务${serviceDiscountText} 产品${productDiscountText}
已开${item.openedCount}张
`; }); container.innerHTML = html; }, openModal(id = null) { const modal = document.getElementById('memberCardModal'); const title = document.getElementById('memberCardModalTitle'); const form = document.getElementById('memberCardForm'); form.reset(); if (id) { title.textContent = '编辑会员卡类型'; const item = DataManager.getMemberCardItems().find(x => x.id === id); if (item) { document.getElementById('memberCardId').value = id; document.getElementById('memberCardName').value = item.name; document.getElementById('memberCardStoredAmount').value = item.storedAmount; document.getElementById('memberCardBonusAmount').value = item.bonusAmount; document.getElementById('memberCardBonusRate').value = item.bonusRate || 0; // 设置赠送模式 radio const _mode = item.bonusMode === 'percent' ? 'percent' : 'fixed'; const _radios = document.getElementsByName('memberCardBonusMode'); for (const r of _radios) { r.checked = (r.value === _mode); } this.onBonusModeChange(); document.getElementById('memberCardTotalAmount').value = item.totalAmount; document.getElementById('memberCardServiceDiscount').value = item.serviceDiscount; document.getElementById('memberCardProductDiscount').value = item.productDiscount; document.getElementById('memberCardUpgradeThreshold').value = item.upgradeThreshold || ''; } } else { title.textContent = '添加会员卡类型'; document.getElementById('memberCardId').value = ''; document.getElementById('memberCardUpgradeThreshold').value = ''; // 默认 fixed 模式 const _radios = document.getElementsByName('memberCardBonusMode'); for (const r of _radios) { r.checked = (r.value === 'fixed'); } const _rateInput = document.getElementById('memberCardBonusRate'); if (_rateInput) _rateInput.value = ''; this.onBonusModeChange(); } modal.classList.add('active'); document.body.style.overflow = 'hidden'; }, closeModal() { document.getElementById('memberCardModal').classList.remove('active'); document.body.style.overflow = ''; }, onBonusModeChange() { const checked = document.querySelector('input[name="memberCardBonusMode"]:checked'); const mode = checked ? checked.value : 'fixed'; document.getElementById('bonusFixedWrap').style.display = (mode === 'fixed') ? '' : 'none'; document.getElementById('bonusPercentWrap').style.display = (mode === 'percent') ? '' : 'none'; // 同步高亮选中态 const fixedLabel = document.getElementById('bonusModeFixedLabel'); const percentLabel = document.getElementById('bonusModePercentLabel'); if (fixedLabel) fixedLabel.style.background = (mode === 'fixed') ? '#fdf2f8' : ''; if (fixedLabel) fixedLabel.style.borderColor = (mode === 'fixed') ? '#9B1B5E' : '#ddd'; if (percentLabel) percentLabel.style.background = (mode === 'percent') ? '#fdf2f8' : ''; if (percentLabel) percentLabel.style.borderColor = (mode === 'percent') ? '#9B1B5E' : '#ddd'; this.calculateTotal(); }, calculateTotal() { const stored = parseFloat(document.getElementById('memberCardStoredAmount').value) || 0; const checked = document.querySelector('input[name="memberCardBonusMode"]:checked'); const mode = checked ? checked.value : 'fixed'; let bonus = 0; if (mode === 'percent') { const rate = parseFloat(document.getElementById('memberCardBonusRate').value) || 0; bonus = Math.floor(stored * rate / 100); } else { bonus = parseFloat(document.getElementById('memberCardBonusAmount').value) || 0; } document.getElementById('memberCardTotalAmount').value = stored + bonus; }, openDeleteModal(id) { this.deleteTargetId = id; document.getElementById('memberCardDeleteModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeDeleteModal() { document.getElementById('memberCardDeleteModal').classList.remove('active'); document.body.style.overflow = ''; this.deleteTargetId = null; }, confirmDelete() { if (this.deleteTargetId) { DataManager.deleteMemberCardItem(this.deleteTargetId); this.renderList(); this.closeDeleteModal(); } }, bindEvents() { document.getElementById('memberCardForm').addEventListener('submit', (e) => { e.preventDefault(); const id = document.getElementById('memberCardId').value; const name = document.getElementById('memberCardName').value.trim(); const storedAmount = parseFloat(document.getElementById('memberCardStoredAmount').value) || 0; const _modeRadio = document.querySelector('input[name="memberCardBonusMode"]:checked'); const bonusMode = _modeRadio && _modeRadio.value === 'percent' ? 'percent' : 'fixed'; const bonusRate = bonusMode === 'percent' ? Math.max(0, Math.min(100, parseFloat(document.getElementById('memberCardBonusRate').value) || 0)) : 0; const bonusAmount = bonusMode === 'fixed' ? (parseFloat(document.getElementById('memberCardBonusAmount').value) || 0) : Math.floor(storedAmount * bonusRate / 100); const totalAmount = storedAmount + bonusAmount; const serviceDiscount = parseFloat(document.getElementById('memberCardServiceDiscount').value) || 100; const productDiscount = parseFloat(document.getElementById('memberCardProductDiscount').value) || 100; if (!name || storedAmount <= 0) { alert('请填写完整信息'); return; } if (bonusMode === 'percent' && bonusRate <= 0) { alert('请填写赠送比例'); return; } const data = { name, storedAmount, bonusAmount, bonusMode, bonusRate, totalAmount, serviceDiscount, productDiscount, upgradeThreshold: parseFloat(document.getElementById('memberCardUpgradeThreshold').value) || 0 }; if (id) DataManager.updateMemberCardItem(id, data); else DataManager.addMemberCardItem(data); this.renderList(); this.closeModal(); }); document.getElementById('memberCardModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeModal(); }); document.getElementById('memberCardDeleteModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeDeleteModal(); }); } }; // ==================== 会员管理模块 ==================== const MemberModule = { members: [], currentFilter: 'all', currentDetailId: null, currentDetailTab: 'consume', recordOrderDesc: true, // true=倒序(最新在前),false=正序 // [Fix7 20260709] 到期会员集合 & 本月新增数签名缓存(避免每次切菜单遍历 36471 张卡 / 4910 会员) _expireCache: { sig: null, ids: null }, _newCountCache: { sig: null, month: null, count: 0 }, rechargeSelectedEmployees: [{}], // 充值页选中的员工 [{employeeId}] currentReceiptData: null, // 当前小票数据 usePointsDeduct: false, // 是否使用积分抵扣 currentTagFilter: '', // 标签筛选 init() { this.members = DataManager.getMembers(); this.renderList(); this.bindEvents(); }, // 获取会员等级标签 (M3-3d: 直接显示卡类型名称, 按金额高低配色) getLevelTag(cardType) { if (!cardType) return { text: '普通', class: 'level-tag-gray' }; const cardTypes = DataManager.getMemberCardItems(); const card = cardTypes.find(c => c.name === cardType); if (!card) { // 卡类型已被删除: 仍显示原名称, 但用灰色标识异常 return { text: cardType, class: 'level-tag-gray' }; } const amt = parseFloat(card.storedAmount) || 0; let cls = 'level-tag-theme'; if (amt >= 3000) cls = 'level-tag-gold'; else if (amt >= 1000) cls = 'level-tag-purple'; else if (amt >= 500) cls = 'level-tag-silver'; return { text: card.name, class: cls }; }, // 脱敏手机号 maskPhone(phone) { if (!phone || phone.length < 11) return phone; return phone.substring(0, 3) + '****' + phone.substring(7); }, // 渲染会员列表 renderList() { this.members = DataManager.getMembers(); let filtered = this.members; const now = new Date(); const thisMonth = now.getMonth(); const thisYear = now.getFullYear(); // 计算即将到期的会员ID(统一计算,用于筛选和计数) const allHeldCards = DataManager.getAllMemberHeldCards ? DataManager.getAllMemberHeldCards() : (JSON.parse(localStorage.getItem('salon_member_held_cards') || '[]')); // [SEC-LS-CLEANUP P1] salon_expire_remind_settings 是死键 (无写入), 改为常量 7 天 const expireSettings = { enabled: true, days: 7 }; const expireDays = expireSettings.days || 7; // [Fix7 20260709] 到期会员集合带签名缓存:签名 = held cards raw sig + 当日日期 + expireDays // 免除每次切换菜单时的 36471 次 forEach + new Date 计算 const __todayStr = now.toISOString().slice(0, 10); const __expSig = (DataManager._cachedHeldCardsSig || ('nosig_' + allHeldCards.length)) + '|' + __todayStr + '|' + expireDays; let expireMemberIds; if (this._expireCache && this._expireCache.sig === __expSig && this._expireCache.ids) { expireMemberIds = this._expireCache.ids; } else { expireMemberIds = new Set(); const __nowMs = now.getTime(); const __dayMs = 86400000; for (let i = 0, len = allHeldCards.length; i < len; i++) { const card = allHeldCards[i]; if (card.status !== 'active') continue; if (card.expireDate) { const diffDays = Math.ceil((new Date(card.expireDate).getTime() - __nowMs) / __dayMs); if (diffDays <= expireDays) { expireMemberIds.add(String(card.memberId)); continue; } } if (card.cardType === 'count' && (card.remainingCount || 0) <= 2) { expireMemberIds.add(String(card.memberId)); continue; } if (card.cardType === 'time' && (card.remainingHours || 0) <= 2) { expireMemberIds.add(String(card.memberId)); continue; } if (card.cardType === 'package') { const pkg = card.packageItems || []; let rem = 0; for (let j = 0; j < pkg.length; j++) if ((pkg[j].remainingCount || 0) > 0) rem++; if (rem <= 3) expireMemberIds.add(String(card.memberId)); // [FIX_EXPIRE_ID_TYPE_20260709] } } this._expireCache = { sig: __expSig, ids: expireMemberIds }; } if (this.currentFilter === 'new') { filtered = this.members.filter(m => { const d = new Date(m.createDate); return d.getMonth() === thisMonth && d.getFullYear() === thisYear; }); } else if (this.currentFilter === 'expire') { filtered = this.members.filter(m => expireMemberIds.has(String(m.id))); // [FIX_EXPIRE_ID_TYPE_20260709] 统一 String 比较,配合上面 add(String()) 修 memberId number/string 类型混淆 } // 标签筛选 if (this.currentTagFilter) { filtered = filtered.filter(m => (m.tags || []).includes(this.currentTagFilter)); } const listEl = document.getElementById('memberList'); // [Fix7 20260709] counts.new 缓存:签名 = members 长度 + 首尾 id + 当月 const __mLen = this.members.length; const __mSig = __mLen + '|' + (__mLen > 0 ? (this.members[0].id || '') : '') + '|' + (__mLen > 0 ? (this.members[__mLen - 1].id || '') : ''); const __ymKey = thisYear + '-' + thisMonth; let __newCount; if (this._newCountCache && this._newCountCache.sig === __mSig && this._newCountCache.month === __ymKey) { __newCount = this._newCountCache.count; } else { __newCount = 0; for (let i = 0; i < __mLen; i++) { const d = new Date(this.members[i].createDate || this.members[i].createdAt); if (d.getMonth() === thisMonth && d.getFullYear() === thisYear) __newCount++; } this._newCountCache = { sig: __mSig, month: __ymKey, count: __newCount }; } const counts = { all: __mLen, new: __newCount, expire: expireMemberIds.size }; // 更新筛选标签数量 document.getElementById('memberAllCount').textContent = counts.all; document.getElementById('memberNewCount').textContent = counts.new; document.getElementById('memberExpireCount').textContent = counts.expire; // 更新标签筛选下拉选项 this.updateTagFilterOptions(); if (filtered.length === 0) { listEl.innerHTML = '
暂无会员数据
'; return; } // 获取所有标签用于显示 const allTags = DataManager.getMemberTags(); // [Fix8 20260709] paginated expand: 按 _visibleMembers 分批渲染 const __MEMBER_DEFAULT_LIMIT = 10; this.__lastFilteredMembersLen = filtered.length; if (typeof this._visibleMembers !== 'number') this._visibleMembers = __MEMBER_DEFAULT_LIMIT; if (typeof this._MEMBER_BATCH !== 'number') this._MEMBER_BATCH = 50; const __visibleMN = Math.max(__MEMBER_DEFAULT_LIMIT, this._visibleMembers); const __memberDisplayList = filtered.slice(0, __visibleMN); const __memberOlderCount = Math.max(0, filtered.length - __visibleMN); const __memberNextBatch = Math.min(__memberOlderCount, this._MEMBER_BATCH); const __showAllMembers = __visibleMN > __MEMBER_DEFAULT_LIMIT; let __memberListHtml = __memberDisplayList.map((m, index) => { const level = this.getLevelTag(m.cardType); const levelClass = level.text === '金卡' ? '' : level.text === '银卡' ? 'silver' : 'normal'; // 生成标签HTML const memberTags = (m.tags || []).map(tagId => { const tag = allTags.find(t => t.id === tagId); if (!tag) return ''; return '' + tag.name + ''; }).join(''); return `
${escapeHtml(m.name.charAt(0))}
${index + 1}
${escapeHtml(m.name)} ${escapeHtml(level.text)}
${escapeHtml(this.maskPhone(m.phone))}
${memberTags ? '
' + memberTags + '
' : ''}
¥${m.balance.toLocaleString()}
`; }).join(''); // [Fix8 20260709] paginated expand: 展开下一批 + 收起(已展开时) if (__memberOlderCount > 0) { __memberListHtml += `
${__showAllMembers ? `` : ''}
`; } else if (__showAllMembers) { __memberListHtml += `
`; } listEl.innerHTML = __memberListHtml; }, // [Fix8 20260709] paginated expand: 每次追加 50 位(原一次性全展被替换,保留函数名兼容 onclick) toggleShowAllMembers() { const total = this.__lastFilteredMembersLen || 0; const cur = this._visibleMembers || 10; const next = Math.min(cur + (this._MEMBER_BATCH || 50), total || (cur + 50)); this._visibleMembers = next; this.showAllMembers = next > 10; this.renderList(); }, // [Fix8 20260709] paginated expand: 独立收起入口 collapseMembers() { this._visibleMembers = 10; this.showAllMembers = false; this.renderList(); }, // 打开添加会员弹窗 openAddModal() { document.getElementById('memberAddModal').classList.add('active'); document.body.style.overflow = 'hidden'; // 填充会员卡类型下拉 const select = document.getElementById('newMemberCardType'); const cardTypes = DataManager.getMemberCardItems(); select.innerHTML = '' + cardTypes.map(c => ``).join(''); // 清空表单 document.getElementById('newMemberName').value = ''; document.getElementById('newMemberPhone').value = ''; document.getElementById('newMemberCardType').value = ''; document.getElementById('newMemberRechargeAmount').value = ''; document.getElementById('newMemberRemark').value = ''; document.getElementById('newMemberPaymentSection').style.display = 'none'; document.getElementById('addMemberCommissionArea').style.display = 'none'; this.newMemberPaymentMethod = ''; this.addMemberSelectedEmployees = [{}]; document.querySelectorAll('#newMemberPaymentSection .pay-method-tag').forEach(t => t.classList.remove('selected')); }, // ==================== SEC-R3: 智能批量导入向导 v2 ==================== _importState: null, async openImportModal() { this._importState = { step: 1, file: null, rawRows: [], headers: [], mapping: {}, cardNames: [], cardTemplates: [], cardMatch: {}, onDuplicate: 'skip', dryRunResult: null, dryRunToken: null, commitResult: null, _lastPayload: null, expandedColumns: { hasCards: false }, aiProvider: 'deepseek', aiState: { step2: { busy: false, source: null, message: null }, step3: { busy: false, source: null, message: null } } }; document.getElementById('memberImportModal').classList.add('active'); document.body.style.overflow = 'hidden'; // 重置 step1 UI document.getElementById('memberImportFile').value = ''; document.getElementById('memberImportFileName').textContent = '点击选择 Excel 文件'; document.getElementById('importFileInfo').textContent = ''; this._renderImportStep(1); // 预加载卡模板 try { const data = await ApiManager.get('/card-templates'); this._importState.cardTemplates = (data && data.cards) || []; } catch (e) { this._importState.cardTemplates = []; } }, closeImportModal() { document.getElementById('memberImportModal').classList.remove('active'); document.body.style.overflow = ''; this._importState = null; }, _renderImportStep(step) { this._importState.step = step; for (let i = 1; i <= 5; i++) { const pane = document.getElementById('importStep' + i); if (pane) pane.classList.toggle('active', i === step); } document.querySelectorAll('#importStepBar .r3-step').forEach(el => { const s = parseInt(el.dataset.step, 10); el.classList.toggle('active', s === step); el.classList.toggle('done', s < step); }); const prevBtn = document.getElementById('importPrevBtn'); const nextBtn = document.getElementById('importNextBtn'); prevBtn.style.display = (step > 1 && step < 5) ? 'block' : 'none'; nextBtn.disabled = false; nextBtn.style.opacity = ''; if (step === 1) nextBtn.textContent = '下一步'; else if (step === 2) nextBtn.textContent = '下一步'; else if (step === 3) nextBtn.textContent = '生成预览'; else if (step === 4) nextBtn.textContent = '确认导入'; else if (step === 5) nextBtn.textContent = '完成'; }, onImportFileChange(input) { const f = input.files && input.files[0]; if (!f) return; document.getElementById('memberImportFileName').textContent = f.name; document.getElementById('importFileInfo').textContent = '正在解析...'; const reader = new FileReader(); reader.onload = (ev) => { try { const wb = XLSX.read(ev.target.result, { type: 'array', cellDates: true }); if (!wb.SheetNames.length) { alert('Excel 无工作表'); return; } const sheet = wb.Sheets[wb.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(sheet, { raw: false, defval: '', blankrows: false }); if (!rows.length) { alert('Excel 内没有数据'); return; } if (rows.length > 1000) { alert('单次最多 1000 行,当前 ' + rows.length + ' 行'); return; } this._importState.file = f; this._importState.rawRows = rows; this._importState.headers = Object.keys(rows[0]); this._importState.mapping = this._autoGuessMapping(this._importState.headers); document.getElementById('importFileInfo').innerHTML = `✅ ${f.name}
${rows.length} 行 · ${this._importState.headers.length} 列 · 已自动猜测 ${Object.keys(this._importState.mapping).length} 个字段映射`; } catch (e) { alert('Excel 解析失败: ' + (e.message || e)); document.getElementById('importFileInfo').textContent = ''; } }; reader.readAsArrayBuffer(f); }, _autoGuessMapping(headers) { const dict = { name: ['姓名','会员名','名字','客户名','顾客姓名','客户'], phone: ['手机号','电话','手机','联系电话','号码','phone','mobile'], gender: ['性别','sex','gender'], birthday: ['生日','出生日期','生日日期','birth'], balance: ['余额','储值','储值余额','卡余额','卡内余额','现有余额'], points: ['积分','积分余额','可用积分'], remark: ['备注','备注说明','说明','remark','note'], cardName: ['项目','卡名','卡名称','服务项目','卡券名','次卡名称','card_name','购买项目','项目名称'], remainingTimes: ['剩余次数','次数','剩余','余次','剩余可用次数','可用次数'], remainingHours: ['剩余时长','剩余小时','剩余时间'], expireDate: ['到期日','到期日期','有效期','失效日期','expire','到期时间'], cardPrice: ['卡金额','金额','购卡金额','card_price','原价','卡价格'] }; const m = {}; for (const h of headers) { const norm = String(h).trim().toLowerCase().replace(/\s+/g, ''); for (const [field, aliases] of Object.entries(dict)) { if (aliases.some(a => String(a).toLowerCase().replace(/\s+/g, '') === norm)) { m[h] = field; break; } } } return m; }, _renderStep2() { const fields = [ { value: '', label: '— 忽略 —' }, { value: 'name', label: '姓名 *' }, { value: 'phone', label: '手机号 *' }, { value: 'gender', label: '性别' }, { value: 'birthday', label: '生日' }, { value: 'balance', label: '余额(储值)' }, { value: 'points', label: '积分' }, { value: 'remark', label: '备注' }, { value: 'cardName', label: '卡名(持卡)' }, { value: 'remainingTimes', label: '剩余次数' }, { value: 'remainingHours', label: '剩余时长' }, { value: 'expireDate', label: '到期日' }, { value: 'cardPrice', label: '卡价格' } ]; const ai = this._importState.aiState.step2; const prov = this._importState.aiProvider; let badge = ''; if (ai.busy) { badge = '🤖 AI 推荐中...'; } else if (ai.source === 'ai') { badge = '✨ AI 已推荐'; } else if (ai.source === 'fallback') { badge = `⚠ 规则兜底 (AI 不可用)`; } let html = `
AI 助手: ${badge}
请确认 Excel 表头对应到系统字段(已自动猜测,可修改):姓名、手机号为必填
`; for (const h of this._importState.headers) { const cur = this._importState.mapping[h] || ''; const sample = String(this._importState.rawRows[0][h] || '').slice(0, 20); const hEsc = h.replace(/'/g, "\\'").replace(/"/g, '"'); html += ``; } html += '
Excel 表头第 1 行样本系统字段
${this._escHtml(h)} ${this._escHtml(sample)}
'; document.getElementById('importStep2Body').innerHTML = html; }, _escHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); }, _setMapping(header, value) { this._importState.mapping[header] = value; }, _validateStep2() { const m = this._importState.mapping; const used = Object.values(m).filter(v => v); for (const req of ['name', 'phone']) { if (!used.includes(req)) { alert('必须映射字段: ' + (req === 'name' ? '姓名' : '手机号')); return false; } } const counts = {}; used.forEach(v => { counts[v] = (counts[v] || 0) + 1; }); const dup = Object.entries(counts).filter(([_, c]) => c > 1).map(([k]) => k); if (dup.length > 0) { alert('以下字段被映射了多次: ' + dup.join(', ')); return false; } this._importState.expandedColumns.hasCards = used.includes('cardName'); return true; }, _renderStep3() { const body = document.getElementById('importStep3Body'); if (!this._importState.expandedColumns.hasCards) { body.innerHTML = '
未识别到卡名列,本步骤跳过
'; this._importState.cardMatch = {}; this._importState.cardNames = []; return; } const cardHeader = Object.entries(this._importState.mapping).find(([_, v]) => v === 'cardName')[0]; const cardNames = new Set(); for (const r of this._importState.rawRows) { const cn = String(r[cardHeader] || '').trim(); if (cn) cardNames.add(cn); } this._importState.cardNames = Array.from(cardNames); const templates = this._importState.cardTemplates; if (!templates.length) { body.innerHTML = '
⚠ 系统中暂无卡模板,请先到「卡券管理」创建。
'; return; } // AI 工具栏 const ai = this._importState.aiState.step3; const prov = this._importState.aiProvider; let badge = ''; if (ai.busy) { badge = '🤖 AI 推荐中...'; } else if (ai.source === 'ai') { badge = '✨ AI 已推荐'; } else if (ai.source === 'fallback') { badge = `⚠ 规则兜底 (AI 不可用)`; } let html = `
AI 助手: ${badge}
为 Excel 中的每个卡名指定系统卡模板(已自动模糊匹配)
`; for (const cn of this._importState.cardNames) { const guess = this._guessCardTemplate(cn, templates); if (guess && !this._importState.cardMatch[cn]) this._importState.cardMatch[cn] = guess.id; const curId = this._importState.cardMatch[cn] || ''; const cnEsc = cn.replace(/'/g, "\\'"); html += ``; } html += '
Excel 卡名系统模板
${this._escHtml(cn)}
'; body.innerHTML = html; }, _guessCardTemplate(cardName, templates) { const cn = String(cardName || '').replace(/\s+/g, '').toLowerCase(); if (!cn) return null; let m = templates.find(t => t.name && String(t.name).replace(/\s+/g, '').toLowerCase() === cn); if (m) return m; m = templates.find(t => { const tn = String(t.name || '').replace(/\s+/g, '').toLowerCase(); return tn && (cn.includes(tn) || tn.includes(cn)); }); if (m) return m; const words = cn.match(/[\u4e00-\u9fa5]{2,}/g) || []; if (!words.length) return null; let best = null, bestScore = 0; for (const t of templates) { const tn = String(t.name || '').toLowerCase(); let score = 0; for (const w of words) { if (tn.includes(w)) score++; } if (score > bestScore) { best = t; bestScore = score; } } return bestScore > 0 ? best : null; }, _setCardMatch(cardName, tplId) { if (tplId) this._importState.cardMatch[cardName] = parseInt(tplId, 10); else delete this._importState.cardMatch[cardName]; }, _setAiProvider(provider, step) { this._importState.aiProvider = (provider === 'doubao') ? 'doubao' : 'deepseek'; // 切换 provider 立即重新推荐 if (step === 2) this._doAiMappingSuggest(true); else if (step === 3) this._doAiCardMatchSuggest(true); }, async _doAiMappingSuggest(force) { const st = this._importState; if (!st || !st.rawRows.length) return; const ai = st.aiState.step2; if (ai.busy) return; if (!force && ai.source === 'ai') return; // 已成功过则跳过(除非强制) ai.busy = true; ai.source = null; ai.message = null; if (st.step === 2) this._renderStep2(); try { const sampleRows = st.rawRows.slice(0, 3); const res = await fetch(API_BASE + '/store/import/members/ai-assist', { method: 'POST', headers: { 'Authorization': 'Bearer ' + authToken, 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: st.aiProvider, task: 'mapping', headers: st.headers, sampleRows }) }); if (res.status === 401) { doLogout(); return; } const data = await res.json(); const d = data && data.data; if (data.code === 0 && d && d.source === 'ai' && d.suggestion && d.suggestion.mapping) { // 用 AI 推荐覆盖(仅覆盖 Excel 中存在的表头) const aiMap = d.suggestion.mapping; const newMap = {}; for (const h of st.headers) { const v = aiMap[h]; // AI 给的字段必须在允许列表里 const allowed = ['', 'name', 'phone', 'gender', 'birthday', 'balance', 'points', 'remark', 'cardName', 'remainingTimes', 'remainingHours', 'expireDate', 'cardPrice']; newMap[h] = (typeof v === 'string' && allowed.includes(v)) ? v : (st.mapping[h] || ''); } st.mapping = newMap; ai.source = 'ai'; ai.message = (d.suggestion.reason || '').slice(0, 100); } else { ai.source = 'fallback'; ai.message = (d && d.message) || 'AI 不可用,已使用规则猜测'; } } catch (e) { ai.source = 'fallback'; ai.message = String(e && e.message || e).slice(0, 100); } finally { ai.busy = false; if (st.step === 2) this._renderStep2(); } }, async _doAiCardMatchSuggest(force) { const st = this._importState; if (!st) return; if (!st.expandedColumns.hasCards) return; const ai = st.aiState.step3; if (ai.busy) return; if (!force && ai.source === 'ai') return; // 确保 cardNames 已收集 if (!st.cardNames.length) { const cardHeader = Object.entries(st.mapping).find(([_, v]) => v === 'cardName'); if (!cardHeader) return; const set = new Set(); for (const r of st.rawRows) { const cn = String(r[cardHeader[0]] || '').trim(); if (cn) set.add(cn); } st.cardNames = Array.from(set); } if (!st.cardNames.length || !st.cardTemplates.length) return; ai.busy = true; ai.source = null; ai.message = null; if (st.step === 3) this._renderStep3(); try { const res = await fetch(API_BASE + '/store/import/members/ai-assist', { method: 'POST', headers: { 'Authorization': 'Bearer ' + authToken, 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: st.aiProvider, task: 'cardMatch', cardNames: st.cardNames, cardTemplates: st.cardTemplates.map(t => ({ id: t.id, name: t.name, card_type: t.card_type, total_times: t.total_times, total_hours: t.total_hours, price: t.price })) }) }); if (res.status === 401) { doLogout(); return; } const data = await res.json(); const d = data && data.data; if (data.code === 0 && d && d.source === 'ai' && d.suggestion && d.suggestion.match) { const aiMatch = d.suggestion.match; const validIds = new Set(st.cardTemplates.map(t => t.id)); for (const cn of st.cardNames) { const v = aiMatch[cn]; if (v != null && validIds.has(Number(v))) { st.cardMatch[cn] = Number(v); } // null 或非法 id:保留规则猜测(如已有) } ai.source = 'ai'; ai.message = (d.suggestion.reason || '').slice(0, 100); } else { ai.source = 'fallback'; ai.message = (d && d.message) || 'AI 不可用,已使用规则猜测'; } } catch (e) { ai.source = 'fallback'; ai.message = String(e && e.message || e).slice(0, 100); } finally { ai.busy = false; if (st.step === 3) this._renderStep3(); } }, _validateStep3() { if (!this._importState.expandedColumns.hasCards) return true; const unmatched = this._importState.cardNames.filter(cn => !this._importState.cardMatch[cn]); if (unmatched.length > 0) { alert('以下卡名未匹配到系统模板:\n· ' + unmatched.slice(0, 8).join('\n· ') + (unmatched.length > 8 ? `\n…(其余 ${unmatched.length - 8} 个省略)` : '')); return false; } return true; }, _normGender(v) { if (v == null) return 'unknown'; const s = String(v).trim(); if (['男', 'M', 'm', 'male'].includes(s)) return 'male'; if (['女', 'F', 'f', 'female'].includes(s)) return 'female'; return 'unknown'; }, _normDate(v) { if (v == null || v === '') return null; if (v instanceof Date) return v.toISOString().slice(0, 10); const s = String(v).trim(); if (!s) return null; // 支持 2026/1/1, 2026.1.1, 2026-1-1 const m = s.match(/^(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})/); if (m) { const [_, y, mo, d] = m; return `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`; } const d = new Date(s); if (isNaN(d.getTime())) return s; // 让后端再校验报错 return d.toISOString().slice(0, 10); }, _buildPayload() { const m = this._importState.mapping; const headerByField = {}; for (const [h, f] of Object.entries(m)) { if (f) headerByField[f] = h; } const hasCards = this._importState.expandedColumns.hasCards; const get = (row, field) => { const h = headerByField[field]; return h !== undefined ? row[h] : undefined; }; const members = this._importState.rawRows.map((r, idx) => { const obj = { _rowIndex: idx + 2, name: get(r, 'name') == null ? '' : String(get(r, 'name')).trim(), phone: get(r, 'phone') == null ? '' : String(get(r, 'phone')).trim(), gender: this._normGender(get(r, 'gender')), birthday: this._normDate(get(r, 'birthday')), balance: get(r, 'balance') != null && get(r, 'balance') !== '' ? Number(get(r, 'balance')) : 0, points: get(r, 'points') != null && get(r, 'points') !== '' ? parseInt(get(r, 'points'), 10) || 0 : 0, remark: get(r, 'remark') == null ? '' : String(get(r, 'remark')) }; if (hasCards) { const cn = String(get(r, 'cardName') || '').trim(); if (cn) { const tplId = this._importState.cardMatch[cn]; if (tplId) { const rt = get(r, 'remainingTimes'); const rh = get(r, 'remainingHours'); const card = { templateId: tplId, remaining_times: rt != null && rt !== '' ? parseInt(rt, 10) : null, remaining_hours: rh != null && rh !== '' ? Number(rh) : null, expire_date: this._normDate(get(r, 'expireDate')) }; const cp = get(r, 'cardPrice'); if (cp != null && cp !== '') card.total_price = Number(cp); obj.cards = [card]; } } } return obj; }); return { members, options: { onDuplicate: this._importState.onDuplicate || 'skip' } }; }, async _doDryRun() { const payload = this._buildPayload(); const res = await fetch(API_BASE + '/store/import/members/v2/dry-run', { method: 'POST', headers: { 'Authorization': 'Bearer ' + authToken, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.status === 401) { doLogout(); return null; } const data = await res.json(); if (data.code !== 0) { alert('校验请求失败: ' + (data.message || '未知错误')); return null; } this._importState.dryRunResult = data.data; this._importState.dryRunToken = data.data.dryRunToken; this._importState._lastPayload = payload; return data.data; }, _renderStep4() { const r = this._importState.dryRunResult; if (!r) return; const s = r.summary; let html = `
总行数:${s.totalRows}
有效行:${s.validRows}
将新增:${s.willInsert}
将更新:${s.willUpdate}
将跳过:${s.willSkip}
将入卡:${s.willInsertCards}
错误行:${s.errorRows}
重复手机:${s.duplicatePhoneCount}
手机号在数据库已存在时:
`; if (r.errors && r.errors.length > 0) { html += `
❌ 错误明细(共 ${r.errors.length} 条,显示前 30)
${r.errors.slice(0, 30).map(e => `
· 第 ${e.row} 行【${this._escHtml(e.field)}】${this._escHtml(e.message)}
`).join('')} ${r.errors.length > 30 ? `
…(其余 ${r.errors.length - 30} 条省略)
` : ''}
`; } if (r.duplicates && r.duplicates.length > 0) { html += `
⚠️ 数据库已存在的手机号(共 ${r.duplicates.length},显示前 30)
${r.duplicates.slice(0, 30).map(d => `
· 第 ${d.row} 行 ${this._escHtml(d.phone)} → 当前会员【${this._escHtml(d.existsName)}】(id=${d.existsId})
`).join('')}
`; } document.getElementById('importStep4Body').innerHTML = html; const nextBtn = document.getElementById('importNextBtn'); if (s.errorRows > 0 || !this._importState.dryRunToken) { nextBtn.disabled = true; nextBtn.style.opacity = '0.5'; nextBtn.textContent = '请先修复错误'; } else if (s.willInsert === 0 && s.willUpdate === 0) { nextBtn.disabled = true; nextBtn.style.opacity = '0.5'; nextBtn.textContent = '无可入库数据'; } else { nextBtn.disabled = false; nextBtn.style.opacity = ''; nextBtn.textContent = '确认导入'; } }, _setDupStrategy(s) { this._importState.onDuplicate = s; const nextBtn = document.getElementById('importNextBtn'); nextBtn.disabled = true; nextBtn.textContent = '重新校验中...'; this._doDryRun().then(d => { if (d) this._renderStep4(); }); }, async _doCommit() { const payload = Object.assign({}, this._importState._lastPayload, { dryRunToken: this._importState.dryRunToken }); const res = await fetch(API_BASE + '/store/import/members/v2/commit', { method: 'POST', headers: { 'Authorization': 'Bearer ' + authToken, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.status === 401) { doLogout(); return null; } const data = await res.json(); if (data.code !== 0) { alert('入库失败: ' + (data.message || '未知错误')); return null; } this._importState.commitResult = data.data; return data.data; }, _renderStep5() { const r = this._importState.commitResult; if (!r) return; const html = `
导入成功
批次号:${this._escHtml(r.batchId)}
新增会员:${r.imported}
更新会员:${r.updated}
跳过会员:${r.skipped}
导入卡张数:${r.importedCards}
💡 误导入回滚 SQL(联系管理员执行)
DELETE FROM member_held_cards WHERE remark LIKE '%[#batch=${this._escHtml(r.batchId)}]%';
DELETE FROM members WHERE remark LIKE '%[#batch=${this._escHtml(r.batchId)}]%';
`; document.getElementById('importStep5Body').innerHTML = html; }, async importNext() { const step = this._importState.step; const nextBtn = document.getElementById('importNextBtn'); if (step === 1) { if (!this._importState.rawRows.length) { alert('请先选择 Excel 文件'); return; } this._renderImportStep(2); this._renderStep2(); // 异步触发 AI 字段映射(不阻塞 UI,结果回来后会自动重渲) this._doAiMappingSuggest(false); } else if (step === 2) { if (!this._validateStep2()) return; this._renderImportStep(3); this._renderStep3(); // 异步触发 AI 卡名匹配(仅当有卡列) if (this._importState.expandedColumns.hasCards) { this._doAiCardMatchSuggest(false); } } else if (step === 3) { if (!this._validateStep3()) return; nextBtn.disabled = true; nextBtn.textContent = '校验中...'; try { const r = await this._doDryRun(); if (!r) { nextBtn.disabled = false; nextBtn.textContent = '生成预览'; return; } this._renderImportStep(4); this._renderStep4(); } catch (e) { alert('校验失败: ' + (e.message || e)); nextBtn.disabled = false; nextBtn.textContent = '生成预览'; } } else if (step === 4) { if (!this._importState.dryRunToken) { alert('请先校验通过'); return; } nextBtn.disabled = true; nextBtn.textContent = '导入中...'; try { const r = await this._doCommit(); if (!r) { nextBtn.disabled = false; nextBtn.textContent = '确认导入'; return; } this._renderImportStep(5); this._renderStep5(); try { if (typeof DataManager !== 'undefined' && typeof DataManager.syncMembersFromServer === 'function') { await DataManager.syncMembersFromServer(); } if (typeof MemberModule.renderList === 'function') MemberModule.renderList(); } catch (e) { /* noop */ } } catch (e) { alert('导入失败: ' + (e.message || e)); nextBtn.disabled = false; nextBtn.textContent = '确认导入'; } } else if (step === 5) { this.closeImportModal(); } }, importPrev() { const step = this._importState.step; if (step > 1 && step < 5) { this._renderImportStep(step - 1); if (step - 1 === 2) this._renderStep2(); if (step - 1 === 3) this._renderStep3(); if (step - 1 === 4) this._renderStep4(); } }, async downloadMemberTemplate() { try { const res = await fetch(API_BASE + '/store/import/members/template', { headers: { 'Authorization': 'Bearer ' + authToken } }); if (res.status === 401) { doLogout(); return; } if (!res.ok) { alert('下载模板失败: HTTP ' + res.status); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '会员导入模板.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (e) { alert('下载模板失败: ' + (e.message || e)); } }, // ==================== /SEC-R3 ==================== onCardTypeChange() { const cardTypeName = document.getElementById('newMemberCardType').value; const cardTypes = DataManager.getMemberCardItems(); const cardType = cardTypes.find(c => c.name === cardTypeName); // 重置提成选择 this.addMemberSelectedEmployees = [{}]; if (cardType) { document.getElementById('newMemberRechargeAmount').value = cardType.storedAmount; this.onNewMemberRechargeInput(); // 渲染提成员工区域 this.renderAddMemberCommissionArea(); } else { document.getElementById('newMemberRechargeAmount').value = ''; document.getElementById('newMemberPaymentSection').style.display = 'none'; this.newMemberPaymentMethod = ''; document.querySelectorAll('#newMemberPaymentSection .pay-method-tag').forEach(t => t.classList.remove('selected')); // 隐藏提成区域 document.getElementById('addMemberCommissionArea').style.display = 'none'; } }, renderAddMemberCommissionArea() { const cardTypeName = document.getElementById('newMemberCardType').value; const cardTypes = DataManager.getMemberCardItems(); const cardType = cardTypes.find(c => c.name === cardTypeName); const area = document.getElementById('addMemberCommissionArea'); if (!cardType) { area.style.display = 'none'; return; } const cardTypeId = cardType.id; // 筛选该会员卡类型的充值提成规则 const rules = DataManager.getCommissionRules().filter(r => String(r.targetType) === 'recharge' && String(r.targetId) === String(cardTypeId) && r.enabled === true ); const rowsContainer = document.getElementById('addMemberCommissionRows'); const employees = DataManager.getEmployeeItems().filter(e => e.status === 'active'); // 始终显示提成区域(即使无规则也需记录员工),无员工时才隐藏 if (employees.length === 0) { area.style.display = 'none'; return; } // 初始化选中的员工数组(如果没有的话) if (!this.addMemberSelectedEmployees) { this.addMemberSelectedEmployees = [{}]; } const rechargeAmount = parseFloat(document.getElementById('newMemberRechargeAmount').value) || 0; let html = ''; this.addMemberSelectedEmployees.forEach((sel, idx) => { const empId = sel.employeeId || ''; const emp = employees.find(e => String(e.id) === String(empId)); // 根据员工职位查找提成规则 let commissionText = ''; if (emp) { const rule = rules.find(r => String(r.positionId) === '0') || rules[0]; if (rule) { // [SEC-COMM-PRECISION-2DP-PREVIEW] 保留 2 位精度 const commissionAmount = rule.commissionType === 'percentage' ? Math.round(rechargeAmount * rule.commissionValue) / 100 : rule.commissionValue; commissionText = rule.commissionType === 'percentage' ? `提成${rule.commissionValue}% ¥${Number(commissionAmount).toFixed(2)}` : `固定¥${Number(rule.commissionValue).toFixed(2)}`; } } // 已选的员工ID列表(排除当前行) const otherSelectedIds = this.addMemberSelectedEmployees .filter((s, i) => i !== idx && s.employeeId) .map(s => String(s.employeeId)); html += `
${commissionText ? `${commissionText}` : ''} ${this.addMemberSelectedEmployees.length > 1 ? `
` : ''}
`; }); rowsContainer.innerHTML = html; area.style.display = 'block'; // 恢复已选择的值 rowsContainer.querySelectorAll('select.commission-emp-select').forEach(sel => { const idx = parseInt(sel.getAttribute('data-row-idx')); if (this.addMemberSelectedEmployees[idx] && this.addMemberSelectedEmployees[idx].employeeId) { sel.value = this.addMemberSelectedEmployees[idx].employeeId; } }); }, addAddMemberCommissionRow() { if (!this.addMemberSelectedEmployees) this.addMemberSelectedEmployees = [{}]; this.addMemberSelectedEmployees.push({}); this.renderAddMemberCommissionArea(); }, removeAddMemberCommissionRow(idx) { this.addMemberSelectedEmployees.splice(idx, 1); if (this.addMemberSelectedEmployees.length === 0) this.addMemberSelectedEmployees = [{}]; this.renderAddMemberCommissionArea(); }, onAddMemberEmployeeChange(rowIdx, employeeId) { if (!this.addMemberSelectedEmployees) this.addMemberSelectedEmployees = [{}]; this.addMemberSelectedEmployees[rowIdx] = { employeeId: employeeId || '' }; this.renderAddMemberCommissionArea(); }, newMemberPaymentMethod: '', addMemberSelectedEmployees: [{}], onNewMemberRechargeInput() { const amount = parseFloat(document.getElementById('newMemberRechargeAmount').value) || 0; document.getElementById('newMemberPaymentSection').style.display = amount > 0 ? 'block' : 'none'; }, selectNewMemberPayMethod(method) { this.newMemberPaymentMethod = method; document.querySelectorAll('#newMemberPaymentSection .pay-method-tag').forEach(t => { t.classList.toggle('selected', t.textContent.trim() === method); }); }, closeAddModal() { document.getElementById('memberAddModal').classList.remove('active'); document.body.style.overflow = ''; }, // 添加会员 addMember() { if (this.isSubmitting) return; this.isSubmitting = true; const name = document.getElementById('newMemberName').value.trim(); const phone = document.getElementById('newMemberPhone').value.trim(); const cardType = document.getElementById('newMemberCardType').value; const birthday = document.getElementById('newMemberBirthday').value; const remark = document.getElementById('newMemberRemark').value.trim(); const rechargeAmount = parseFloat(document.getElementById('newMemberRechargeAmount').value) || 0; if (!name || !phone) { this.isSubmitting = false; alert('请填写姓名和手机号'); return; } // 检查手机号是否重复 const existingMember = DataManager.getMembers().find(m => m.phone === phone); if (existingMember) { this.isSubmitting = false; alert('手机号已存在,会员:' + existingMember.name); return; } if (cardType && rechargeAmount <= 0) { this.isSubmitting = false; alert('请选择卡类型后充值金额必填'); return; } if (rechargeAmount > 0 && !this.newMemberPaymentMethod) { this.isSubmitting = false; alert('请选择付款方式'); return; } const cardTypes = DataManager.getMemberCardItems(); const card = cardTypes.find(c => c.name === cardType); // 余额 = 卡类型的总额(储值+赠送),不再重复加充值金额 const totalBalance = card ? card.totalAmount : 0; const member = { id: Date.now().toString(), name, phone, gender: 'unknown', cardType: cardType || '普通会员', cardTypeId: card ? card.id : '', balance: totalBalance, points: 0, coupons: 0, consumeCount: 0, createDate: new Date().toISOString(), birthday: birthday || '', remark, createdAt: new Date().toISOString(), // 传递给API需要的字段 payMethod: this.newMemberPaymentMethod || '' }; DataManager.addMember(member); // 如果有充值金额,记录充值 if (rechargeAmount > 0) { // M3-3: 按门槛 + 模式计算赠送 const bonus = window.calcBonusByCard(card, rechargeAmount); const totalAmount = rechargeAmount + bonus; const newMemberRechargeOrderNo = DataManager.generateOrderNo(); DataManager.addMemberRecord({ memberId: member.id, type: '充值', itemName: '充值' + rechargeAmount + '元', amount: totalAmount, date: new Date().toISOString(), payMethod: this.newMemberPaymentMethod || '', remark: remark || '', cardTypeId: card ? card.id : '', orderNo: newMemberRechargeOrderNo }); // 累计积分(充值1元=1积分) DataManager.updateMemberPoints(member.id, Math.floor(totalAmount)); // 检查会员等级升级 DataManager.checkMemberUpgrade(member.id); // 计算多员工提成 const cardTypeId = card ? card.id : ''; (this.addMemberSelectedEmployees || []).forEach(sel => { if (!sel.employeeId) return; const employees = DataManager.getEmployeeItems(); const employee = employees.find(e => String(e.id) === String(sel.employeeId)); if (employee) { const commissionResult = DataManager.calculateCommission(employee.positionId, 'recharge', cardTypeId, rechargeAmount); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission(sel.employeeId, member.id, '充值提成', '充值', rechargeAmount, commissionResult, this.newMemberPaymentMethod, newMemberRechargeOrderNo); } } }); } this.closeAddModal(); this.renderList(); this.isSubmitting = false; alert('添加成功'); }, // 打开会员详情 openDetail(id) { this.currentDetailId = id; this.showAllRecords = false; // SEC-MEMBER-DETAIL-RECORD-COLLAPSE: 复位折叠状态 const member = this.members.find(m => String(m.id) === String(id)); if (!member) return; document.getElementById('memberListPage').classList.remove('active'); document.getElementById('memberDetailPage').classList.add('active'); // 填充详情信息 const level = this.getLevelTag(member.cardType); const avatarBg = 'linear-gradient(135deg, #9B1B5E, #D4A843)'; document.getElementById('detailAvatar').style.background = avatarBg; document.getElementById('detailAvatar').textContent = member.name.charAt(0); document.getElementById('detailName').textContent = member.name; document.getElementById('detailCardType').className = level.class; document.getElementById('detailCardType').textContent = level.text; document.getElementById('detailPhone').textContent = this.maskPhone(member.phone); // 显示生日 const birthdayEl = document.getElementById('detailBirthday'); if (member.birthday) { birthdayEl.textContent = '🎂 ' + member.birthday; birthdayEl.style.display = 'block'; } else { birthdayEl.style.display = 'none'; } document.getElementById('detailBalance').textContent = '¥' + (parseFloat(member.balance) || 0).toLocaleString(); document.getElementById('detailPoints').textContent = (parseInt(member.points) || 0).toLocaleString(); this.refreshPointsExpireBadge(member); document.getElementById('detailCoupons').textContent = (member.coupons || 0) + '张'; document.getElementById('detailConsume').textContent = (member.consumeCount || 0) + '次'; // 渲染会员标签 this.renderDetailTags(); // 渲染明细记录 this.renderRecords(); // 渲染会员持卡 this.renderMemberHeldCards(); // 默认滚到顶部(防止从上一个会员详情继承滚动位置) try { window.scrollTo(0, 0); const __detailPage = document.getElementById('memberDetailPage'); if (__detailPage) { const __main = __detailPage.querySelector('main'); if (__main) __main.scrollTop = 0; } } catch (e) {} }, // 返回会员列表 backToList() { document.getElementById('memberDetailPage').classList.remove('active'); document.getElementById('memberListPage').classList.add('active'); this.currentDetailId = null; }, // 切换筛选标签 setFilter(filter) { this.currentFilter = filter; this.showAllMembers = false; // SEC-MEMBER-LIST-COLLAPSE: 切筛选复位 this._visibleMembers = 10; // [Fix8 20260709] paginated expand: 复位 // 更新标签样式 ['filterAll', 'filterNew', 'filterExpire'].forEach(id => { const el = document.getElementById(id); if (id === 'filter' + (filter === 'all' ? 'All' : filter === 'new' ? 'New' : 'Expire')) { el.classList.add('active'); } else { el.classList.remove('active'); } }); this.renderList(); }, // 切换详情Tab setDetailTab(tab) { this.currentDetailTab = tab; this.showAllRecords = false; // SEC-MEMBER-DETAIL-RECORD-COLLAPSE: 切 tab 时复位 ['detailConsume', 'detailCharge', 'detailLog'].forEach(id => { const el = document.getElementById(id); if (id === 'detail' + (tab === 'consume' ? 'Consume' : tab === 'charge' ? 'Charge' : 'Log')) { el.classList.add('active'); } else { el.classList.remove('active'); } }); this.renderRecords(); }, // 渲染明细记录 renderRecords() { const records = DataManager.getMemberRecords(this.currentDetailId); const listEl = document.getElementById('memberRecords'); const now = new Date(); // 过滤记录 let filtered = records; if (this.currentDetailTab === 'consume') { filtered = records.filter(r => r.type === '消费'); } else if (this.currentDetailTab === 'recharge') { filtered = records.filter(r => r.type === '充值'); } else { // 操作日志显示全部记录 filtered = records; } // 按时间排序(根据recordOrderDesc决定方向) filtered.sort((a, b) => { const diff = new Date(b.date) - new Date(a.date); return this.recordOrderDesc ? diff : -diff; }); if (filtered.length === 0) { listEl.innerHTML = '
暂无记录
'; return; } // SEC-MEMBER-DETAIL-RECORD-COLLAPSE: 默认仅显示最近 10 条,超出的折叠 const __DEFAULT_LIMIT = 10; const __showAll = !!this.showAllRecords; const __recentList = filtered.slice(0, __DEFAULT_LIMIT); const __olderList = filtered.slice(__DEFAULT_LIMIT); const __displayList = __showAll ? filtered : __recentList; let __cardsHtml = __displayList.map(r => { const date = new Date(r.date); const dateStr = `${date.getFullYear()}/${date.getMonth()+1}/${date.getDate()}`; const timeStr = `${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}`; const isPointsOnly = r.type === '积分'; const amountClass = r.type === '充值' ? 'text-green-500' : 'text-[#D4A843]'; const amountPrefix = r.type === '充值' ? '+' : '-'; /* SEC-J6: 积分变动徽章(pointsChange 在 J2 已挂到流水缓存上) */ const pcNum = parseInt(r.pointsChange) || 0; const pointsBadge = (pcNum !== 0 && !r.cancelled) ? `
${pcNum>0?'+':''}${pcNum} 积分
` : ''; const displayName = r.itemName || r.remark || r.type || ''; const payMethodTag = r.payMethod ? `${escapeHtml(r.payMethod)}` : ''; const orderNoDisplay = r.orderNo || ''; // 判断是否可撤销 const isToday = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate(); const canCancel = isToday && !r.cancelled; const cancelledStyle = r.cancelled ? 'opacity:0.5;' : ''; // 判断是否显示备注(有值且不是默认值) const defaultRemarks = ['卡券划扣', '购买卡券', '充值', '卡券消费']; const showRemark = r.remark && !defaultRemarks.includes(r.remark) && r.remark !== displayName; const remarkHtml = showRemark ? `
📝 ${escapeHtml(r.remark)}
` : ''; return `
${dateStr}
${timeStr}
${orderNoDisplay ? `
${orderNoDisplay}
` : ''}
${displayName}${payMethodTag}
${remarkHtml}
${isPointsOnly ? '积分流水' : `${r.cancelled ? '已撤销' : amountPrefix + '¥' + Math.abs(r.amount)}`} ${canCancel && !isPointsOnly ? '撤销' : ''}
${pointsBadge}
`; }).join(''); // SEC-MEMBER-DETAIL-RECORD-COLLAPSE: 拼接折叠/展开按钮 if (__olderList.length > 0) { if (!__showAll) { __cardsHtml += `
`; } else { __cardsHtml += `
`; } } listEl.innerHTML = __cardsHtml; }, // SEC-MEMBER-DETAIL-RECORD-COLLAPSE: 切换历史记录展开/收起 toggleShowAllRecords() { this.showAllRecords = !this.showAllRecords; this.renderRecords(); }, // 撤销记录 // [SEC-VOID-AUTOREFRESH-FIX] 改为 async,在 alert 后 await 后端同步完成再渲染 // [SEC-VOID-RACE-FIX] 加 orderNo 参数,解决前端临时 id race async cancelRecord(recordId, orderNo) { if (!confirm('确定撤销该记录?撤销后不可恢复。')) return; const result = DataManager.cancelRecord(recordId, orderNo); if (result.success) { alert(result.message); // [SEC-VOID-AUTOREFRESH-FIX] 等后端 refreshAfter 完成,确保 localStorage 已更新 if (result.syncPromise) { try { await result.syncPromise; } catch(_) {} } this.members = DataManager.getMembers(); this.renderRecords(); // 刷新详情页数据 const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (member) { document.getElementById('detailBalance').textContent = '¥' + (parseFloat(member.balance) || 0).toLocaleString(); document.getElementById('detailConsume').textContent = (member.consumeCount || 0) + '次'; } this.renderMemberHeldCards(); } else { alert(result.message); } }, // 打开编辑弹窗 openEditModal() { const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (!member) return; document.getElementById('memberEditModal').classList.add('active'); document.body.style.overflow = 'hidden'; // 填充会员卡类型下拉 const select = document.getElementById('editMemberCardType'); const cardTypes = DataManager.getMemberCardItems(); select.innerHTML = '' + cardTypes.map(c => ``).join(''); // 填充表单 document.getElementById('editMemberName').value = member.name; document.getElementById('editMemberPhone').value = member.phone; document.getElementById('editMemberCardType').value = member.cardTypeId || ''; document.getElementById('editMemberBirthday').value = member.birthday || ''; const birthdayPicker = document.getElementById('editMemberBirthdayPicker'); if (birthdayPicker) { birthdayPicker.value = member.birthday || ''; } document.getElementById('editMemberRemark').value = member.remark || ''; }, closeEditModal() { document.getElementById('memberEditModal').classList.remove('active'); document.body.style.overflow = ''; }, // 保存编辑 saveEdit() { const name = document.getElementById('editMemberName').value.trim(); const phone = document.getElementById('editMemberPhone').value.trim(); const cardTypeId = document.getElementById('editMemberCardType').value; const birthday = document.getElementById('editMemberBirthday').value; const remark = document.getElementById('editMemberRemark').value.trim(); if (!name || !phone) { alert('请填写姓名和手机号'); return; } // [SEC-MEMBER-EDIT-CARDTYPE-DISPLAY-FIX 20260701] 反查卡类型名,避免详情页 level tag 显示"普通" const _selCardObj = DataManager.getMemberCardItems().find(c => String(c.id) === String(cardTypeId)); const cardType = _selCardObj ? _selCardObj.name : ''; DataManager.updateMember(this.currentDetailId, { name, phone, cardType, cardTypeId, birthday, remark }); this.closeEditModal(); this.members = DataManager.getMembers(); this.openDetail(this.currentDetailId); alert('保存成功'); }, bindEvents() { document.getElementById('memberAddModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeAddModal(); }); document.getElementById('memberEditModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) this.closeEditModal(); }); }, // 筛选状态 [FIX_MEMBER_FILTER_TAG_20260709] 会员筛选按钮 class 是 member-filter-tag 不是 filter-tag,加 null 保护 filterByStatus(status) { this.currentFilter = status; // 更新标签样式 document.querySelectorAll('.member-filter-tag').forEach(tag => tag.classList.remove('active')); const target = event && event.target && event.target.closest('.member-filter-tag'); if (target) target.classList.add('active'); this.renderList(); }, // 生日输入格式化 formatBirthdayInput(fieldId) { const input = document.getElementById(fieldId); if (!input) return; let val = input.value.trim(); if (!val) return; // 如果已经是YYYY-MM-DD格式,直接返回 if (/^\d{4}-\d{2}-\d{2}$/.test(val)) return; // 处理YYYY/MM/DD或YYYY.MM.DD格式 val = val.replace(/[\/\.]/g, '-'); // 处理YYYYMMDD格式(8位纯数字) if (/^\d{8}$/.test(val)) { val = val.substring(0,4) + '-' + val.substring(4,6) + '-' + val.substring(6,8); } // 补齐月份和日期的0 const parts = val.split('-'); if (parts.length === 3) { if (parts[1].length === 1) parts[1] = '0' + parts[1]; if (parts[2].length === 1) parts[2] = '0' + parts[2]; val = parts.join('-'); } input.value = val; // 同步到隐藏的date picker const pickerId = fieldId + 'Picker'; const picker = document.getElementById(pickerId); if (picker && /^\d{4}-\d{2}-\d{2}$/.test(val)) { picker.value = val; } }, // 搜索会员 searchMembers(keyword) { keyword = keyword.trim().toLowerCase(); this.showAllMembers = false; // SEC-MEMBER-LIST-COLLAPSE: 搜索时复位 this._visibleMembers = 10; // [Fix8 20260709] paginated expand: 复位 if (!keyword) { this.renderList(); return; } // 获取卡类型和持卡数据用于扩展搜索 const cardTypes = DataManager.getMemberCardItems() || []; const heldCards = DataManager.getAllMemberHeldCards() || []; const filtered = this.members.filter(m => { // 基础匹配:姓名和电话 if (m.name.toLowerCase().includes(keyword) || m.phone.includes(keyword)) return true; // 会员卡类型名称匹配 const cardType = cardTypes.find(ct => String(ct.id) === String(m.cardTypeId)); if (cardType && cardType.name.toLowerCase().includes(keyword)) return true; if (m.cardType && m.cardType.toLowerCase().includes(keyword)) return true; // 持有卡券名称匹配 const memberCards = heldCards.filter(c => String(c.memberId) === String(m.id) && c.status === 'active'); if (memberCards.some(c => c.cardName && c.cardName.toLowerCase().includes(keyword))) return true; return false; }); const listEl = document.getElementById('memberList'); if (filtered.length === 0) { listEl.innerHTML = '
未找到匹配的会员
'; return; } listEl.innerHTML = filtered.map(m => { const level = this.getLevelTag(m.cardType); const avatarBg = 'linear-gradient(135deg, #9B1B5E, #D4A843)'; // 获取持卡标签 const memberCards = heldCards.filter(c => String(c.memberId) === String(m.id) && c.status === 'active'); let heldCardsHtml = ''; if (memberCards.length > 0) { const displayCards = memberCards.slice(0, 2); let tagHtml = displayCards.map(card => { let detailStr = ''; if (card.cardType === 'count') { detailStr = `${card.remainingCount || 0}次`; } else if (card.cardType === 'time') { detailStr = `${card.remainingHours || 0}h`; } else if (card.cardType === 'package') { const total = (card.packageItems || []).reduce((sum, pi) => sum + (pi.totalCount || 0), 0); const remaining = (card.packageItems || []).reduce((sum, pi) => sum + (pi.remainingCount || 0), 0); detailStr = `${remaining}/${total}项`; } return `${escapeHtml(card.cardName || '卡券')}(${detailStr})`; }).join(''); if (memberCards.length > 2) { tagHtml += `+${memberCards.length - 2}`; } heldCardsHtml = `
${tagHtml}
`; } return `
${escapeHtml(m.name.charAt(0))}
${escapeHtml(m.name)} ${escapeHtml(level.text)}
${escapeHtml(this.maskPhone(m.phone))}
¥${m.balance.toLocaleString()}
${heldCardsHtml}
`; }).join(''); }, // 返回列表(修复goBackToList方法名) goBackToList() { this.backToList(); }, // 切换记录标签页(别名方法) switchRecordTab(tab) { this.currentDetailTab = tab; this.showAllRecords = false; // SEC-MEMBER-DETAIL-RECORD-COLLAPSE: 切 tab 时复位 // 更新标签样式 document.querySelectorAll('.record-tab').forEach(t => t.classList.remove('active')); event.target.classList.add('active'); this.renderRecords(); }, toggleRecordOrder() { this.recordOrderDesc = !this.recordOrderDesc; const btn = document.getElementById('recordOrderBtn'); btn.textContent = this.recordOrderDesc ? '↓ 最新优先' : '↑ 最早优先'; this.renderRecords(); }, // ========== 充值功能 ========== currentRechargeId: null, rechargeAmount: 1000, rechargePayMethod: '现金', openRecharge(memberId) { this.currentRechargeId = memberId; const member = this.members.find(m => String(m.id) === String(memberId)); if (!member) return; document.getElementById('memberDetailPage').classList.remove('active'); document.getElementById('rechargePage').classList.add('active'); // 填充会员信息 const avatarBg = 'linear-gradient(135deg, #9B1B5E, #D4A843)'; document.getElementById('rechargeAvatar').style.background = avatarBg; document.getElementById('rechargeAvatar').textContent = member.name.charAt(0); document.getElementById('rechargeName').textContent = member.name; document.getElementById('rechargePhone').textContent = this.maskPhone(member.phone); document.getElementById('rechargeBalance').textContent = `当前余额 ¥${(parseFloat(member.balance) || 0).toLocaleString()}`; // 渲染充值提成区域 this.rechargeSelectedEmployees = [{}]; this.renderRechargeCommissionArea(member); // 重置表单 this.rechargeAmount = 1000; this.rechargePayMethod = '现金'; this.selectRechargeAmount(1000); document.getElementById('customAmountInput').value = ''; document.getElementById('rechargeRemark').value = ''; this.updateRechargeBonus(); this.updateRechargePayMethodUI(); }, renderRechargeCommissionArea(member) { // 获取会员的卡类型ID let cardTypeObj = DataManager.getMemberCardItems().find(c => String(c.id) === String(member.cardTypeId)); if (!cardTypeObj) { cardTypeObj = DataManager.getMemberCardItems().find(c => c.name === member.cardType); } const cardTypeId = cardTypeObj ? cardTypeObj.id : null; const area = document.getElementById('rechargeCommissionArea'); if (!cardTypeId) { area.style.display = 'none'; return; } // 筛选该会员卡类型的充值提成规则 const rules = DataManager.getCommissionRules().filter(r => String(r.targetType) === 'recharge' && String(r.targetId) === String(cardTypeId) && r.enabled === true ); const rowsContainer = document.getElementById('rechargeCommissionRows'); const employees = DataManager.getEmployeeItems().filter(e => e.status === 'active'); // 始终显示提成区域(即使无规则也需记录员工),无员工时才隐藏 if (employees.length === 0) { area.style.display = 'none'; return; } // 初始化选中员工数组 if (!this.rechargeSelectedEmployees) { this.rechargeSelectedEmployees = [{}]; } const rechargeAmount = this.rechargeAmount || 1000; let html = ''; this.rechargeSelectedEmployees.forEach((sel, idx) => { const empId = sel.employeeId || ''; const emp = employees.find(e => String(e.id) === String(empId)); let commissionText = ''; if (emp) { const rule = rules.find(r => String(r.positionId) === '0') || rules[0]; if (rule) { // [SEC-COMM-PRECISION-2DP-PREVIEW] 保留 2 位精度 const commissionAmount = rule.commissionType === 'percentage' ? Math.round(rechargeAmount * rule.commissionValue) / 100 : rule.commissionValue; commissionText = rule.commissionType === 'percentage' ? `提成${rule.commissionValue}% ¥${Number(commissionAmount).toFixed(2)}` : `固定¥${Number(rule.commissionValue).toFixed(2)}`; } } const otherSelectedIds = this.rechargeSelectedEmployees .filter((s, i) => i !== idx && s.employeeId) .map(s => String(s.employeeId)); html += `
${commissionText ? `${commissionText}` : ''} ${this.rechargeSelectedEmployees.length > 1 ? `
` : ''}
`; }); rowsContainer.innerHTML = html; area.style.display = 'block'; // 恢复已选择的值 rowsContainer.querySelectorAll('select.commission-emp-select').forEach(sel => { const idx = parseInt(sel.getAttribute('data-row-idx')); if (this.rechargeSelectedEmployees[idx] && this.rechargeSelectedEmployees[idx].employeeId) { sel.value = this.rechargeSelectedEmployees[idx].employeeId; } }); }, addRechargeCommissionRow() { if (!this.rechargeSelectedEmployees) this.rechargeSelectedEmployees = [{}]; this.rechargeSelectedEmployees.push({}); const member = this.members.find(m => String(m.id) === String(this.currentRechargeId)); if (member) this.renderRechargeCommissionArea(member); }, removeRechargeCommissionRow(idx) { this.rechargeSelectedEmployees.splice(idx, 1); if (this.rechargeSelectedEmployees.length === 0) this.rechargeSelectedEmployees = [{}]; const member = this.members.find(m => String(m.id) === String(this.currentRechargeId)); if (member) this.renderRechargeCommissionArea(member); }, onRechargeEmployeeChange(rowIdx, employeeId) { if (!this.rechargeSelectedEmployees) this.rechargeSelectedEmployees = [{}]; this.rechargeSelectedEmployees[rowIdx] = { employeeId: employeeId || '' }; const member = this.members.find(m => String(m.id) === String(this.currentRechargeId)); if (member) this.renderRechargeCommissionArea(member); }, closeRecharge() { document.getElementById('rechargePage').classList.remove('active'); document.getElementById('memberDetailPage').classList.add('active'); this.currentRechargeId = null; if (this._fromQuickAction) { this._fromQuickAction = false; document.getElementById('memberDetailPage').classList.remove('active'); if (typeof switchTab === 'function') switchTab('home'); } }, selectRechargeAmount(amount) { this.rechargeAmount = amount; document.querySelectorAll('.amount-btn').forEach(btn => { btn.classList.remove('selected'); if (parseInt(btn.textContent) === amount) { btn.classList.add('selected'); } }); document.getElementById('customAmountInput').value = ''; this.updateRechargeBonus(); }, onCustomAmountInput() { const value = document.getElementById('customAmountInput').value; if (value && parseInt(value) > 0) { this.rechargeAmount = parseInt(value); document.querySelectorAll('.amount-btn').forEach(btn => btn.classList.remove('selected')); this.updateRechargeBonus(); } }, updateRechargeBonus() { // M3-3: 按门槛 + 模式计算赠送 (充值 < storedAmount 不赠送) // M3-3c: cardType 不存在(被删除/未设置)时给出明确提示 const currentMember = this.members.find(m => String(m.id) === String(this.currentRechargeId)); const rechargeCardType = currentMember ? DataManager.getMemberCardItems().find(c => String(c.id) === String(currentMember.cardTypeId)) : null; const bonus = window.calcBonusByCard(rechargeCardType, this.rechargeAmount); const tipEl = document.getElementById('bonusAmountTip'); if (currentMember && !rechargeCardType) { // 会员有 cardTypeId 但已被删除,或会员未设置卡类型 if (currentMember.cardTypeId) { tipEl.innerHTML = `⚠ 该会员原绑定的卡类型已被删除,本次充值无赠送。请到会员资料重新选择卡类型`; } else { tipEl.textContent = `充值${this.rechargeAmount}元 (未设置会员卡类型,无赠送)`; } } else if (rechargeCardType && this.rechargeAmount < (parseFloat(rechargeCardType.storedAmount) || 0)) { tipEl.textContent = `充值${this.rechargeAmount}元 (需充值满 ¥${rechargeCardType.storedAmount} 才有赠送)`; } else if (bonus > 0) { tipEl.textContent = `充值${this.rechargeAmount}元 赠送${bonus}元`; } else { tipEl.textContent = `充值${this.rechargeAmount}元`; } document.getElementById('confirmRechargeBtn').textContent = `确认充值 ¥${this.rechargeAmount}`; // 更新提成区域显示金额 const member = this.members.find(m => String(m.id) === String(this.currentRechargeId)); if (member) { this.renderRechargeCommissionArea(member); } }, selectRechargePayMethod(method) { this.rechargePayMethod = method; this.updateRechargePayMethodUI(); }, updateRechargePayMethodUI() { document.querySelectorAll('.pay-method-tag').forEach(tag => { tag.classList.remove('selected'); if (tag.textContent.trim() === this.rechargePayMethod) { tag.classList.add('selected'); } }); }, confirmRecharge() { if (this.isSubmitting) return; this.isSubmitting = true; const confirmBtn = document.getElementById('confirmRechargeBtn'); if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.style.opacity = '0.6'; } const member = this.members.find(m => String(m.id) === String(this.currentRechargeId)); if (!member) { this.isSubmitting = false; if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.style.opacity = '1'; } return; } // M3-3: 按门槛 + 模式计算赠送 const rechargeCardType = DataManager.getMemberCardItems().find(c => String(c.id) === String(member.cardTypeId)); const bonus = window.calcBonusByCard(rechargeCardType, this.rechargeAmount); const totalAmount = this.rechargeAmount + bonus; const remark = document.getElementById('rechargeRemark').value.trim(); // 更新会员余额 member.balance += totalAmount; DataManager.updateMember(member.id, { balance: member.balance }); // 累计积分(1元=1积分) DataManager.updateMemberPoints(member.id, Math.floor(totalAmount)); // 添加充值记录 const orderNo = DataManager.generateOrderNo(); DataManager.addMemberRecord({ memberId: member.id, type: '充值', itemName: '充值' + this.rechargeAmount + '元', amount: totalAmount, date: new Date().toISOString(), payMethod: this.rechargePayMethod || '', remark: remark || '', orderNo: orderNo }); // 计算充值提成(多员工) let cardTypeObj = DataManager.getMemberCardItems().find(c => String(c.id) === String(member.cardTypeId)); if (!cardTypeObj) { cardTypeObj = DataManager.getMemberCardItems().find(c => c.name === member.cardType); } const cardTypeId = cardTypeObj ? cardTypeObj.id : ''; (this.rechargeSelectedEmployees || []).forEach(sel => { if (!sel.employeeId) return; const employee = DataManager.getEmployeeItems().find(e => String(e.id) === String(sel.employeeId)); if (employee) { const commissionResult = DataManager.calculateCommission(employee.positionId, 'recharge', cardTypeId, this.rechargeAmount); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission(sel.employeeId, member.id, '充值提成', '充值', this.rechargeAmount, commissionResult, this.rechargePayMethod, orderNo); } } }); // 检查会员等级升级 const upgradeResult = DataManager.checkMemberUpgrade(member.id); if (upgradeResult && upgradeResult.canUpgrade) { // 自动升级 DataManager.upgradeMember(member.id, upgradeResult.newCardTypeId); // 刷新会员列表 this.members = DataManager.getMembers(); // 弹窗提示升级成功 const newCardType = DataManager.getMemberCardItems().find(ct => String(ct.id) === String(upgradeResult.newCardTypeId)); setTimeout(() => { alert(`恭喜!会员已升级为【${newCardType ? newCardType.name : '新等级'}】!累计充值¥${upgradeResult.totalRecharge}`); }, 100); } // 刷新会员列表 this.members = DataManager.getMembers(); this.isSubmitting = false; if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.style.opacity = '1'; } // 显示成功弹窗(带打印小票按钮) this.showSuccessModal({ title: '充值成功', message: '充值' + this.rechargeAmount + '元,赠送' + bonus + '元,实际到账' + totalAmount + '元', type: '充值', recordData: { orderNo: orderNo, memberName: member.name, type: '充值', itemName: '充值' + this.rechargeAmount + '元', amount: totalAmount, payMethod: this.rechargePayMethod || '', remark: remark || '' } }); const rechargeMemberId = this.currentRechargeId; this.closeRecharge(); this.openDetail(rechargeMemberId); }, // ========== 消费功能 ========== currentConsumeId: null, consumePayMethod: '会员余额', isSubmitting: false, // 防重复提交锁 selectedDeductCard: null, selectedPackageItems: [], deductCount: 1, // 计次卡扣减次数 deductHours: 1, // 计时卡扣减时长(小时) cardDeductEmployees: {}, // 卡券划扣提成员工选择 {deductKey: {positionId: employeeId}} selectedServices: [], selectedProducts: [], // 已选产品 {id,name,price(原价),qty,categoryName,commissionEmployees:[{employeeId}]} productPickerData: [], consumeDiscount: 100, productDiscount: 100, // 会员卡产品折扣 openConsume(memberId) { this.currentConsumeId = memberId; const member = this.members.find(m => String(m.id) === String(memberId)); if (!member) return; document.getElementById('memberDetailPage').classList.remove('active'); document.getElementById('consumePage').classList.add('active'); // 填充会员信息 const avatarBg = 'linear-gradient(135deg, #9B1B5E, #D4A843)'; document.getElementById('consumeAvatar').style.background = avatarBg; document.getElementById('consumeAvatar').textContent = member.name.charAt(0); document.getElementById('consumeName').textContent = member.name; document.getElementById('consumePhone').textContent = this.maskPhone(member.phone); document.getElementById('consumeBalance').textContent = `当前余额 ¥${(parseFloat(member.balance) || 0).toLocaleString()}`; document.getElementById('consumeBalanceTip').textContent = `¥${(parseFloat(member.balance) || 0).toLocaleString()}`; // 重置表单 this.selectedServices = []; this.selectedProducts = []; this.selectedDeductCard = null; this.selectedPackageItems = []; this.cardDeductEmployees = {}; this.consumePayMethod = '会员余额'; this.usePointsDeduct = false; // 重置积分抵扣开关 const cardTypes = DataManager.getMemberCardItems(); const memberCard = cardTypes.find(c => c.name === member.cardType); this.consumeDiscount = memberCard ? memberCard.serviceDiscount : 100; this.productDiscount = memberCard ? (memberCard.productDiscount || 100) : 100; this.renderSelectedServices(); this.renderSelectedProducts(); this.updateConsumePayMethodUI(); document.getElementById('discountInput').value = this.consumeDiscount < 100 ? (this.consumeDiscount / 10) + '折' : ''; document.getElementById('discountTip').textContent = memberCard && memberCard.serviceDiscount < 100 ? member.cardType + ' 专享' + (memberCard.serviceDiscount / 10) + '折优惠' : ''; document.getElementById('cardDeductSection').style.display = 'none'; document.getElementById('cardDeductTotal').style.display = 'none'; document.getElementById('consumeRemarkInput').value = ''; // 重置积分抵扣UI const pointsToggle = document.getElementById('pointsDeductToggle'); if (pointsToggle) pointsToggle.checked = false; const sliderEl = document.getElementById('pointsDeductSlider'); const trackEl = document.getElementById('pointsDeductTrack'); if (sliderEl) sliderEl.style.transform = 'translateX(0)'; if (trackEl) trackEl.style.background = '#ddd'; this.updatePointsDeductInfo(memberId); this.updateConsumeTotal(); }, closeConsume() { document.getElementById('consumePage').classList.remove('active'); document.getElementById('memberDetailPage').classList.add('active'); this.currentConsumeId = null; this.selectedServices = []; this.selectedProducts = []; if (this._fromQuickAction) { this._fromQuickAction = false; document.getElementById('memberDetailPage').classList.remove('active'); if (typeof switchTab === 'function') switchTab('home'); } }, renderSelectedServices() { const listEl = document.getElementById('selectedServiceList'); if (this.selectedServices.length === 0) { listEl.innerHTML = '
暂未添加服务
'; return; } const positions = DataManager.getEmployeePositions(); const rules = DataManager.getCommissionRules(); const employees = DataManager.getEmployeeItems().filter(e => e.status === 'active'); listEl.innerHTML = this.selectedServices.map((s, idx) => { // 计算折扣后价格 const discountRate = this.consumeDiscount / 100; const discountedPrice = Math.round(parseFloat(s.price) * discountRate * 100) / 100; // 只筛选该服务有启用提成规则的职位 const serviceRules = rules.filter(r => r.targetType === 'service' && String(r.targetId) === String(s.id) && r.enabled ); // 按positionId去重 const enabledPositionIds = [...new Set(serviceRules.map(r => String(r.positionId)))]; // 生成员工选择区域HTML(按职位选择) let commissionHtml = ''; if (enabledPositionIds.length > 0) { commissionHtml = '
'; // 始终显示职位名称 enabledPositionIds.forEach(posId => { const pos = positions.find(p => String(p.id) === posId); if (!pos) return; // 查找该职位对该服务的提成规则 const rule = serviceRules.find(r => String(r.positionId) === posId); // 该职位的员工列表 const posEmployees = employees.filter(e => String(e.positionId) === posId); if (rule) { // [SEC-COMM-PRECISION-2DP-PREVIEW] 保留 2 位精度 const commissionAmount = rule.commissionType === 'percentage' ? Math.round(discountedPrice * rule.commissionValue) / 100 : rule.commissionValue; const commissionText = rule.commissionType === 'percentage' ? `提成 ${rule.commissionValue}% ¥${Number(commissionAmount).toFixed(2)}` : `提成 固定¥${Number(rule.commissionValue).toFixed(2)}`; const labelText = pos.name; // 获取已选择的员工 if (!this.selectedServices[idx].selectedEmployees) { this.selectedServices[idx].selectedEmployees = {}; } const selectedEmpId = this.selectedServices[idx].selectedEmployees[posId] || ''; commissionHtml += `
${labelText} ${commissionText.replace('提成 ', '')}
`; } }); commissionHtml += '
'; } return `
${escapeHtml(s.name)}
${s.category || ''}
${commissionHtml}
`; }).join(''); if (this.consumePayMethod === '卡券扣次') { this.renderCardDeductList(); } }, // 服务项员工选择变化(按职位) onServiceEmployeeChange(serviceIdx, positionId, employeeId) { if (!this.selectedServices[serviceIdx].selectedEmployees) { this.selectedServices[serviceIdx].selectedEmployees = {}; } this.selectedServices[serviceIdx].selectedEmployees[positionId] = employeeId; }, removeSelectedService(idx) { this.selectedServices.splice(idx, 1); this.renderSelectedServices(); this.updateConsumeTotal(); if (this.consumePayMethod === '卡券扣次') { this.renderCardDeductList(); } }, updateServicePrice(idx, value) { const price = parseFloat(value) || 0; if (price < 0) return; this.selectedServices[idx].price = price; this.updateConsumeTotal(); this.renderSelectedServices(); // 价格变化后重新渲染以更新提成金额显示 }, // ===== 卡券功能 ===== selectedPurchaseCard: null, purchaseCardTotal: 0, purchasePayMethod: '', renderMemberHeldCards() { const cards = DataManager.getMemberHeldCards(this.currentDetailId); const container = document.getElementById('memberHeldCards'); if (!cards.length) { container.innerHTML = '
暂无卡券,点击上方购买
'; // SEC-S1-Task3-Fix: 即使没有 active 卡,也要尝试加载历史卡券 this.loadHistoryCards(); return; } const now = new Date(); container.innerHTML = cards.map(c => { const typeClass = c.cardType === 'count' ? 'type-count' : (c.cardType === 'time' ? 'type-time' : 'type-package'); const tagText = c.cardType === 'count' ? '计次' : (c.cardType === 'time' ? '计时' : '套餐'); let remainingText = ''; let warningText = ''; // 计算到期状态 (SEC-EXPIRE-UI: 高对比白色字体 + 分级预警) let expireStatus = ''; if (c.expireDate) { const expireDate = new Date(c.expireDate); const diffDays = Math.ceil((expireDate - now) / (1000 * 60 * 60 * 24)); const _base = 'font-size:12px;margin-top:8px;padding:3px 8px;border-radius:6px;display:inline-block;font-weight:600;'; if (diffDays < 0) { expireStatus = '
❌ 已过期 ' + Math.abs(diffDays) + ' 天
'; } else if (diffDays <= 7) { expireStatus = '
⚠️ 仅剩 ' + diffDays + ' 天
'; } else if (diffDays <= 30) { expireStatus = '
📅 ' + diffDays + ' 天后到期
'; } else { expireStatus = '
📅 到期 ' + c.expireDate + '
'; } } else { expireStatus = '
♾️ 长期有效
'; } // 计算即将耗尽状态 if (c.cardType === 'count' && (c.remainingCount || 0) <= 2) { warningText = '
⚠️ 次数即将用完
'; } else if (c.cardType === 'time' && (c.remainingHours || 0) <= 2) { warningText = '
⚠️ 时长即将耗尽
'; } else if (c.cardType === 'package') { const lowItems = (c.packageItems || []).filter(p => (p.remainingCount || 0) <= 0); if (lowItems.length > 0) { warningText = '
⚠️ ' + lowItems.length + '项已用完
'; } } if (c.cardType === 'count') remainingText = '剩余' + (c.remainingCount || 0) + ''; else if (c.cardType === 'time') remainingText = '剩余' + (c.remainingHours || 0) + 'h'; else { const items = (c.packageItems || []).map(p => p.serviceName + '×' + (p.remainingCount || 0)).join(' '); remainingText = items || '0'; } return '
' + '
' + tagText + '
' + '
' + c.cardName + '
' + '
' + remainingText + '
' + expireStatus + warningText + '
'; }).join(''); // SEC-S1-Task3: 异步加载历史卡券 this.loadHistoryCards(); }, openPurchaseCard() { const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (!member) return; this.selectedPurchaseCard = null; this.purchaseCardTotal = 0; this.purchasePayMethod = ''; this.purchaseCardSelectedEmployees = [{}]; // 初始化购卡选中的员工 document.getElementById('memberDetailPage').classList.remove('active'); document.getElementById('purchaseCardPage').classList.add('active'); const avatarBg = 'linear-gradient(135deg, #9B1B5E, #D4A843)'; document.getElementById('purchaseAvatar').style.background = avatarBg; document.getElementById('purchaseAvatar').textContent = member.name.charAt(0); document.getElementById('purchaseMemberName').textContent = member.name; document.getElementById('purchaseMemberPhone').textContent = this.maskPhone(member.phone); this.renderPurchaseCardList(); this.updatePurchaseTotal(); // 初始隐藏提成区域(等选了卡券后再渲染) document.getElementById('purchaseCardCommissionArea').style.display = 'none'; }, closePurchaseCard() { document.getElementById('purchaseCardPage').classList.remove('active'); document.getElementById('memberDetailPage').classList.add('active'); if (this._fromQuickAction) { this._fromQuickAction = false; document.getElementById('memberDetailPage').classList.remove('active'); if (typeof switchTab === 'function') switchTab('home'); } }, renderPurchaseCardCommissionArea(templateId) { // 筛选购卡提成规则 const rules = DataManager.getCommissionRules().filter(r => String(r.targetType) === 'card' && String(r.targetId) === String(templateId) && r.enabled === true ); const area = document.getElementById('purchaseCardCommissionArea'); const rowsContainer = document.getElementById('purchaseCardCommissionRows'); const employees = DataManager.getEmployeeItems().filter(e => e.status === 'active'); // 始终显示提成区域(即使无规则也需记录员工),无员工时才隐藏 if (employees.length === 0) { area.style.display = 'none'; return; } // 初始化选中员工数组 if (!this.purchaseCardSelectedEmployees) { this.purchaseCardSelectedEmployees = [{}]; } const purchaseAmount = this.selectedPurchaseCard ? parseFloat(this.selectedPurchaseCard.price) || 0 : 0; let html = ''; this.purchaseCardSelectedEmployees.forEach((sel, idx) => { const empId = sel.employeeId || ''; const emp = employees.find(e => String(e.id) === String(empId)); let commissionText = ''; if (emp) { const rule = rules.find(r => String(r.positionId) === '0') || rules[0]; if (rule) { // [SEC-COMM-PRECISION-2DP-PREVIEW] 保留 2 位精度 const commissionAmount = rule.commissionType === 'percentage' ? Math.round(purchaseAmount * rule.commissionValue) / 100 : rule.commissionValue; commissionText = rule.commissionType === 'percentage' ? `提成${rule.commissionValue}% ¥${Number(commissionAmount).toFixed(2)}` : `固定¥${Number(rule.commissionValue).toFixed(2)}`; } else if (rules.length > 0) { commissionText = '无匹配规则'; } } const otherSelectedIds = this.purchaseCardSelectedEmployees .filter((s, i) => i !== idx && s.employeeId) .map(s => String(s.employeeId)); html += `
${commissionText ? `${commissionText}` : ''} ${this.purchaseCardSelectedEmployees.length > 1 ? `
` : ''}
`; }); rowsContainer.innerHTML = html; area.style.display = 'block'; // 恢复已选择的值 rowsContainer.querySelectorAll('select.commission-emp-select').forEach(sel => { const idx = parseInt(sel.getAttribute('data-row-idx')); if (this.purchaseCardSelectedEmployees[idx] && this.purchaseCardSelectedEmployees[idx].employeeId) { sel.value = this.purchaseCardSelectedEmployees[idx].employeeId; } }); }, addPurchaseCardCommissionRow() { if (!this.purchaseCardSelectedEmployees) this.purchaseCardSelectedEmployees = [{}]; this.purchaseCardSelectedEmployees.push({}); if (this.selectedPurchaseCard) this.renderPurchaseCardCommissionArea(this.selectedPurchaseCard.id); }, removePurchaseCardCommissionRow(idx) { this.purchaseCardSelectedEmployees.splice(idx, 1); if (this.purchaseCardSelectedEmployees.length === 0) this.purchaseCardSelectedEmployees = [{}]; if (this.selectedPurchaseCard) this.renderPurchaseCardCommissionArea(this.selectedPurchaseCard.id); }, onPurchaseCardEmployeeChange(rowIdx, employeeId) { if (!this.purchaseCardSelectedEmployees) this.purchaseCardSelectedEmployees = [{}]; this.purchaseCardSelectedEmployees[rowIdx] = { employeeId: employeeId || '' }; if (this.selectedPurchaseCard) this.renderPurchaseCardCommissionArea(this.selectedPurchaseCard.id); }, renderPurchaseCardList() { const cards = DataManager.getCardItems(); const container = document.getElementById('purchaseCardList'); if (!cards.length) { container.innerHTML = '
暂无可购买卡券
'; return; } container.innerHTML = cards.map(c => { const isSelected = this.selectedPurchaseCard && String(this.selectedPurchaseCard.id) === String(c.id); const typeClass = c.type === 'count' ? 'count' : (c.type === 'time' ? 'time' : 'package'); const typeText = c.type === 'count' ? '计次' : (c.type === 'time' ? '计时' : '套餐'); return `
${escapeHtml(c.name)}${typeText}
${escapeHtml(c.description || (c.type === 'count' ? (c.count || 0) + '次' : (c.type === 'time' ? (c.duration || 0) + '小时' : '套餐')))}
¥${c.price}
${isSelected ? '' : ''}
`; }).join(''); // 重置付款方式标签 const payTabs = document.querySelectorAll('#purchasePayMethodTabs .pay-method-tag'); payTabs.forEach(tab => tab.classList.remove('selected')); this.purchasePayMethod = ''; }, selectPurchaseCard(cardId) { const cards = DataManager.getCardItems(); const card = cards.find(c => String(c.id) === String(cardId)); if (!card) return; if (this.selectedPurchaseCard && String(this.selectedPurchaseCard.id) === String(cardId)) { this.selectedPurchaseCard = null; this.purchaseCardSelectedEmployees = [{}]; // 清空选中的员工 } else { this.selectedPurchaseCard = card; this.purchaseCardSelectedEmployees = [{}]; // 重置选中的员工 } this.renderPurchaseCardList(); this.updatePurchaseTotal(); // 渲染购卡提成区域 if (this.selectedPurchaseCard) { this.renderPurchaseCardCommissionArea(this.selectedPurchaseCard.id); } else { document.getElementById('purchaseCardCommissionArea').style.display = 'none'; } }, selectPurchasePayMethod(method) { this.purchasePayMethod = method; const payTabs = document.querySelectorAll('#purchasePayMethodTabs .pay-method-tag'); payTabs.forEach(tab => { if (tab.textContent.trim() === method) { tab.classList.add('selected'); } else { tab.classList.remove('selected'); } }); }, updatePurchaseTotal() { const total = this.selectedPurchaseCard ? (this.selectedPurchaseCard.price || 0) : 0; this.purchaseCardTotal = total; document.getElementById('purchaseCardCount').textContent = this.selectedPurchaseCard ? 1 : 0; document.getElementById('purchaseCardOriginalPrice').textContent = total; document.getElementById('purchaseActualAmount').value = total; }, onActualAmountChange() { const input = document.getElementById('purchaseActualAmount'); let val = parseFloat(input.value); if (isNaN(val) || val < 0) val = 0; this.purchaseCardTotal = Math.round(val); }, confirmPurchaseCard() { if (this.isSubmitting) return; this.isSubmitting = true; const purchaseConfirmBtn = document.querySelector('#purchaseCardPage .confirm-btn'); if (purchaseConfirmBtn) { purchaseConfirmBtn.disabled = true; purchaseConfirmBtn.style.opacity = '0.6'; } if (!this.selectedPurchaseCard) { alert('请选择要购买的卡券'); this.isSubmitting = false; if (purchaseConfirmBtn) { purchaseConfirmBtn.disabled = false; purchaseConfirmBtn.style.opacity = '1'; } return; } if (!this.purchasePayMethod) { alert('请选择付款方式'); this.isSubmitting = false; if (purchaseConfirmBtn) { purchaseConfirmBtn.disabled = false; purchaseConfirmBtn.style.opacity = '1'; } return; } const purchaseId = this.currentDetailId; const member = this.members.find(m => String(m.id) === String(purchaseId)); if (!member) { this.isSubmitting = false; if (purchaseConfirmBtn) { purchaseConfirmBtn.disabled = false; purchaseConfirmBtn.style.opacity = '1'; } return; } // 只有会员余额才扣减余额并检查余额是否充足 if (this.purchasePayMethod === '会员余额') { if (member.balance < this.purchaseCardTotal) { alert('余额不足!当前余额¥' + member.balance); this.isSubmitting = false; if (purchaseConfirmBtn) { purchaseConfirmBtn.disabled = false; purchaseConfirmBtn.style.opacity = '1'; } return; } member.balance -= this.purchaseCardTotal; DataManager.updateMember(member.id, { balance: member.balance }); } const card = this.selectedPurchaseCard; const services = DataManager.getServiceItems(); const heldCard = { memberId: purchaseId, templateId: card.id, cardName: card.name, cardType: card.type, totalPrice: card.price }; if (card.type === 'count') { heldCard.remainingCount = card.count || 0; heldCard.totalCount = card.count || 0; } else if (card.type === 'time') { heldCard.remainingHours = card.duration || 0; heldCard.totalHours = card.duration || 0; } else { heldCard.packageItems = (card.packageItems || []).map(p => { const svc = services.find(s => String(s.id) === String(p.serviceId)); return { serviceId: p.serviceId, serviceName: svc ? svc.name : '', totalCount: p.count, remainingCount: p.count }; }); } DataManager.addMemberHeldCard(heldCard); // 自动计算到期日 if (card.validity && card.validity > 0) { const purchaseDate = new Date(); const expireDate = new Date(purchaseDate); expireDate.setMonth(expireDate.getMonth() + card.validity); heldCard.purchaseDate = purchaseDate.toISOString().split('T')[0]; heldCard.expireDate = expireDate.toISOString().split('T')[0]; DataManager.updateMemberHeldCard(heldCard.id, heldCard); } // 累计积分 DataManager.updateMemberPoints(member.id, Math.floor(this.purchaseCardTotal)); const orderNo = DataManager.generateOrderNo(); DataManager.addMemberRecord({ memberId: purchaseId, type: '购卡', itemName: '购买' + card.name, amount: -this.purchaseCardTotal, date: new Date().toISOString(), payMethod: this.purchasePayMethod || '', remark: '购买卡券', cardTemplateId: card.id, actualPrice: this.purchaseCardTotal, orderNo: orderNo }); // 计算购卡提成(多员工) (this.purchaseCardSelectedEmployees || []).forEach(sel => { if (!sel.employeeId) return; const employee = DataManager.getEmployeeItems().find(e => String(e.id) === String(sel.employeeId)); if (employee) { const commissionResult = DataManager.calculateCommission(employee.positionId, 'card', card.id, this.purchaseCardTotal); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission(sel.employeeId, member.id, '购卡提成', card.name, this.purchaseCardTotal, commissionResult, this.purchasePayMethod, orderNo); } } }); this.isSubmitting = false; if (purchaseConfirmBtn) { purchaseConfirmBtn.disabled = false; purchaseConfirmBtn.style.opacity = '1'; } const closeId = purchaseId; this.closePurchaseCard(); this.members = DataManager.getMembers(); this.openDetail(closeId); this.renderMemberHeldCards(); // 显示成功弹窗(带打印小票按钮) this.showSuccessModal({ title: '购卡成功', message: card.name + ' 购买成功', type: '购卡', recordData: { orderNo: orderNo, memberName: member.name, type: '购卡', itemName: '购买' + card.name, amount: this.purchaseCardTotal, payMethod: this.purchasePayMethod || '', remark: '购买卡券' } }); }, selectConsumePayMethod(method) { this.consumePayMethod = method; this.updateConsumePayMethodUI(); if (method === '卡券扣次') { document.getElementById('cardDeductSection').style.display = 'block'; document.getElementById('cardDeductTotal').style.display = 'flex'; document.getElementById('discountInput').style.display = 'none'; document.querySelector('.discount-title').style.display = 'none'; document.getElementById('discountTip').style.display = 'none'; document.querySelector('.total-amount-row').style.display = 'none'; this.renderCardDeductList(); } else { document.getElementById('cardDeductSection').style.display = 'none'; document.getElementById('cardDeductTotal').style.display = 'none'; document.getElementById('discountInput').style.display = ''; document.querySelector('.discount-title').style.display = ''; document.getElementById('discountTip').style.display = ''; document.querySelector('.total-amount-row').style.display = ''; document.getElementById('cardDeductCommissionArea').style.display = 'none'; this.selectedDeductCard = null; this.selectedPackageItems = []; this.cardDeductEmployees = {}; } }, renderCardDeductList() { const memberId = this.currentConsumeId; const heldCards = DataManager.getMemberHeldCards(memberId); const container = document.getElementById('cardDeductList'); if (heldCards.length === 0) { container.innerHTML = '
暂无可用卡券
'; return; } const selectedServices = this.selectedServices; const serviceIds = selectedServices.map(s => String(s.id)); container.innerHTML = heldCards.map(card => { const isSelected = this.selectedDeductCard && String(this.selectedDeductCard.id) === String(card.id); let usable = true; let reason = ''; let deductInfo = ''; if (card.cardType === 'count') { if (card.remainingCount <= 0) { usable = false; reason = '次数已用完'; } else { deductInfo = '扣1次(剩' + (card.remainingCount - 1) + '次)'; } } else if (card.cardType === 'time') { if (card.remainingHours <= 0) { usable = false; reason = '时长已用完'; } else { deductInfo = '当前剩余' + card.remainingHours + '小时'; } } else if (card.cardType === 'package') { if (!card.packageItems || card.packageItems.length === 0) { usable = false; reason = '套餐无项目'; } else { const hasAnyRemaining = card.packageItems.some(p => (p.remainingCount || 0) > 0); if (!hasAnyRemaining) { usable = false; reason = '套餐项目已全部用完'; } else { const availItems = card.packageItems.filter(p => (p.remainingCount || 0) > 0); deductInfo = availItems.map(p => p.serviceName + '(剩' + p.remainingCount + '次)').join('、'); const usedItems = card.packageItems.filter(p => (p.remainingCount || 0) <= 0); if (usedItems.length > 0) { deductInfo += ';' + usedItems.map(p => p.serviceName + '(已用完)').join('、'); } } } } const cardBg = card.cardType === 'count' ? 'linear-gradient(135deg,#E8B4B8,#D4919A)' : card.cardType === 'time' ? 'linear-gradient(135deg,#F0D68A,#E8C252)' : 'linear-gradient(135deg,#C9B3E0,#A98BD6)'; const typeLabel = card.cardType === 'count' ? '计次' : card.cardType === 'time' ? '计时' : '套餐'; return '
' + '
' + '
' + typeLabel + '
' + '
' + '
' + card.cardName + '
' + '
' + (deductInfo || (usable ? '' : reason)) + '
' + '
' + (isSelected ? '
✓ 已选
' : '') + (!usable ? '
' + reason + '
' : '') + '
' + (isSelected ? (() => { if (card.cardType === 'count') { // 计次卡:扣次数量选择器 return '
' + '
' + '扣减次数' + '
' + '
' + '
' + this.deductCount + '
' + '
+
' + '
' + '剩余' + card.remainingCount + '次' + '
'; } else if (card.cardType === 'time') { // 计时卡:扣减时长选择器 return '
' + '
' + '扣减时长' + '
' + '
' + '
' + this.deductHours + 'h
' + '
+
' + '
' + '剩余' + card.remainingHours + '小时' + '
'; } else if (card.cardType === 'package' && card.packageItems && card.packageItems.length > 0) { // 套餐卡:项目勾选框(保持不变) return '
' + card.packageItems.map(pi => { const piChecked = this.selectedPackageItems.includes(String(pi.serviceId)); const piDisabled = (pi.remainingCount || 0) <= 0; return '
' + '
' + (piChecked ? '' : '') + '
' + '' + pi.serviceName + '' + '' + (piDisabled ? '已用完' : '剩' + pi.remainingCount + '次') + '' + '
'; }).join('') + '
'; } return ''; })() : ''); }).join(''); this.updateCardDeductTotal(); this.renderCardDeductCommission(this.selectedDeductCard); }, selectDeductCard(cardId) { const heldCards = DataManager.getMemberHeldCards(this.currentConsumeId); const card = heldCards.find(c => String(c.id) === String(cardId)); if (!card) return; if (this.selectedDeductCard && String(this.selectedDeductCard.id) === String(cardId)) { this.selectedDeductCard = null; this.selectedPackageItems = []; this.cardDeductEmployees = {}; } else { this.selectedDeductCard = card; this.selectedPackageItems = []; this.cardDeductEmployees = {}; // 初始化扣减数量 if (card.cardType === 'count') { this.deductCount = 1; } else if (card.cardType === 'time') { this.deductHours = 1; } } this.renderCardDeductList(); this.updateCardDeductTotal(); }, togglePackageItem(serviceId) { const idx = this.selectedPackageItems.indexOf(String(serviceId)); if (idx >= 0) { this.selectedPackageItems.splice(idx, 1); } else { this.selectedPackageItems.push(String(serviceId)); } this.renderCardDeductList(); this.updateCardDeductTotal(); this.renderCardDeductCommission(this.selectedDeductCard); }, adjustDeductCount(delta) { const card = this.selectedDeductCard; if (!card || card.cardType !== 'count') return; this.deductCount = Math.max(1, Math.min(this.deductCount + delta, Math.min(card.remainingCount, 5))); this.renderCardDeductList(); this.updateCardDeductTotal(); this.renderCardDeductCommission(this.selectedDeductCard); }, adjustDeductHours(delta) { const card = this.selectedDeductCard; if (!card || card.cardType !== 'time') return; this.deductHours = Math.max(1, Math.min(this.deductHours + delta, Math.min(card.remainingHours, 5))); this.renderCardDeductList(); this.updateCardDeductTotal(); this.renderCardDeductCommission(this.selectedDeductCard); }, // 获取卡券关联的服务ID列表(用于提成计算) getCardServiceIds(card) { if (!card) return []; if (card.cardType === 'package' && card.packageItems) { // 套餐卡:每个项目都有serviceId return card.packageItems.map(p => String(p.serviceId)); } // 计次卡/计时卡:尝试从卡模板获取关联服务 const cardItems = DataManager.getCardItems(); const template = cardItems.find(c => String(c.id) === String(card.cardTemplateId)); if (template && template.packageItems && template.packageItems.length > 0) { return template.packageItems.map(p => String(p.serviceId)); } // 从卡名尝试匹配服务名 const services = DataManager.getServiceItems(); const cardName = card.cardName || ''; // 移除常见前后缀:数字+次, 卡, 计次卡, 计时卡 等 let serviceName = cardName.replace(/^\d+次/, '').replace(/卡$/, '').replace(/计次$/, '').replace(/计时$/, '').trim(); if (serviceName) { const matched = services.find(s => serviceName.includes(s.name) || s.name.includes(serviceName)); if (matched) return [String(matched.id)]; } return []; }, // 卡券划扣提成员工选择变化(支持多次/多项目分别选员工) // deductionKey: 计次/计时卡为索引"0","1"...; 套餐卡为serviceId onCardDeductEmployeeChange(deductionKey, positionId, employeeId) { if (!this.cardDeductEmployees[deductionKey]) this.cardDeductEmployees[deductionKey] = {}; this.cardDeductEmployees[deductionKey][positionId] = employeeId; }, // 渲染卡券划扣提成员工选择区域(支持多次/多项目分别选员工) renderCardDeductCommission(card) { const area = document.getElementById('cardDeductCommissionArea'); if (!card || !this.selectedDeductCard) { area.style.display = 'none'; area.innerHTML = ''; return; } const positions = DataManager.getEmployeePositions(); const employees = DataManager.getEmployeeItems().filter(e => e.status === 'active'); const commissionRules = DataManager.getCommissionRules(); const services = DataManager.getServiceItems(); // 获取该卡关联的服务ID const serviceIds = this.getCardServiceIds(card); // 找出有提成规则的职位(关联服务的service提成规则) const enabledPositionIds = [...new Set( commissionRules .filter(r => r.enabled && r.targetType === 'service' && serviceIds.some(sid => String(r.targetId) === sid)) .map(r => String(r.positionId)) )]; if (enabledPositionIds.length === 0) { area.style.display = 'none'; area.innerHTML = ''; return; } // 确定提成列数和列标题 let deductKeys = []; // 每列的key let deductLabels = []; // 每列的标题 let deductServiceIds = []; // 每列对应的服务ID(用于提成计算) if (card.cardType === 'count') { const cnt = this.deductCount || 1; for (let i = 0; i < cnt; i++) { deductKeys.push(String(i)); deductLabels.push('第' + (i + 1) + '次'); deductServiceIds.push(serviceIds); // 计次卡所有列用相同的服务 } } else if (card.cardType === 'time') { const hrs = this.deductHours || 1; for (let i = 0; i < hrs; i++) { deductKeys.push(String(i)); deductLabels.push('第' + (i + 1) + '小时'); deductServiceIds.push(serviceIds); } } else if (card.cardType === 'package') { // 套餐卡:每个选中的项目一列 for (const svcId of this.selectedPackageItems) { const pItem = (card.packageItems || []).find(p => String(p.serviceId) === String(svcId)); deductKeys.push(String(svcId)); deductLabels.push(pItem ? pItem.serviceName : ''); deductServiceIds.push([String(svcId)]); } } if (deductKeys.length === 0) { area.style.display = 'none'; area.innerHTML = ''; return; } area.style.display = 'block'; // 固定列宽:职位标签56px,每列选择器固定70px,提成比例40px const colFixedWidth = 70; let html = '
'; html += '
提成员工
'; // 外层可横向滚动容器(2列以上时才滚动) html += '
'; // 内层固定最小宽度表格 const innerMinWidth = 56 + deductKeys.length * colFixedWidth + 40; html += '
'; // 表头行:职位 | 第1次 | 第2次 | ... html += '
'; html += '职位'; deductLabels.forEach(label => { html += '' + label + ''; }); html += '提成'; html += '
'; // 每个职位一行:职位名 | 选择员工1 | 选择员工2 | ... enabledPositionIds.forEach(posId => { const pos = positions.find(p => String(p.id) === posId); if (!pos) return; const posEmployees = employees.filter(e => String(e.positionId) === posId); if (posEmployees.length === 0) return; // 找该职位对关联服务的提成规则 const matchedServiceId = serviceIds.find(sid => commissionRules.some(r => String(r.positionId) === posId && r.targetType === 'service' && String(r.targetId) === sid && r.enabled) ); const rule = commissionRules.find(r => String(r.positionId) === posId && r.targetType === 'service' && String(r.targetId) === matchedServiceId && r.enabled); let commissionText = ''; if (rule) { let amount = 0; const svc = services.find(s => String(s.id) === matchedServiceId); if (svc) amount = parseFloat(svc.price) || 0; // [SEC-COMM-PRECISION-2DP-PREVIEW] 保留 2 位精度 const commissionAmount = rule.commissionType === 'percentage' ? Math.round(amount * rule.commissionValue) / 100 : rule.commissionValue; commissionText = rule.commissionType === 'percentage' ? rule.commissionValue + '%' : '¥' + Number(rule.commissionValue).toFixed(2); } html += '
'; html += '' + pos.name + ''; // 每列一个选择器,固定宽度 deductKeys.forEach((dKey, colIdx) => { const selectedEmpId = (this.cardDeductEmployees[dKey] && this.cardDeductEmployees[dKey][posId]) || ''; html += ''; }); // 提成比例固定列 html += '' + (commissionText || '') + ''; html += '
'; }); html += '
'; // end inner table html += '
'; // end scroll container // 多列时提示可滑动 if (deductKeys.length > 2) { html += '
← 左右滑动查看更多 →
'; } html += '
'; area.innerHTML = html; }, updateCardDeductTotal() { const countText = document.getElementById('cardDeductCountText'); if (!this.selectedDeductCard) { countText.textContent = '未选择卡券'; return; } const card = this.selectedDeductCard; let text = ''; if (card.cardType === 'count') { text = card.cardName + ' 扣' + this.deductCount + '次'; } else if (card.cardType === 'time') { text = card.cardName + ' 扣' + this.deductHours + '小时'; } else { // 套餐卡:根据选中的项目显示 if (this.selectedPackageItems.length > 0) { const matchedItems = (card.packageItems || []).filter(p => this.selectedPackageItems.includes(String(p.serviceId))); text = matchedItems.map(p => p.serviceName + '扣1次').join('、'); } else { text = '请选择要扣减的项目'; } } countText.textContent = text; }, updateConsumePayMethodUI() { const tabs = document.querySelectorAll('#consumePage .pay-method-tag'); tabs.forEach(tag => { const methodText = tag.textContent.trim().split('会员余额')[0].trim(); tag.classList.remove('selected'); if (methodText === this.consumePayMethod || (this.consumePayMethod === '会员余额' && tag.textContent.includes('会员余额'))) { tag.classList.add('selected'); } }); }, updateConsumeTotal() { const discountVal = document.getElementById('discountInput').value.trim(); let discountRate = 100; if (discountVal) { let cleanVal = discountVal.replace(/折/g, '').trim(); let num = parseFloat(cleanVal); if (!isNaN(num)) { if (num > 0 && num <= 10) { discountRate = Math.round(num * 10); } else if (num > 10 && num <= 100) { discountRate = Math.round(num); } } } this.consumeDiscount = discountRate; const serviceOriginal = this.selectedServices.reduce((sum, s) => sum + (parseFloat(s.price) || 0), 0); const serviceFinal = Math.round(serviceOriginal * discountRate) / 100; const productOriginal = this.selectedProducts.reduce((sum, p) => sum + (parseFloat(p.price) || 0) * (parseInt(p.qty) || 1), 0); const productRate = (this.productDiscount || 100) / 100; const productFinal = Math.round(productOriginal * productRate * 100) / 100; const finalTotal = Math.round((serviceFinal + productFinal) * 100) / 100; document.getElementById('consumeTotal').textContent = `¥${finalTotal}`; document.getElementById('confirmConsumeBtn').textContent = `确认消费 ¥${finalTotal}`; }, // ==================== 已选产品渲染 ==================== renderSelectedProducts() { const listEl = document.getElementById('selectedProductList'); if (!listEl) return; if (!this.selectedProducts || this.selectedProducts.length === 0) { listEl.innerHTML = '
暂未添加产品
'; return; } const positions = DataManager.getEmployeePositions(); const rules = DataManager.getCommissionRules(); const employees = DataManager.getEmployeeItems().filter(e => e.status === 'active'); const productRate = (this.productDiscount || 100) / 100; listEl.innerHTML = this.selectedProducts.map((p, idx) => { const qty = parseInt(p.qty) || 1; const unitPrice = parseFloat(p.price) || 0; const discountedUnit = Math.round(unitPrice * productRate * 100) / 100; const subTotal = Math.round(discountedUnit * qty * 100) / 100; // 查找产品提成规则 const productRules = rules.filter(r => r.targetType === 'product' && String(r.targetId) === String(p.id) && r.enabled ); // 初始化 commissionEmployees 数组(兼容旧数据) if (!Array.isArray(this.selectedProducts[idx].commissionEmployees)) { this.selectedProducts[idx].commissionEmployees = [{}]; } if (this.selectedProducts[idx].commissionEmployees.length === 0) { this.selectedProducts[idx].commissionEmployees.push({}); } let commissionHtml = ''; if (employees.length > 0) { // 提成规则提示(多职位时显示所有职位规则) let ruleHintHtml = ''; if (productRules.length > 0) { const _r0 = productRules[0]; const _txt = _r0.commissionType === 'percentage' ? _r0.commissionValue + '%' : '固定¥' + _r0.commissionValue; ruleHintHtml = `
提成规则:${_txt}
`; } commissionHtml = `
提成员工
${ruleHintHtml}`; const selectedEmps = this.selectedProducts[idx].commissionEmployees; selectedEmps.forEach((sel, rowIdx) => { const empId = sel.employeeId || ''; const emp = employees.find(e => String(e.id) === String(empId)); let commissionText = ''; if (emp) { const rule = productRules.find(r => String(r.positionId) === '0') || productRules[0]; if (rule) { // [SEC-COMM-PRECISION-2DP-PREVIEW] 保留 2 位精度 const commissionAmount = rule.commissionType === 'percentage' ? Math.round(discountedUnit * qty * rule.commissionValue) / 100 : rule.commissionValue * qty; commissionText = rule.commissionType === 'percentage' ? `提成${rule.commissionValue}% ¥${Number(commissionAmount).toFixed(2)}` : `固定¥${Number(rule.commissionValue * qty).toFixed(2)}`; } else { commissionText = '无规则'; } } const otherSelectedIds = selectedEmps .filter((s, i) => i !== rowIdx && s.employeeId) .map(s => String(s.employeeId)); commissionHtml += `
${commissionText ? `${commissionText}` : ''} ${selectedEmps.length > 1 ? `
` : ''}
`; }); commissionHtml += `
添加员工
`; } return `
${escapeHtml(p.name)}
${escapeHtml(p.categoryName || '')} · 单价¥${unitPrice}${productRate < 1 ? ' × ' + Math.round(productRate * 100) + '%' : ''}
${qty} ¥${subTotal}
${commissionHtml}
`; }).join(''); }, onProductEmployeeChange(idx, rowIdx, employeeId) { const p = this.selectedProducts[idx]; if (!p) return; if (!Array.isArray(p.commissionEmployees)) p.commissionEmployees = [{}]; if (!p.commissionEmployees[rowIdx]) p.commissionEmployees[rowIdx] = {}; p.commissionEmployees[rowIdx].employeeId = employeeId || ''; this.renderSelectedProducts(); }, addProductCommissionRow(idx) { const p = this.selectedProducts[idx]; if (!p) return; if (!Array.isArray(p.commissionEmployees)) p.commissionEmployees = [{}]; p.commissionEmployees.push({}); this.renderSelectedProducts(); }, removeProductCommissionRow(idx, rowIdx) { const p = this.selectedProducts[idx]; if (!p || !Array.isArray(p.commissionEmployees)) return; p.commissionEmployees.splice(rowIdx, 1); if (p.commissionEmployees.length === 0) p.commissionEmployees = [{}]; this.renderSelectedProducts(); }, adjustProductQty(idx, delta) { const p = this.selectedProducts[idx]; if (!p) return; const newQty = (parseInt(p.qty) || 1) + delta; if (newQty < 1) return; // 库存校验 const item = DataManager.getProductItems().find(x => String(x.id) === String(p.id)); const stock = item ? (parseInt(item.stock) || 0) : 0; if (newQty > stock) { alert('库存不足!剩余库存:' + stock + '件'); return; } p.qty = newQty; this.renderSelectedProducts(); this.updateConsumeTotal(); }, removeSelectedProduct(idx) { this.selectedProducts.splice(idx, 1); this.renderSelectedProducts(); this.updateConsumeTotal(); }, // ==================== 产品选择器 ==================== openProductPicker() { const cats = DataManager.getProductCategories(); const items = DataManager.getProductItems().filter(i => i.enabled); this.productPickerData = items; const tabsEl = document.getElementById('productPickerCategoryTabs'); tabsEl.innerHTML = `
全部
${cats.map(c => `
${escapeHtml(c.name)}
`).join('')} `; // 重置搜索/分类 this._productPickerKeyword = ''; this._productPickerCatId = '全部'; var _prdSearch = document.getElementById('productPickerSearch'); if (_prdSearch) _prdSearch.value = ''; var _prdClear = document.getElementById('productPickerSearchClear'); if (_prdClear) _prdClear.classList.remove('show'); this._applyProductPickerFilter(); document.getElementById('productPickerModal').classList.add('active'); document.getElementById('confirmProductBtn').textContent = `确认添加(${this.selectedProducts.length}项)`; }, closeProductPicker() { document.getElementById('productPickerModal').classList.remove('active'); }, filterProductCategory(catId) { document.querySelectorAll('#productPickerCategoryTabs .service-category-tab').forEach(tab => { tab.classList.remove('selected'); if (tab.dataset.id === catId) tab.classList.add('selected'); }); this._productPickerCatId = catId; this._applyProductPickerFilter(); }, onProductPickerSearchInput(v) { this._productPickerKeyword = (v || '').trim(); var clr = document.getElementById('productPickerSearchClear'); if (clr) clr.classList.toggle('show', !!this._productPickerKeyword); this._applyProductPickerFilter(); }, clearProductPickerSearch() { var el = document.getElementById('productPickerSearch'); if (el) el.value = ''; this._productPickerKeyword = ''; var clr = document.getElementById('productPickerSearchClear'); if (clr) clr.classList.remove('show'); this._applyProductPickerFilter(); if (el) el.focus(); }, _applyProductPickerFilter() { var data = this.productPickerData || []; var cat = this._productPickerCatId || '全部'; var kw = (this._productPickerKeyword || '').toLowerCase(); var list = data; if (cat !== '全部') list = list.filter(function(i){ return String(i.categoryId) === String(cat); }); if (kw) list = list.filter(function(i){ return String(i.name||'').toLowerCase().indexOf(kw) >= 0; }); this.renderProductPickerList(list); }, renderProductPickerList(items) { const listEl = document.getElementById('productPickerList'); if (items.length === 0) { listEl.innerHTML = '
暂无可售产品
'; return; } const cats = DataManager.getProductCategories(); listEl.innerHTML = items.map(it => { const stock = parseInt(it.stock) || 0; const isSelected = this.selectedProducts.some(sel => String(sel.id) === String(it.id)); const outOfStock = stock <= 0; const lowStock = stock > 0 && stock <= 5; const catName = cats.find(c => String(c.id) === String(it.categoryId))?.name || ''; let btnHtml, itemClass; if (outOfStock) { itemClass = 'service-picker-item'; btnHtml = '
'; } else if (isSelected) { itemClass = 'service-picker-item selected'; btnHtml = `
`; } else { itemClass = 'service-picker-item'; btnHtml = `
`; } const stockTagStyle = outOfStock ? 'background:#ffebee;color:#f44336;' : (lowStock ? 'background:#fff3e0;color:#FF9800;' : 'background:#f0f9f4;color:#4CAF50;'); const stockTagText = outOfStock ? '已售罄' : (lowStock ? `库存${stock}(紧张)` : `库存${stock}`); return `
${escapeHtml(it.name)}
${escapeHtml(catName)} · ${stockTagText}
¥${it.price}
${btnHtml}
`; }).join(''); }, addProductToPicker(productId) { const it = this.productPickerData.find(x => String(x.id) === String(productId)); if (!it) return; const stock = parseInt(it.stock) || 0; if (stock <= 0) { alert('该产品已售罄'); return; } const exists = this.selectedProducts.find(p => String(p.id) === String(productId)); if (exists) { alert('该产品已添加,请直接调整数量'); return; } const cats = DataManager.getProductCategories(); const catName = cats.find(c => String(c.id) === String(it.categoryId))?.name || ''; this.selectedProducts.push({ id: it.id, name: it.name, price: parseFloat(it.price), qty: 1, categoryName: catName, commissionEmployees: [{}] }); document.getElementById('confirmProductBtn').textContent = `确认添加(${this.selectedProducts.length}项)`; this.renderProductPickerList(this.productPickerData); }, confirmProductPicker() { this.renderSelectedProducts(); this.updateConsumeTotal(); this.closeProductPicker(); }, openServicePicker() { // 获取服务分类和项目 const categories = DataManager.getServiceCategories(); const services = DataManager.getServiceItems(); // 渲染分类标签 const categoryTabsEl = document.getElementById('serviceCategoryTabs'); categoryTabsEl.innerHTML = `
全部
${categories.map(c => `
${escapeHtml(c.name)}
`).join('')} `; // 渲染服务列表 this.servicePickerData = services; // 重置搜索/分类 this._servicePickerKeyword = ''; this._servicePickerCatId = '全部'; var _svcSearch = document.getElementById('servicePickerSearch'); if (_svcSearch) _svcSearch.value = ''; var _svcClear = document.getElementById('servicePickerSearchClear'); if (_svcClear) _svcClear.classList.remove('show'); this._applyServicePickerFilter(); document.getElementById('servicePickerModal').classList.add('active'); }, closeServicePicker() { document.getElementById('servicePickerModal').classList.remove('active'); }, filterServiceCategory(category) { // 只切换 servicePickerModal 内的分类标签,避免误伤 productPickerCategoryTabs document.querySelectorAll('#serviceCategoryTabs .service-category-tab').forEach(tab => { tab.classList.remove('selected'); if (tab.dataset.id === category) { tab.classList.add('selected'); } }); this._servicePickerCatId = category; this._applyServicePickerFilter(); }, onServicePickerSearchInput(v) { this._servicePickerKeyword = (v || '').trim(); var clr = document.getElementById('servicePickerSearchClear'); if (clr) clr.classList.toggle('show', !!this._servicePickerKeyword); this._applyServicePickerFilter(); }, clearServicePickerSearch() { var el = document.getElementById('servicePickerSearch'); if (el) el.value = ''; this._servicePickerKeyword = ''; var clr = document.getElementById('servicePickerSearchClear'); if (clr) clr.classList.remove('show'); this._applyServicePickerFilter(); if (el) el.focus(); }, _applyServicePickerFilter() { var data = this.servicePickerData || []; var cat = this._servicePickerCatId || '全部'; var kw = (this._servicePickerKeyword || '').toLowerCase(); var list = data; if (cat !== '全部') list = list.filter(function(s){ return String(s.categoryId) === String(cat); }); if (kw) list = list.filter(function(s){ return String(s.name||'').toLowerCase().indexOf(kw) >= 0; }); this.renderServicePickerList(list); }, renderServicePickerList(services) { const listEl = document.getElementById('servicePickerList'); if (services.length === 0) { listEl.innerHTML = '
暂无服务项目
'; return; } const categories = DataManager.getServiceCategories(); listEl.innerHTML = services.map(s => { const isSelected = this.selectedServices.some(sel => String(sel.id) === String(s.id)); const categoryName = categories.find(c => String(c.id) === String(s.categoryId))?.name || ''; const itemClass = isSelected ? 'service-picker-item selected' : 'service-picker-item'; const nameClass = isSelected ? 'service-picker-item-name selected' : 'service-picker-item-name'; const btnClass = isSelected ? 'service-added-btn' : 'service-add-btn'; const btnIcon = isSelected ? '' : ''; return `
${escapeHtml(s.name)}
${categoryName}
¥${s.price}
${btnIcon}
`}).join(''); }, addServiceToPicker(serviceId) { const service = this.servicePickerData.find(s => String(s.id) === String(serviceId)); if (!service) return; // 检查是否已添加 const exists = this.selectedServices.find(s => String(s.id) === String(serviceId)); if (exists) { alert('该服务已添加'); return; } const categories = DataManager.getServiceCategories(); const categoryName = categories.find(c => String(c.id) === String(service.categoryId))?.name || ''; this.selectedServices.push({ id: service.id, name: service.name, price: service.price, category: categoryName }); document.getElementById('confirmServiceBtn').textContent = `确认添加(${this.selectedServices.length}项)`; this.renderServicePickerList(this.servicePickerData); }, confirmServicePicker() { this.renderSelectedServices(); this.updateConsumeTotal(); this.closeServicePicker(); }, confirmConsume() { console.log('[confirmConsume] start, isSubmitting:', this.isSubmitting, 'payMethod:', this.consumePayMethod, 'selectedDeductCard:', !!this.selectedDeductCard, 'selectedPackageItems:', this.selectedPackageItems); if (this.isSubmitting) return; this.isSubmitting = true; const consumeConfirmBtn = document.querySelector('#consumePage .confirm-btn'); try { if (consumeConfirmBtn) { consumeConfirmBtn.disabled = true; consumeConfirmBtn.style.opacity = '0.6'; } if (this.selectedServices.length === 0 && this.selectedProducts.length === 0 && this.consumePayMethod !== '卡券扣次') { alert('请先添加服务项目或产品'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } // 产品库存校验(任何支付方式都校验) if (this.selectedProducts.length > 0) { const allProducts = DataManager.getProductItems(); for (const p of this.selectedProducts) { const item = allProducts.find(x => String(x.id) === String(p.id)); const stock = item ? (parseInt(item.stock) || 0) : 0; const qty = parseInt(p.qty) || 1; if (stock < qty) { alert('产品库存不足!「' + p.name + '」剩余库存:' + stock + '件,需购买:' + qty + '件'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } } } // 获取付款方式 const paymentMethod = this.consumePayMethod; // 获取备注内容 const remarkInput = document.getElementById('consumeRemarkInput'); const remarkContent = remarkInput ? remarkInput.value.trim() : ''; const member = this.members.find(m => String(m.id) === String(this.currentConsumeId)); if (!member) { this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } // 卡券扣次逻辑 if (this.consumePayMethod === '卡券扣次') { if (!this.selectedDeductCard) { alert('请选择要使用的卡券'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } const card = this.selectedDeductCard; const heldCards = DataManager.getMemberHeldCards(); const heldCard = heldCards.find(c => String(c.id) === String(card.id)); if (!heldCard) { alert('卡券不存在'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } // [SEC-CARD-ORDERNO-FIX-20260702] Bug 2 修复:orderNo 提前生成,用于 _apiSync heldCard.deduct 透传 // 让 kept_card_ledger.related_order_no 与 member_records.order_no 关联(原逻辑生成太晚,导致 apiSync 拿不到 orderNo,后端 fallback 到假 VOID-DED--) const cardDeductOrderNo = DataManager.generateOrderNo(); // 根据卡券类型处理扣次 if (heldCard.cardType === 'count') { const deductCnt = this.deductCount || 1; if (heldCard.remainingCount < deductCnt) { alert('剩余次数不足!当前剩余' + heldCard.remainingCount + '次,需扣' + deductCnt + '次'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } heldCard.remainingCount -= deductCnt; if (heldCard.remainingCount <= 0) { heldCard.status = 'used_up'; } // [SEC-CARD-CONSUME-UNIFIED-20260702] 计次卡:不再单独调 _apiSync heldCard.deduct // 改为随 addMemberRecord → POST /consume 一站式(传 heldCardId + deductTimes) } else if (heldCard.cardType === 'time') { const deductHrs = this.deductHours || 1; if (heldCard.remainingHours < deductHrs) { alert('卡券时长不足!当前剩余' + heldCard.remainingHours + '小时,需扣' + deductHrs + '小时'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } heldCard.remainingHours -= deductHrs; if (heldCard.remainingHours <= 0) { heldCard.status = 'used_up'; } // [SEC-CARD-CONSUME-UNIFIED-20260702] 计时卡:不再单独调 _apiSync heldCard.deduct // 改为随 addMemberRecord → POST /consume 一站式(传 heldCardId + deductHours) } else if (heldCard.cardType === 'package') { // 套餐卡:根据用户勾选的项目扣减次数 if (this.selectedPackageItems.length === 0) { alert('请选择要扣减的套餐项目'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } // [SEC-CARD-CONSUME-UNIFIED-20260702] 一站式 /consume 后端 packageItemKey 只支持单值 // 若一次勾选多项,请分次操作(后续可扩展后端支持数组) if (this.selectedPackageItems.length > 1) { alert('套餐卡一次只支持扣 1 项,请分次操作'); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } let pkgDeductItems = []; for (const svcId of this.selectedPackageItems) { const pItem = heldCard.packageItems.find(p => String(p.serviceId) === String(svcId)); if (!pItem) continue; if ((pItem.remainingCount || 0) <= 0) { alert('套餐项目次数已用完:' + pItem.serviceName); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } pItem.remainingCount -= 1; pkgDeductItems.push({ serviceId: svcId, count: 1 }); } // 检查是否所有项目都用完 const allUsedUp = heldCard.packageItems.every(p => (p.remainingCount || 0) <= 0); if (allUsedUp) { heldCard.status = 'used_up'; } // [SEC-CARD-CONSUME-UNIFIED-20260702] 套餐卡:不再单独调 _apiSync heldCard.deduct // 改为随 addMemberRecord → POST /consume 一站式(传 heldCardId + packageItemKey) } DataManager.updateMemberHeldCard(heldCard.id, heldCard); // 更新消费次数 member.consumeCount = (member.consumeCount || 0) + 1; DataManager.updateMember(member.id, { consumeCount: member.consumeCount }); // 添加消费记录 let serviceNames, serviceTotal; if (this.selectedServices.length > 0) { serviceNames = this.selectedServices.map(s => s.name).join('、'); serviceTotal = this.selectedServices.reduce((sum, s) => sum + (parseFloat(s.price) || 0), 0); } else if (heldCard.cardType === 'count') { serviceNames = heldCard.cardName + ' 扣' + (this.deductCount || 1) + '次'; serviceTotal = 0; } else if (heldCard.cardType === 'time') { serviceNames = heldCard.cardName + ' 扣' + (this.deductHours || 1) + '小时'; serviceTotal = 0; } else if (heldCard.cardType === 'package') { // 套餐卡 - 用勾选的项目名 const selectedPkgItems = heldCard.packageItems.filter(p => this.selectedPackageItems.includes(String(p.serviceId))); serviceNames = selectedPkgItems.map(p => p.serviceName).join('、'); serviceTotal = 0; // 套餐卡不扣金额 } else { serviceNames = heldCard.cardName; serviceTotal = 0; } const cardDeductOrderNo2 = cardDeductOrderNo; // [SEC-CARD-ORDERNO-FIX-20260702] 复用上方提前生成的 orderNo // [SEC-CARD-CONSUME-UNIFIED-20260702] 一站式 /consume 参数计算:单值 packageItemKey / deductTimes / deductHours const _unifiedDeductTimes = (heldCard.cardType === 'count') ? (this.deductCount || 1) : undefined; const _unifiedDeductHours = (heldCard.cardType === 'time') ? (this.deductHours || 1) : undefined; const _unifiedPackageItemKey = (heldCard.cardType === 'package' && this.selectedPackageItems.length === 1) ? String(this.selectedPackageItems[0]) : undefined; DataManager.addMemberRecord({ memberId: member.id, type: '消费', itemName: serviceNames, amount: -serviceTotal, originalAmount: serviceTotal, date: new Date().toISOString(), payMethod: '卡券扣次(' + heldCard.cardName + ')', remark: remarkContent || '卡券划扣', orderNo: cardDeductOrderNo2, // [SEC-CARD-CONSUME-UNIFIED-20260702] 一站式:让后端 /consume 统一处理持卡扣减+ledger+record heldCardId: heldCard.id, deductTimes: _unifiedDeductTimes, deductHours: _unifiedDeductHours, packageItemKey: _unifiedPackageItemKey }); // 计算服务项提成(按职位)- 如果卡券扣次的同时还添加了服务项目 // [SEC-COMM-FINAL] 附带服务也按 consumeDiscount 折后金额计提成 const employees = DataManager.getEmployeeItems(); const _svcDiscount2 = this.consumeDiscount || 100; for (const svc of this.selectedServices) { const originalAmount = parseFloat(svc.price) || 0; const finalAmount = Math.round(originalAmount * _svcDiscount2) / 100; if (originalAmount > 0 && svc.selectedEmployees) { for (const [positionId, empId] of Object.entries(svc.selectedEmployees)) { if (empId) { const emp = employees.find(e => String(e.id) === String(empId)); if (emp) { const commissionResult = DataManager.calculateCommission(positionId, 'service', svc.id, finalAmount); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission( empId, member.id, '服务提成', svc.name, originalAmount, commissionResult, paymentMethod, cardDeductOrderNo, finalAmount, _svcDiscount2 ); } } } } } } // 计算卡券划扣提成(按职位+按次/项)- 支持多次/多项目分别选不同员工 const cardServices = DataManager.getServiceItems(); const cardServiceIds = this.getCardServiceIds(heldCard); if (Object.keys(this.cardDeductEmployees).length > 0 && cardServiceIds.length > 0) { // cardDeductEmployees格式: {deductionKey: {positionId: employeeId}} // 计次/计时: deductionKey = "0","1"... (第几次/第几小时) // 套餐: deductionKey = serviceId for (const [deductKey, posMap] of Object.entries(this.cardDeductEmployees)) { if (!posMap || typeof posMap !== 'object') continue; for (const [positionId, empId] of Object.entries(posMap)) { if (!empId) continue; const emp = employees.find(e => String(e.id) === String(empId)); if (!emp) continue; if (heldCard.cardType === 'package') { // 套餐卡:deductKey就是serviceId // [SEC-COMM-PACKAGE-FILTER-20260702] 只给本次实际扣次的套餐项算提成,避免 cardDeductEmployees 状态残留(多勾后取消)导致 commission 多算 if (!this.selectedPackageItems.includes(String(deductKey))) continue; const svc = cardServices.find(s => String(s.id) === String(deductKey)); if (!svc) continue; const svcPrice = parseFloat(svc.price) || 0; const commissionResult = DataManager.calculateCommission(positionId, 'service', deductKey, svcPrice); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission( empId, member.id, '服务提成', svc.name + '(套餐划扣)', svcPrice, commissionResult, paymentMethod, cardDeductOrderNo ); } } else if (heldCard.cardType === 'count') { // 计次卡:deductKey是索引,每次用关联服务的价格 const deductIdx = parseInt(deductKey); for (const svcId of cardServiceIds) { const svc = cardServices.find(s => String(s.id) === String(svcId)); if (!svc) continue; const svcPrice = parseFloat(svc.price) || 0; const commissionResult = DataManager.calculateCommission(positionId, 'service', svcId, svcPrice); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission( empId, member.id, '服务提成', svc.name + '(第' + (deductIdx + 1) + '次)', svcPrice, commissionResult, paymentMethod, cardDeductOrderNo ); } } } else if (heldCard.cardType === 'time') { // 计时卡:deductKey是索引,每小时用关联服务的价格 const deductIdx = parseInt(deductKey); for (const svcId of cardServiceIds) { const svc = cardServices.find(s => String(s.id) === String(svcId)); if (!svc) continue; const svcPrice = parseFloat(svc.price) || 0; const commissionResult = DataManager.calculateCommission(positionId, 'service', svcId, svcPrice); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission( empId, member.id, '服务提成', svc.name + '(第' + (deductIdx + 1) + '小时)', svcPrice, commissionResult, paymentMethod, cardDeductOrderNo ); } } } } } } this.members = DataManager.getMembers(); // 显示成功弹窗(带打印小票按钮) this.showSuccessModal({ title: '消费成功', message: '使用卡券:' + heldCard.cardName + (heldCard.cardType === 'count' ? ' 扣' + (this.deductCount || 1) + '次' : heldCard.cardType === 'time' ? ' 扣' + (this.deductHours || 1) + '小时' : ''), type: '消费', recordData: { orderNo: cardDeductOrderNo, memberName: member.name, type: '消费', itemName: serviceNames, amount: serviceTotal, payMethod: '卡券扣次(' + heldCard.cardName + ')', remark: remarkContent || '卡券划扣' } }); // 卡券扣次场景:附加产品销售(产品按现金/微信/支付宝结算的另算,提示用户在常规支付方式下再下单) // 注:卡券扣次本身不结算产品金额,但允许同次记录产品提成与库存扣减 if (this.selectedProducts.length > 0) { this._processProductSale(member, paymentMethod === '卡券扣次' ? '现金' : paymentMethod, employees, remarkContent, true, cardDeductOrderNo); } this.selectedServices = []; this.selectedProducts = []; this.selectedDeductCard = null; this.selectedPackageItems = []; this.cardDeductEmployees = {}; this.deductCount = 1; this.deductHours = 1; this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } const consumeMemberId = this.currentConsumeId; this.closeConsume(); this.openDetail(consumeMemberId); return; } // 其他支付方式逻辑 const serviceOriginalTotal = this.selectedServices.reduce((sum, s) => sum + (parseFloat(s.price) || 0), 0); const serviceFinalTotal = Math.round(serviceOriginalTotal * this.consumeDiscount) / 100; const productOriginalTotal = this.selectedProducts.reduce((sum, p) => sum + (parseFloat(p.price) || 0) * (parseInt(p.qty) || 1), 0); const productFinalTotal = Math.round(productOriginalTotal * (this.productDiscount || 100)) / 100; const originalTotal = serviceOriginalTotal + productOriginalTotal; let finalTotal = Math.round((serviceFinalTotal + productFinalTotal) * 100) / 100; // 积分抵扣逻辑 let pointsDeducted = 0; if (this.usePointsDeduct && member.points > 0) { const maxDeduct = Math.floor(member.points / 100); // 最多可抵扣金额 pointsDeducted = Math.min(maxDeduct, finalTotal); if (pointsDeducted > 0) { finalTotal -= pointsDeducted; DataManager.updateMemberPoints(member.id, -(pointsDeducted * 100)); DataManager.addMemberRecord({ memberId: member.id, type: '积分', itemName: '积分抵扣消费', amount: -(pointsDeducted * 100), date: new Date().toISOString(), payMethod: '积分抵扣', remark: '抵扣¥' + pointsDeducted }); } } // 检查余额是否足够(会员余额支付时) if (this.consumePayMethod === '会员余额' && member.balance < finalTotal) { alert(`余额不足!当前余额¥${member.balance},消费金额¥${finalTotal}`); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } return; } // 扣减余额(会员余额支付时) if (this.consumePayMethod === '会员余额') { member.balance -= finalTotal; DataManager.updateMember(member.id, { balance: member.balance }); } // 更新消费次数 member.consumeCount = (member.consumeCount || 0) + 1; DataManager.updateMember(member.id, { consumeCount: member.consumeCount }); // 累计积分(按实际支付金额1元=1积分) DataManager.updateMemberPoints(member.id, Math.round(finalTotal)); // 添加消费记录 const serviceNames = this.selectedServices.map(s => s.name).join('、'); const productNames = this.selectedProducts.map(p => p.name + (p.qty > 1 ? '×' + p.qty : '')).join('、'); const allItemNames = [serviceNames, productNames].filter(x => x).join(' | '); const orderNo = DataManager.generateOrderNo(); // [SEC-MR-PRODUCT-DISCOUNT] 按订单类型决定 discount/productDiscount 写入 // 纯产品订单:discount 字段写产品折扣(修复历史脏数据:旧逻辑写 service_discount=80 但实际按 product_discount=90 算) // 其他订单:discount 字段保持服务折扣口径,productDiscount 单独记录产品折扣 const _hasSvc = this.selectedServices.length > 0; const _hasProd = this.selectedProducts.length > 0; const _mrDiscount = (!_hasSvc && _hasProd) ? (this.productDiscount || 100) : (this.consumeDiscount || 100); const _mrProductDiscount = _hasProd ? (this.productDiscount || 100) : null; DataManager.addMemberRecord({ memberId: member.id, type: '消费', itemName: allItemNames, amount: -finalTotal, originalAmount: originalTotal, date: new Date().toISOString(), payMethod: this.consumePayMethod || '会员余额', discount: _mrDiscount, productDiscount: _mrProductDiscount, serviceAmount: serviceFinalTotal, productAmount: productFinalTotal, productItems: this.selectedProducts.map(p => ({ id: p.id, name: p.name, price: p.price, qty: p.qty })), remark: remarkContent, orderNo: orderNo }); // 计算服务提成(按职位) // [SEC-COMM-FINAL] 提成基数改用折后实收金额 const employees = DataManager.getEmployeeItems(); const _svcDiscount = this.consumeDiscount || 100; for (const svc of this.selectedServices) { const originalAmount = parseFloat(svc.price) || 0; const finalAmount = Math.round(originalAmount * _svcDiscount) / 100; if (originalAmount > 0 && svc.selectedEmployees) { for (const [positionId, empId] of Object.entries(svc.selectedEmployees)) { if (empId) { const emp = employees.find(e => String(e.id) === String(empId)); if (emp) { const commissionResult = DataManager.calculateCommission(positionId, 'service', svc.id, finalAmount); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission( empId, member.id, '服务提成', svc.name, originalAmount, commissionResult, paymentMethod, orderNo, finalAmount, _svcDiscount ); } } } } } } // 产品销售:扣库存 + 产品提成(折扣后金额作为提成基数) if (this.selectedProducts.length > 0) { this._processProductSale(member, paymentMethod, employees, remarkContent, false, orderNo); } // 刷新会员列表 this.members = DataManager.getMembers(); // 显示成功弹窗(带打印小票按钮) let successMessage = '消费金额¥' + finalTotal; if (pointsDeducted > 0) { successMessage += '(积分抵扣¥' + pointsDeducted + ')'; } this.showSuccessModal({ title: '消费成功', message: successMessage, type: '消费', recordData: { orderNo: orderNo, memberName: member.name, type: '消费', itemName: serviceNames, amount: finalTotal, payMethod: this.consumePayMethod || '会员余额', discount: this.consumeDiscount < 100 ? this.consumeDiscount : null, remark: remarkContent } }); this.selectedServices = []; this.selectedProducts = []; this.usePointsDeduct = false; this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } const consumeMemberId = this.currentConsumeId; this.closeConsume(); this.openDetail(consumeMemberId); } catch(err) { console.error('[confirmConsume] ERROR:', err); this.isSubmitting = false; if (consumeConfirmBtn) { consumeConfirmBtn.disabled = false; consumeConfirmBtn.style.opacity = '1'; } alert('消费处理异常: ' + err.message + '\n\n' + (err.stack || '').split('\n').slice(0,3).join('\n')); } }, // ==================== 产品销售处理(扣库存+产品提成) ==================== _processProductSale(member, paymentMethod, employees, remarkContent, fromCardDeduct, orderNo) { const productItems = DataManager.getProductItems(); const productRate = (this.productDiscount || 100) / 100; for (const p of this.selectedProducts) { const qty = parseInt(p.qty) || 1; const unitPrice = parseFloat(p.price) || 0; const discountedUnit = Math.round(unitPrice * productRate * 100) / 100; const subTotal = Math.round(discountedUnit * qty * 100) / 100; // 扣库存 const item = productItems.find(x => String(x.id) === String(p.id)); if (item) { const newStock = (parseInt(item.stock) || 0) - qty; const _beforeStock = parseInt(item.stock) || 0; const _afterStock = Math.max(0, newStock); DataManager.updateProductItem(item.id, { stock: _afterStock }); // p0_patch13: 销售扣库存写出库流水 try { DataManager.addInventoryRecord({ productId: item.id, productName: item.name, type: '出库', qtyChange: _afterStock - _beforeStock, beforeStock: _beforeStock, afterStock: _afterStock, source: fromCardDeduct ? '卡券扣次同单' : '产品销售', orderNo: orderNo || '', memberName: member ? member.name : '', remark: `销售出库 ×${qty}` }); } catch(e) { console.warn('库存流水写入失败:', e); } } // 产品提成 (按 子总额=折扣后单价*数量 作为基数) // 多员工:每个员工按自身职位的rule独立计提成;同员工只算一次 // [SEC-COMM-FINAL] originalAmount 改填真正的原价(unitPrice*qty),finalAmount=subTotal if (subTotal > 0 && Array.isArray(p.commissionEmployees)) { const seen = new Set(); const _productOriginal = Math.round(unitPrice * qty * 100) / 100; const _productDiscount = this.productDiscount || 100; for (const sel of p.commissionEmployees) { if (!sel || !sel.employeeId) continue; const empIdStr = String(sel.employeeId); if (seen.has(empIdStr)) continue; seen.add(empIdStr); const emp = employees.find(e => String(e.id) === empIdStr); if (!emp) continue; const commissionResult = DataManager.calculateCommission(emp.positionId, 'product', p.id, subTotal); if (commissionResult && commissionResult.commissionAmount > 0) { DataManager.recordCommission( sel.employeeId, member.id, '产品提成', p.name + (qty > 1 ? '×' + qty : '') + (fromCardDeduct ? '(卡券扣次同单)' : ''), _productOriginal, commissionResult, paymentMethod, orderNo, subTotal, _productDiscount ); } } } } // 卡券扣次场景:单独添加一条产品消费记录(区别于服务扣次的记录) if (fromCardDeduct && this.selectedProducts.length > 0) { const productTotal = this.selectedProducts.reduce((sum, p) => sum + Math.round((parseFloat(p.price) || 0) * (parseInt(p.qty) || 1) * productRate * 100) / 100, 0); const pNames = this.selectedProducts.map(p => p.name + (p.qty > 1 ? '×' + p.qty : '')).join('、'); DataManager.addMemberRecord({ memberId: member.id, type: '消费', itemName: pNames + '(卡券同单)', amount: -productTotal, originalAmount: this.selectedProducts.reduce((s, p) => s + p.price * p.qty, 0), date: new Date().toISOString(), payMethod: paymentMethod, productDiscount: this.productDiscount, serviceAmount: 0, productAmount: productTotal, productItems: this.selectedProducts.map(p => ({ id: p.id, name: p.name, price: p.price, qty: p.qty })), remark: remarkContent || '卡券扣次同单产品销售', orderNo: orderNo }); // 扣余额(产品在卡券同单下默认走现金/会员余额,会员余额则扣减) if (paymentMethod === '会员余额' && member.balance >= productTotal) { member.balance -= productTotal; DataManager.updateMember(member.id, { balance: member.balance }); } } }, // ==================== 打印小票 ==================== printReceipt(recordData) { const shopInfo = DataManager.getShopInfo(); const shopName = (shopInfo && shopInfo.shopName) || '美容美发店'; const now = new Date(); const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); const timeStr = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0'); // SEC-L2: 小票顶部 logo const logoUrl = (typeof currentStore === 'object' && currentStore && (currentStore.logo_url || currentStore.logoUrl)) || (shopInfo && shopInfo.logoUrl) || ''; let html = ''; if (logoUrl) { html += '
'; } html += '
' + shopName + '
'; html += '
消费凭证
'; html += '
'; html += '
单据号' + (recordData.orderNo || '-') + '
'; html += '
日期' + dateStr + ' ' + timeStr + '
'; html += '
会员' + (recordData.memberName || '-') + '
'; html += '
类型' + recordData.type + '
'; html += '
'; html += '
项目' + (recordData.itemName || '-') + '
'; const amount = Math.abs(recordData.amount || 0); const sign = recordData.type === '充值' ? '+' : '-'; html += '
金额' + sign + '¥' + amount + '
'; if (recordData.discount && recordData.discount < 100) { html += '
折扣' + recordData.discount + '折
'; } html += '
付款方式' + (recordData.payMethod || '-') + '
'; if (recordData.remark) { html += '
备注' + recordData.remark + '
'; } html += '
'; html += ''; document.getElementById('receiptContent').innerHTML = html; const receiptArea = document.getElementById('receiptPrintArea'); receiptArea.style.display = 'flex'; receiptArea.classList.add('active'); document.body.style.overflow = 'hidden'; }, closeReceipt() { const receiptArea = document.getElementById('receiptPrintArea'); receiptArea.style.display = 'none'; receiptArea.classList.remove('active'); document.body.style.overflow = ''; }, showSuccessModal(data) { this.currentReceiptData = data.recordData || null; document.getElementById('successTitle').textContent = data.title; document.getElementById('successMessage').textContent = data.message; document.getElementById('successModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeSuccessModal() { document.getElementById('successModal').classList.remove('active'); document.body.style.overflow = ''; this.currentReceiptData = null; }, printCurrentReceipt() { if (this.currentReceiptData) { this.printReceipt(this.currentReceiptData); } }, // ==================== 积分抵扣 ==================== togglePointsDeduct() { this.usePointsDeduct = document.getElementById('pointsDeductToggle').checked; const slider = document.getElementById('pointsDeductSlider'); const track = slider ? slider.previousElementSibling : null; if (this.usePointsDeduct) { if (slider) slider.style.transform = 'translateX(20px)'; if (track) track.style.background = '#9B1B5E'; } else { if (slider) slider.style.transform = 'translateX(0)'; if (track) track.style.background = '#ddd'; } this.updateConsumeTotal(); }, updatePointsDeductInfo(memberId) { const member = this.members.find(m => String(m.id) === String(memberId)); const infoEl = document.getElementById('pointsDeductInfo'); if (member && infoEl) { const points = member.points || 0; const deductAmount = Math.floor(points / 100); // 100积分=1元 infoEl.textContent = '可用' + points + '积分,抵¥' + deductAmount; } }, showPointsDetail() { if (!this.currentDetailId) return; const records = DataManager.getMemberRecords(this.currentDetailId); /* [SEC-J2] 只看真实有积分变动的流水(已被冲掉的 cancelled 排除) */ const pointsRecords = records.filter(r => { const pc = parseInt(r.pointsChange); return !r.cancelled && pc !== 0 && !isNaN(pc); }); const listEl = document.getElementById('pointsDetailList'); if (pointsRecords.length === 0) { listEl.innerHTML = '
暂无积分记录
'; } else { listEl.innerHTML = pointsRecords.map(r => { const pc = parseInt(r.pointsChange) || 0; const isMinus = pc < 0; const abs = Math.abs(pc); const dateObj = new Date(r.date); const dateStr = dateObj.toLocaleDateString('zh-CN') + ' ' + dateObj.toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'}); /* 优先显示原始业务名 (如"充值100元/服务A消费") 而非积分类型字面 */ const label = r.itemName || (r.type === '充值' ? '充值送积分' : r.type === '消费' ? '消费送积分' : r.type === '购卡' ? '购卡送积分' : r.type); return '
' + '
' + label + '
' + dateStr + '
' + '
' + (isMinus ? '-' : '+') + abs + '积分
' + '
'; }).join(''); } document.getElementById('pointsDetailModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closePointsDetail() { document.getElementById('pointsDetailModal').classList.remove('active'); document.body.style.overflow = ''; }, /* SEC-J4: 根据 window.__storePointsRules 计算并刷新清零日期徽章 */ refreshPointsExpireBadge(member) { const el = document.getElementById('detailPointsExpire'); if (!el) return; const rules = window.__storePointsRules || null; if (!rules || !rules.expire) { el.style.display = 'none'; return; } const mode = rules.expire.mode || 'by_register'; if (mode === 'never') { el.style.display = 'none'; return; } let dateStr = ''; if (mode === 'yearly') { const now = new Date(); let y = now.getFullYear(); const cd = String(rules.expire.clear_day || '12-31'); dateStr = y + '-' + cd; if (new Date(dateStr) < now) { y += 1; dateStr = y + '-' + cd; } } else { /* by_register: 注册日 + N 个月,循环加到第一个 > 今天的周期 */ const months = parseInt(rules.expire.months) || 12; const base = member.createDate || member.created_at || member.create_date; if (!base) { el.style.display = 'none'; return; } const d = new Date(base); if (isNaN(d.getTime())) { el.style.display = 'none'; return; } const now = new Date(); const next = new Date(d.getTime()); let safety = 0; while (next <= now && safety < 200) { next.setMonth(next.getMonth() + months); safety++; } const yy = next.getFullYear(); const mm = String(next.getMonth() + 1).padStart(2, '0'); const dd = String(next.getDate()).padStart(2, '0'); dateStr = yy + '-' + mm + '-' + dd; } el.textContent = '下次清零:' + dateStr; el.style.display = 'block'; }, /* SEC-J4: 打开积分手动调整弹窗 */ openPointsAdjust() { if (!this.currentDetailId) return; const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (!member) return; document.getElementById('pointsAdjustMemberName').textContent = member.name || ''; document.getElementById('pointsAdjustCurrent').textContent = (parseInt(member.points) || 0).toLocaleString(); document.getElementById('pointsAdjustSign').value = '+'; document.getElementById('pointsAdjustDelta').value = ''; document.getElementById('pointsAdjustReason').value = ''; document.getElementById('pointsAdjustModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closePointsAdjust() { document.getElementById('pointsAdjustModal').classList.remove('active'); document.body.style.overflow = ''; }, async submitPointsAdjust() { if (!this.currentDetailId) return; const sign = document.getElementById('pointsAdjustSign').value; const raw = parseInt(document.getElementById('pointsAdjustDelta').value); const reason = document.getElementById('pointsAdjustReason').value.trim(); if (!Number.isFinite(raw) || raw <= 0) { alert('请输入正整数调整量'); return; } if (!reason) { alert('请填写调整原因'); return; } const delta = sign === '-' ? -raw : raw; try { const data = await ApiManager.post('/members/' + this.currentDetailId + '/points-adjust', { delta, reason }); this.closePointsAdjust(); /* 刷新全量数据后重新渲染会员详情,让流水也同步 */ if (typeof loadAllDataFromAPI === 'function') await loadAllDataFromAPI(); this.members = DataManager.getMembers(); this.openDetail(this.currentDetailId); alert('积分调整成功:' + (data.change > 0 ? '+' : '') + data.change + ' 积分'); } catch (e) { alert('调整失败:' + (e.message || '未知错误')); } }, // ==================== 会员标签 ==================== renderDetailTags() { const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (!member) return; const allTags = DataManager.getMemberTags(); const memberTags = member.tags || []; const container = document.getElementById('detailTagsArea'); if (!container) return; let html = memberTags.map(tagId => { const tag = allTags.find(t => t.id === tagId); if (!tag) return ''; return '' + tag.name + '×'; }).join(''); html += '+标签'; container.innerHTML = html; }, showAddTagModal() { const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (!member) return; const allTags = DataManager.getMemberTags(); const memberTags = member.tags || []; const availableTags = allTags.filter(t => !memberTags.includes(t.id)); if (availableTags.length === 0) { alert('所有标签已添加,可在设置中新增标签'); return; } const tagHtml = availableTags.map(tag => '
' + tag.name + '
' ).join(''); const listEl = document.getElementById('addTagSelectList'); if (listEl) listEl.innerHTML = tagHtml; document.getElementById('addTagSelectModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeAddTagModal() { document.getElementById('addTagSelectModal').classList.remove('active'); document.body.style.overflow = ''; }, addTagToMember(tagId) { const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (!member) return; if (!member.tags) member.tags = []; if (!member.tags.includes(tagId)) { member.tags.push(tagId); DataManager.updateMember(member.id, { tags: member.tags }); } this.closeAddTagModal(); this.renderDetailTags(); this.renderList(); }, removeTagFromMember(tagId) { const member = this.members.find(m => String(m.id) === String(this.currentDetailId)); if (!member || !member.tags) return; member.tags = member.tags.filter(t => t !== tagId); DataManager.updateMember(member.id, { tags: member.tags }); this.renderDetailTags(); this.renderList(); }, filterByTag(tagId) { this.currentTagFilter = tagId; this.showAllMembers = false; // SEC-MEMBER-LIST-COLLAPSE: 切标签复位 this._visibleMembers = 10; // [Fix8 20260709] paginated expand: 复位 this.renderList(); }, updateTagFilterOptions() { const allTags = DataManager.getMemberTags(); const selectEl = document.getElementById('memberTagFilter'); if (!selectEl) return; const currentValue = selectEl.value; selectEl.innerHTML = '' + allTags.map(t => '').join(''); selectEl.value = currentValue; }, // ==================== SEC-S1-Task3: 历史卡券(used_up) ==================== // 后端 snake_case → 前端 camelCase 适配 _mapApiHeldCard(c) { if (!c) return null; let packageItems = []; try { if (c.remaining_items) { packageItems = typeof c.remaining_items === 'string' ? JSON.parse(c.remaining_items) : c.remaining_items; } } catch(e) { packageItems = []; } return { id: c.id, memberId: c.member_id, cardTemplateId: c.card_template_id, cardName: c.card_name || '未知卡券', cardType: c.card_type === 'times' ? 'count' : c.card_type === 'timer' ? 'time' : (c.card_type || 'count'), remainingCount: c.remaining_times, remainingHours: c.remaining_hours != null ? Number(c.remaining_hours) : null, packageItems: packageItems, expireDate: c.expire_date ? String(c.expire_date).slice(0, 10) : null, status: c.status, purchaseStoreName: c.purchase_store_name || '', purchaseDate: c.created_at ? String(c.created_at).slice(0, 10) : null, totalPrice: c.total_price, paidAmount: c.paid_amount, remark: c.remark || '' }; }, // 拉取历史卡券(status=used_up),无则隐藏入口 async loadHistoryCards() { const toggle = document.getElementById('memberHistoryCardsToggle'); const list = document.getElementById('memberHistoryCardsList'); const text = document.getElementById('memberHistoryCardsToggleText'); if (!toggle || !list) return; // 重置 toggle.style.display = 'none'; list.style.display = 'none'; list.innerHTML = ''; this._historyCardsCache = []; if (!this.currentDetailId) return; try { const data = await ApiManager.get('/member-held-cards?memberId=' + encodeURIComponent(this.currentDetailId) + '&status=used_up'); const cards = (data || []).map(c => this._mapApiHeldCard(c)).filter(Boolean); if (!cards.length) return; this._historyCardsCache = cards; toggle.style.display = 'block'; text.textContent = '📜 查看历史卡券 (' + cards.length + ' 张)'; // 预渲染但不显示 list.innerHTML = cards.map(c => { const typeClass = c.cardType === 'count' ? 'type-count' : (c.cardType === 'time' ? 'type-time' : 'type-package'); const tagText = c.cardType === 'count' ? '计次' : (c.cardType === 'time' ? '计时' : '套餐'); let remainingText = '已用完'; if (c.cardType === 'count') remainingText = '已用完 0'; else if (c.cardType === 'time') remainingText = '已用完 0h'; else { const items = (c.packageItems || []).map(p => p.serviceName + '×' + (p.remainingCount || 0)).join(' '); remainingText = items || '0'; } const purchaseInfo = c.purchaseDate ? '
购于 ' + c.purchaseDate + '
' : ''; return '
' + '
' + tagText + '
' + '
已用完
' + '
' + c.cardName + '
' + '
' + remainingText + '
' + purchaseInfo + '
'; }).join(''); } catch (e) { console.warn('[SEC-S1-Task3] 历史卡券加载失败:', e && e.message ? e.message : e); } }, // 折叠/展开 历史卡券区 toggleHistoryCards() { const list = document.getElementById('memberHistoryCardsList'); const text = document.getElementById('memberHistoryCardsToggleText'); const count = (this._historyCardsCache || []).length; if (!list || !text) return; if (list.style.display === 'none' || !list.style.display) { list.style.display = 'flex'; text.textContent = '📜 收起历史卡券 (' + count + ' 张)'; } else { list.style.display = 'none'; text.textContent = '📜 查看历史卡券 (' + count + ' 张)'; } } }; // ==================== 弹窗HTML元素 ==================== // 小票展示弹窗 const receiptPrintAreaHTML = ` `; // 成功弹窗 const successModalHTML = ` `; // 积分明细弹窗 const pointsDetailModalHTML = ` `; // 标签管理弹窗 const tagManageModalHTML = ` `; // 选择标签弹窗 const addTagSelectModalHTML = ` `; // 预约管理弹窗 const appointmentModalHTML = ` `; // 将弹窗HTML添加到body末尾 document.body.insertAdjacentHTML('beforeend', receiptPrintAreaHTML); document.body.insertAdjacentHTML('beforeend', successModalHTML); document.body.insertAdjacentHTML('beforeend', pointsDetailModalHTML); document.body.insertAdjacentHTML('beforeend', tagManageModalHTML); document.body.insertAdjacentHTML('beforeend', addTagSelectModalHTML); document.body.insertAdjacentHTML('beforeend', appointmentModalHTML); // ==================== [SEC-J3] 积分规则配置弹窗 ==================== const pointsRulesModalHTML = ` `; document.body.insertAdjacentHTML('beforeend', pointsRulesModalHTML); /* ==================== SEC-J4: 积分手动调整弹窗 ==================== */ const pointsAdjustModalHTML = ` `; document.body.insertAdjacentHTML('beforeend', pointsAdjustModalHTML); /* SEC-J4: 启动时预拉一次积分规则,缓存到 window.__storePointsRules 用于清零日期徽章 */ /* [CONSOLE-CLEANUP 20260707-r2] 未登录守卫:登录页不发 /settings */ (async () => { if (!authToken) return; try { const settings = await ApiManager.get('/settings'); if (settings && settings.points_rules) window.__storePointsRules = settings.points_rules; } catch (e) { /* 忽略,未配置按 by_register 12 月兜底,但前端不强制展示 */ } })(); window.PointsRulesModule = { DEFAULT: { earn: { recharge_rate: 1, consume_rate: 1, purchase_card_rate: 1 }, deduct: { rate: 100, max_per_order_pct: 50 }, expire: { mode: 'by_register', months: 12, clear_day: '12-31' } }, current: null, async openModal() { try { const settings = await ApiManager.get('/settings'); let pr = (settings && settings.points_rules) || null; if (typeof pr === 'string') { try { pr = JSON.parse(pr); } catch(e) { pr = null; } } this.current = this._merge(this.DEFAULT, pr || {}); } catch(e) { console.warn('[PR] 加载规则失败:', e.message); this.current = JSON.parse(JSON.stringify(this.DEFAULT)); } this._fillForm(this.current); document.getElementById('pointsRulesModal').classList.add('active'); document.body.style.overflow = 'hidden'; }, closeModal() { document.getElementById('pointsRulesModal').classList.remove('active'); document.body.style.overflow = ''; }, maybeClose(ev) { if (ev.target.id === 'pointsRulesModal') this.closeModal(); }, onExpireModeChange() { const mode = (document.querySelector('input[name="prExpireMode"]:checked') || {}).value; const monthsInput = document.getElementById('prExpireMonths'); if (monthsInput) monthsInput.disabled = (mode !== 'by_register'); }, _merge(def, obj) { return { earn: Object.assign({}, def.earn, (obj && obj.earn) || {}), deduct: Object.assign({}, def.deduct, (obj && obj.deduct) || {}), expire: Object.assign({}, def.expire, (obj && obj.expire) || {}) }; }, _fillForm(r) { document.getElementById('prEarnRecharge').value = r.earn.recharge_rate; document.getElementById('prEarnConsume').value = r.earn.consume_rate; document.getElementById('prEarnCard').value = r.earn.purchase_card_rate; document.getElementById('prDeductRate').value = r.deduct.rate; document.getElementById('prDeductPct').value = r.deduct.max_per_order_pct; const mode = r.expire.mode || 'by_register'; const radio = document.querySelector('input[name="prExpireMode"][value="' + mode + '"]'); if (radio) radio.checked = true; document.getElementById('prExpireMonths').value = r.expire.months || 12; this.onExpireModeChange(); }, _readForm() { const earnR = parseFloat(document.getElementById('prEarnRecharge').value); const earnC = parseFloat(document.getElementById('prEarnConsume').value); const earnK = parseFloat(document.getElementById('prEarnCard').value); const dRate = parseInt(document.getElementById('prDeductRate').value); const dPct = parseInt(document.getElementById('prDeductPct').value); const mode = (document.querySelector('input[name="prExpireMode"]:checked') || {}).value || 'by_register'; const months = parseInt(document.getElementById('prExpireMonths').value) || 12; return { earn: { recharge_rate: isNaN(earnR) ? 0 : Math.max(0, earnR), consume_rate: isNaN(earnC) ? 0 : Math.max(0, earnC), purchase_card_rate: isNaN(earnK) ? 0 : Math.max(0, earnK) }, deduct: { rate: (isNaN(dRate) || dRate < 1) ? 100 : dRate, max_per_order_pct: isNaN(dPct) ? 0 : Math.min(100, Math.max(0, dPct)) }, expire: { mode: mode, months: Math.min(120, Math.max(1, months)), clear_day: '12-31' } }; }, async confirmClearAll() { const word = '清零'; const input = prompt('确定要清零本店所有会员积分吗?\n此操作不可撤销!\n\n如确认请输入"' + word + '"两个字:'); if (input === null) return; if (input.trim() !== word) { alert('输入不匹配,已取消'); return; } try { const data = await ApiManager.post('/points/clear-expired', { scope: 'all' }); alert('清零完成:共清零 ' + (data.count || 0) + ' 位会员,合计 ' + (data.cleared || 0) + ' 积分'); this.closeModal(); if (typeof loadAllDataFromAPI === 'function') await loadAllDataFromAPI(); /* 若当前在会员详情页,刷新会员数据 */ try { if (typeof MemberModule !== 'undefined' && MemberModule.currentDetailId) { MemberModule.members = DataManager.getMembers(); MemberModule.openDetail(MemberModule.currentDetailId); } } catch (e) { /* 静默 */ } } catch (e) { alert('清零失败:' + (e.message || '未知错误')); } }, async save() { const rules = this._readForm(); try { await ApiManager.put('/settings', { points_rules: rules }); this.closeModal(); if (window.UIToast && typeof UIToast.success === 'function') UIToast.success('积分规则已保存'); else alert('积分规则已保存'); } catch(e) { console.error('[PR] save failed:', e); alert('保存失败:' + (e.message || e)); } } }; // ==================== 初始化 ==================== document.addEventListener('DOMContentLoaded', () => { switchTab('home'); // 检查是否到定时备份时间(延迟3秒执行) setTimeout(function() { if (typeof SettingsModule !== 'undefined' && SettingsModule.checkAutoBackup) { SettingsModule.checkAutoBackup(); } if (typeof SettingsModule !== 'undefined' && SettingsModule.checkLocalAutoBackup) { SettingsModule.checkLocalAutoBackup(); } }, 3000); });
扫码识别会员码
将会员码放入取景框