import { mockCoaches } from '../../utils/mock-data' import { sortByTimestamp } from '../../utils/sort' /* ── 进度条动画参数(与 task-list 共享相同逻辑) ── * 修改说明见 apps/miniprogram/doc/progress-bar-animation.md */ const SHINE_SPEED = 70 const SPARK_DELAY_MS = -150 const SPARK_DUR_MS = 1400 const NEXT_LOOP_DELAY_MS = 400 const SHINE_WIDTH_RPX = 120 const TRACK_WIDTH_RPX = 634 const SHINE_WIDTH_PCT = (SHINE_WIDTH_RPX / TRACK_WIDTH_RPX) * 100 function calcShineDur(filledPct: number): number { const t = (SHINE_SPEED - 1) / 99 const baseDur = 5000 - t * (5000 - 50) const distRatio = (filledPct + SHINE_WIDTH_PCT) / (100 + SHINE_WIDTH_PCT) return Math.max(50, Math.round(baseDur * distRatio)) } interface TickItem { value: number label: string left: string highlight: boolean } function buildTicks(tierNodes: number[], maxHours: number): TickItem[] { return tierNodes.map((v, i) => ({ value: v, label: String(v), left: `${Math.round((v / maxHours) * 10000) / 100}%`, highlight: i === 2, })) } /** 助教详情(含绩效、收入、任务、客户关系等) */ interface CoachDetail { id: string name: string avatar: string level: string skills: string[] workYears: number customerCount: number hireDate: string performance: { monthlyHours: number monthlySalary: number customerBalance: number tasksCompleted: number /** 绩效档位 */ perfCurrent: number perfTarget: number } income: { thisMonth: IncomeItem[] lastMonth: IncomeItem[] } notes: NoteItem[] } interface IncomeItem { label: string amount: string color: string } interface NoteItem { id: string content: string timestamp: string score: number customerName: string tagLabel: string createdAt: string } interface TaskItem { typeLabel: string typeClass: string customerName: string noteCount: number pinned: boolean notes?: Array<{ pinned?: boolean; text: string; date: string }> } interface AbandonedTask { customerName: string reason: string } interface TopCustomer { id: string name: string initial: string avatarGradient: string heartEmoji: string score: string scoreColor: string serviceCount: number balance: string consume: string } interface ServiceRecord { customerId?: string customerName: string initial: string avatarGradient: string type: string typeClass: string table: string duration: string income: string date: string perfHours?: string } interface HistoryMonth { month: string estimated: boolean customers: string hours: string salary: string callbackDone: number recallDone: number } /** Mock 数据 */ const mockCoachDetail: CoachDetail = { id: 'coach-001', name: '小燕', avatar: '/assets/images/avatar-coach.png', level: '星级', skills: ['中🎱', '🎯斯诺克'], workYears: 3, customerCount: 68, hireDate: '2023-03-15', performance: { monthlyHours: 87.5, monthlySalary: 6950, customerBalance: 86200, tasksCompleted: 38, perfCurrent: 80, perfTarget: 100, }, income: { thisMonth: [ { label: '基础课时费', amount: '¥3,500', color: 'primary' }, { label: '激励课时费', amount: '¥1,800', color: 'success' }, { label: '充值提成', amount: '¥1,200', color: 'warning' }, { label: '酒水提成', amount: '¥450', color: 'purple' }, ], lastMonth: [ { label: '基础课时费', amount: '¥3,800', color: 'primary' }, { label: '激励课时费', amount: '¥1,900', color: 'success' }, { label: '充值提成', amount: '¥1,100', color: 'warning' }, { label: '酒水提成', amount: '¥400', color: 'purple' }, ], }, notes: [ { id: 'n1', content: '本月表现优秀,客户好评率高', timestamp: '2026-03-05T14:30:00', score: 9, customerName: '管理员', tagLabel: '管理员', createdAt: '2026-03-05 14:30' }, { id: 'n2', content: '需要加强斯诺克教学技巧', timestamp: '2026-02-28T10:00:00', score: 7, customerName: '管理员', tagLabel: '管理员', createdAt: '2026-02-28 10:00' }, { id: 'n3', content: '客户王先生反馈服务态度很好', timestamp: '2026-02-20T16:45:00', score: 8, customerName: '王先生', tagLabel: '王先生', createdAt: '2026-02-20 16:45' }, ], } const mockVisibleTasks: TaskItem[] = [ { typeLabel: '高优先召回', typeClass: 'high-priority', customerName: '王先生', noteCount: 2, pinned: true, notes: [{ pinned: true, text: '重点客户,每周必须联系', date: '2026-02-06' }, { text: '上次来说最近出差多', date: '2026-02-01' }] }, { typeLabel: '高优先召回', typeClass: 'high-priority', customerName: '李女士', noteCount: 0, pinned: true }, { typeLabel: '高优先召回', typeClass: 'high-priority', customerName: '陈女士', noteCount: 1, pinned: true, notes: [{ text: '喜欢斯诺克,周末常来', date: '2026-01-28' }] }, { typeLabel: '优先召回', typeClass: 'priority', customerName: '张先生', noteCount: 0, pinned: false }, { typeLabel: '关系构建', typeClass: 'relationship', customerName: '赵总', noteCount: 3, pinned: false, notes: [{ pinned: true, text: '大客户,注意维护关系', date: '2026-02-03' }, { text: '上次带了3个朋友来', date: '2026-01-25' }, { text: '喜欢VIP包厢', date: '2026-01-15' }] }, { typeLabel: '客户回访', typeClass: 'callback', customerName: '周女士', noteCount: 0, pinned: false }, ] const mockHiddenTasks: TaskItem[] = [ { typeLabel: '优先召回', typeClass: 'priority', customerName: '刘先生', noteCount: 0, pinned: false }, { typeLabel: '客户回访', typeClass: 'callback', customerName: '孙先生', noteCount: 0, pinned: false }, { typeLabel: '关系构建', typeClass: 'relationship', customerName: '吴女士', noteCount: 0, pinned: false }, ] const mockAbandonedTasks: AbandonedTask[] = [ { customerName: '吴先生', reason: '客户拒绝' }, { customerName: '郑女士', reason: '超时未响应' }, ] const mockTopCustomers: TopCustomer[] = [ { id: 'c1', name: '王先生', initial: '王', avatarGradient: 'pink', heartEmoji: '❤️', score: '9.5', scoreColor: 'success', serviceCount: 25, balance: '¥8,600', consume: '¥12,800' }, { id: 'c2', name: '李女士', initial: '李', avatarGradient: 'amber', heartEmoji: '❤️', score: '9.2', scoreColor: 'success', serviceCount: 22, balance: '¥6,200', consume: '¥9,500' }, { id: 'c3', name: '陈女士', initial: '陈', avatarGradient: 'green', heartEmoji: '❤️', score: '8.5', scoreColor: 'warning', serviceCount: 18, balance: '¥5,000', consume: '¥7,200' }, { id: 'c4', name: '张先生', initial: '张', avatarGradient: 'blue', heartEmoji: '💛', score: '7.8', scoreColor: 'warning', serviceCount: 12, balance: '¥3,800', consume: '¥5,600' }, { id: 'c5', name: '赵先生', initial: '赵', avatarGradient: 'violet', heartEmoji: '💛', score: '6.8', scoreColor: 'gray', serviceCount: 8, balance: '¥2,000', consume: '¥3,200' }, { id: 'c6', name: '刘女士', initial: '刘', avatarGradient: 'pink', heartEmoji: '💛', score: '6.5', scoreColor: 'gray', serviceCount: 7, balance: '¥1,800', consume: '¥2,900' }, { id: 'c7', name: '孙先生', initial: '孙', avatarGradient: 'teal', heartEmoji: '💛', score: '6.2', scoreColor: 'gray', serviceCount: 6, balance: '¥1,500', consume: '¥2,500' }, { id: 'c8', name: '周女士', initial: '周', avatarGradient: 'amber', heartEmoji: '💛', score: '6.0', scoreColor: 'gray', serviceCount: 6, balance: '¥1,400', consume: '¥2,200' }, { id: 'c9', name: '吴先生', initial: '吴', avatarGradient: 'blue', heartEmoji: '💛', score: '5.8', scoreColor: 'gray', serviceCount: 5, balance: '¥1,200', consume: '¥2,000' }, { id: 'c10', name: '郑女士', initial: '郑', avatarGradient: 'green', heartEmoji: '💛', score: '5.5', scoreColor: 'gray', serviceCount: 5, balance: '¥1,000', consume: '¥1,800' }, { id: 'c11', name: '冯先生', initial: '冯', avatarGradient: 'violet', heartEmoji: '🤍', score: '5.2', scoreColor: 'gray', serviceCount: 4, balance: '¥900', consume: '¥1,600' }, { id: 'c12', name: '褚女士', initial: '褚', avatarGradient: 'pink', heartEmoji: '🤍', score: '5.0', scoreColor: 'gray', serviceCount: 4, balance: '¥800', consume: '¥1,400' }, { id: 'c13', name: '卫先生', initial: '卫', avatarGradient: 'amber', heartEmoji: '🤍', score: '4.8', scoreColor: 'gray', serviceCount: 3, balance: '¥700', consume: '¥1,200' }, { id: 'c14', name: '蒋女士', initial: '蒋', avatarGradient: 'teal', heartEmoji: '🤍', score: '4.5', scoreColor: 'gray', serviceCount: 3, balance: '¥600', consume: '¥1,000' }, { id: 'c15', name: '沈先生', initial: '沈', avatarGradient: 'blue', heartEmoji: '🤍', score: '4.2', scoreColor: 'gray', serviceCount: 3, balance: '¥500', consume: '¥900' }, { id: 'c16', name: '韩女士', initial: '韩', avatarGradient: 'green', heartEmoji: '🤍', score: '4.0', scoreColor: 'gray', serviceCount: 2, balance: '¥400', consume: '¥800' }, { id: 'c17', name: '杨先生', initial: '杨', avatarGradient: 'violet', heartEmoji: '🤍', score: '3.8', scoreColor: 'gray', serviceCount: 2, balance: '¥300', consume: '¥700' }, { id: 'c18', name: '朱女士', initial: '朱', avatarGradient: 'pink', heartEmoji: '🤍', score: '3.5', scoreColor: 'gray', serviceCount: 2, balance: '¥200', consume: '¥600' }, { id: 'c19', name: '秦先生', initial: '秦', avatarGradient: 'amber', heartEmoji: '🤍', score: '3.2', scoreColor: 'gray', serviceCount: 1, balance: '¥100', consume: '¥500' }, { id: 'c20', name: '尤女士', initial: '尤', avatarGradient: 'teal', heartEmoji: '🤍', score: '3.0', scoreColor: 'gray', serviceCount: 1, balance: '¥0', consume: '¥400' }, ] const mockServiceRecords: ServiceRecord[] = [ { customerId: 'c1', customerName: '王先生', initial: '王', avatarGradient: 'pink', type: '基础课', typeClass: 'basic', table: 'A12号台', duration: '2.5h', income: '¥200', date: '2026-02-07 21:30' }, { customerId: 'c2', customerName: '李女士', initial: '李', avatarGradient: 'amber', type: '激励课', typeClass: 'incentive', table: 'VIP1号房', duration: '1.5h', income: '¥150', date: '2026-02-07 19:00', perfHours: '2h' }, { customerId: 'c3', customerName: '陈女士', initial: '陈', avatarGradient: 'green', type: '基础课', typeClass: 'basic', table: '2号台', duration: '2h', income: '¥160', date: '2026-02-06 20:00' }, { customerId: 'c4', customerName: '张先生', initial: '张', avatarGradient: 'blue', type: '激励课', typeClass: 'incentive', table: '5号台', duration: '1h', income: '¥80', date: '2026-02-05 14:00' }, ] const mockHistoryMonths: HistoryMonth[] = [ { month: '本月', estimated: true, customers: '22人', hours: '87.5h', salary: '¥6,950', callbackDone: 14, recallDone: 24 }, { month: '上月', estimated: false, customers: '25人', hours: '92.0h', salary: '¥7,200', callbackDone: 16, recallDone: 28 }, { month: '4月', estimated: false, customers: '20人', hours: '85.0h', salary: '¥6,600', callbackDone: 12, recallDone: 22 }, { month: '3月', estimated: false, customers: '18人', hours: '78.5h', salary: '¥6,100', callbackDone: 10, recallDone: 18 }, { month: '2月', estimated: false, customers: '15人', hours: '65.0h', salary: '¥5,200', callbackDone: 8, recallDone: 15 }, ] Page({ data: { /** 页面状态:四态 */ pageState: 'loading' as 'loading' | 'empty' | 'error' | 'normal', /** 助教 ID */ coachId: '', /** 助教详情 */ detail: null as CoachDetail | null, /** 绩效指标卡片 */ perfCards: [] as Array<{ label: string; value: string; unit: string; sub: string; bgClass: string; valueColor: string }>, /** 绩效进度 */ perfCurrent: 0, perfTarget: 100, perfGap: 0, perfPercent: 0, /** 收入明细 Tab */ incomeTab: 'this' as 'this' | 'last', currentIncome: [] as IncomeItem[], incomeTotal: '', /** 任务执行 */ taskStats: { recall: 24, callback: 14 }, visibleTasks: [] as TaskItem[], hiddenTasks: [] as TaskItem[], abandonedTasks: [] as AbandonedTask[], tasksExpanded: false, /** 客户关系 TOP20 */ topCustomers: [] as TopCustomer[], topCustomersExpanded: false, /** 近期服务明细 */ serviceRecords: [] as ServiceRecord[], /** 更多信息 */ historyMonths: [] as HistoryMonth[], /** 备注 */ sortedNotes: [] as NoteItem[], noteModalVisible: false, /** 备注列表弹窗 */ notesPopupVisible: false, notesPopupName: '', notesPopupList: [] as Array<{ pinned?: boolean; text: string; date: string }>, /** 进度条动画状态(驱动 perf-progress-bar 组件) */ pbFilledPct: 0, pbClampedSparkPct: 0, pbCurrentTier: 0, pbTicks: [] as TickItem[], pbShineRunning: false, pbSparkRunning: false, pbShineDurMs: 1000, pbSparkDurMs: SPARK_DUR_MS, }, _longPressed: false, _animTimer: null as ReturnType | null, onLoad(options: { id?: string }) { const id = options?.id || '' this.setData({ coachId: id }) this.loadData(id) }, onHide() { this._stopAnimLoop() }, onUnload() { this._stopAnimLoop() }, onShow() { if (this.data.pageState === 'normal' && !this._animTimer) { this._startAnimLoop() } }, _startAnimLoop() { this._stopAnimLoop() this._runAnimStep() }, _stopAnimLoop() { if (this._animTimer !== null) { clearTimeout(this._animTimer) this._animTimer = null } this.setData({ pbShineRunning: false, pbSparkRunning: false }) }, _runAnimStep() { const filledPct = this.data.pbFilledPct ?? 0 const shineDurMs = calcShineDur(filledPct) this.setData({ pbShineRunning: true, pbSparkRunning: false, pbShineDurMs: shineDurMs }) const sparkTriggerDelay = Math.max(0, shineDurMs + SPARK_DELAY_MS) this._animTimer = setTimeout(() => { this.setData({ pbSparkRunning: true }) this._animTimer = setTimeout(() => { this.setData({ pbShineRunning: false, pbSparkRunning: false }) this._animTimer = setTimeout(() => { this._runAnimStep() }, Math.max(0, NEXT_LOOP_DELAY_MS)) }, SPARK_DUR_MS) }, sparkTriggerDelay) }, loadData(id: string) { this.setData({ pageState: 'loading' }) setTimeout(() => { try { // TODO: 替换为真实 API 调用 GET /api/coaches/:id const basicCoach = mockCoaches.find((c) => c.id === id) const detail: CoachDetail = basicCoach ? { ...mockCoachDetail, id: basicCoach.id, name: basicCoach.name } : mockCoachDetail if (!detail) { this.setData({ pageState: 'empty' }) return } const perf = detail.performance const perfCards = [ { label: '本月定档业绩', value: `${perf.monthlyHours}`, unit: 'h', sub: '折算前 89.0h', bgClass: 'perf-blue', valueColor: 'text-primary' }, { label: '本月工资(预估)', value: `¥${perf.monthlySalary.toLocaleString()}`, unit: '', sub: '含预估部分', bgClass: 'perf-green', valueColor: 'text-success' }, { label: '客源储值余额', value: `¥${perf.customerBalance.toLocaleString()}`, unit: '', sub: `${detail.customerCount}位客户合计`, bgClass: 'perf-orange', valueColor: 'text-warning' }, { label: '本月任务完成', value: `${perf.tasksCompleted}`, unit: '个', sub: '覆盖 22 位客户', bgClass: 'perf-purple', valueColor: 'text-purple' }, ] const perfGap = perf.perfTarget - perf.perfCurrent const perfPercent = Math.min(Math.round((perf.perfCurrent / perf.perfTarget) * 100), 100) // 进度条组件数据 const tierNodes = [0, 100, 130, 160, 190, 220] // Mock,实际由接口返回 const maxHours = 220 const totalHours = perf.monthlyHours const pbFilledPct = Math.min(100, Math.round((totalHours / maxHours) * 1000) / 10) // 当前档位 let pbCurrentTier = 0 for (let i = 1; i < tierNodes.length; i++) { if (totalHours >= tierNodes[i]) pbCurrentTier = i else break } const sorted = sortByTimestamp(detail.notes || [], 'timestamp') as NoteItem[] this.setData({ pageState: 'normal', detail, perfCards, perfCurrent: perf.perfCurrent, perfTarget: perf.perfTarget, perfGap, perfPercent, visibleTasks: mockVisibleTasks, hiddenTasks: mockHiddenTasks, abandonedTasks: mockAbandonedTasks, topCustomers: mockTopCustomers, serviceRecords: mockServiceRecords, historyMonths: mockHistoryMonths, sortedNotes: sorted, pbFilledPct, pbClampedSparkPct: Math.max(0, Math.min(100, pbFilledPct)), pbCurrentTier, pbTicks: buildTicks(tierNodes, maxHours), pbShineDurMs: calcShineDur(pbFilledPct), pbSparkDurMs: SPARK_DUR_MS, }) this.switchIncomeTab('this') // 数据加载完成后启动动画循环 setTimeout(() => this._startAnimLoop(), 300) } catch (_e) { this.setData({ pageState: 'error' }) } }, 500) }, /** 切换收入明细 Tab */ switchIncomeTab(tab: 'this' | 'last') { const detail = this.data.detail if (!detail) return const items = tab === 'this' ? detail.income.thisMonth : detail.income.lastMonth const total = items.reduce((sum, item) => { const num = parseFloat(item.amount.replace(/[¥,]/g, '')) return sum + (isNaN(num) ? 0 : num) }, 0) this.setData({ incomeTab: tab, currentIncome: items, incomeTotal: `¥${total.toLocaleString()}`, }) }, /** 点击收入 Tab */ onIncomeTabTap(e: WechatMiniprogram.CustomEvent) { const tab = e.currentTarget.dataset.tab as 'this' | 'last' this.switchIncomeTab(tab) }, /** 展开/收起任务 */ onToggleTasks() { this.setData({ tasksExpanded: !this.data.tasksExpanded }) }, /** 点击任务项 — 跳转客户详情 */ onTaskItemTap(e: WechatMiniprogram.CustomEvent) { const name = e.currentTarget.dataset.name as string if (!name) return wx.navigateTo({ url: `/pages/customer-detail/customer-detail?name=${encodeURIComponent(name)}` }) }, /** 展开/收起客户关系列表 */ onToggleTopCustomers() { this.setData({ topCustomersExpanded: !this.data.topCustomersExpanded }) }, /** 点击任务备注图标 — 弹出备注列表 */ onTaskNoteTap(e: WechatMiniprogram.CustomEvent) { const idx = e.currentTarget.dataset.index as number | undefined const hiddenIdx = e.currentTarget.dataset.hiddenIndex as number | undefined let task: TaskItem | undefined if (idx !== undefined) { task = this.data.visibleTasks[idx] } else if (hiddenIdx !== undefined) { task = this.data.hiddenTasks[hiddenIdx] } if (task?.notes && task.notes.length > 0) { this.setData({ notesPopupVisible: true, notesPopupName: task.customerName, notesPopupList: task.notes, }) } }, /** 关闭备注列表弹窗 */ onHideNotesPopup() { this.setData({ notesPopupVisible: false }) }, /** 点击客户卡片 — 跳转客户详情 */ onCustomerTap(e: WechatMiniprogram.CustomEvent) { const id = e.currentTarget.dataset.id as string wx.navigateTo({ url: `/pages/customer-detail/customer-detail?id=${id}`, fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }), }) }, /** 近期服务明细 — 点击跳转客户详情 */ onSvcCardTap(e: WechatMiniprogram.TouchEvent) { const id = e.currentTarget.dataset.id as string wx.navigateTo({ url: `/pages/customer-detail/customer-detail${id ? '?id=' + id : ''}`, fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }), }) }, /** 查看更多服务记录 */ onViewMoreRecords() { const coachId = this.data.coachId || this.data.detail?.id || '' wx.navigateTo({ url: `/pages/performance-records/performance-records?coachId=${coachId}`, fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }), }) }, /** 打开备注弹窗 */ onAddNote() { this.setData({ noteModalVisible: true }) }, /** 备注弹窗确认 */ onNoteConfirm(e: WechatMiniprogram.CustomEvent<{ score: number; content: string }>) { const { content } = e.detail // TODO: 替换为真实 API 调用 POST /api/xcx/notes const newNote: NoteItem = { id: `n-${Date.now()}`, content, timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '), score: 0, customerName: '我', tagLabel: '我', createdAt: new Date().toLocaleString('zh-CN', { hour12: false }), } const notes = [newNote, ...this.data.sortedNotes] this.setData({ noteModalVisible: false, sortedNotes: notes }) wx.showToast({ title: '备注已保存', icon: 'success' }) }, /** 备注弹窗取消 */ onNoteCancel() { this.setData({ noteModalVisible: false }) }, /** 重试 */ onRetry() { const id = this.data.coachId || '' this.loadData(id) }, /** 问问助手 */ onStartChat() { const id = this.data.coachId || this.data.detail?.id || '' wx.navigateTo({ url: `/pages/chat/chat?coachId=${id}`, fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }), }) }, })