1
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { mockCoaches } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
/** 助教详情(含绩效、备注等扩展数据) */
|
||||
/** 助教详情(含绩效、收入、任务、客户关系等) */
|
||||
interface CoachDetail {
|
||||
id: string
|
||||
name: string
|
||||
@@ -10,19 +10,20 @@ interface CoachDetail {
|
||||
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[]
|
||||
}
|
||||
|
||||
@@ -38,9 +39,57 @@ interface NoteItem {
|
||||
timestamp: string
|
||||
score: number
|
||||
customerName: string
|
||||
tagLabel: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 内联 Mock 数据:助教详情扩展 */
|
||||
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 {
|
||||
customerName: 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
|
||||
}
|
||||
|
||||
/** Mock 数据 */
|
||||
const mockCoachDetail: CoachDetail = {
|
||||
id: 'coach-001',
|
||||
name: '小燕',
|
||||
@@ -49,11 +98,14 @@ const mockCoachDetail: CoachDetail = {
|
||||
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: [
|
||||
@@ -70,34 +122,93 @@ const mockCoachDetail: CoachDetail = {
|
||||
],
|
||||
},
|
||||
notes: [
|
||||
{ id: 'n1', content: '本月表现优秀,客户好评率高', timestamp: '2026-03-05 14:30', score: 9, customerName: '管理员' },
|
||||
{ id: 'n2', content: '需要加强斯诺克教学技巧', timestamp: '2026-02-28 10:00', score: 7, customerName: '管理员' },
|
||||
{ id: 'n3', content: '客户王先生反馈服务态度很好', timestamp: '2026-02-20 16:45', score: 8, customerName: '王先生' },
|
||||
{ id: 'n1', content: '本月表现优秀,客户好评率高', timestamp: '2026-03-05 14:30', score: 9, customerName: '管理员', tagLabel: '管理员', createdAt: '2026-03-05 14:30' },
|
||||
{ id: 'n2', content: '需要加强斯诺克教学技巧', timestamp: '2026-02-28 10:00', score: 7, customerName: '管理员', tagLabel: '管理员', createdAt: '2026-02-28 10:00' },
|
||||
{ id: 'n3', content: '客户王先生反馈服务态度很好', timestamp: '2026-02-20 16:45', 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: 'gradient-pink', heartEmoji: '❤️', score: '9.5', scoreColor: 'success', serviceCount: 25, balance: '¥8,600', consume: '¥12,800' },
|
||||
{ id: 'c2', name: '李女士', initial: '李', avatarGradient: 'gradient-amber', heartEmoji: '❤️', score: '9.2', scoreColor: 'success', serviceCount: 22, balance: '¥6,200', consume: '¥9,500' },
|
||||
{ id: 'c3', name: '陈女士', initial: '陈', avatarGradient: 'gradient-green', heartEmoji: '❤️', score: '8.5', scoreColor: 'warning', serviceCount: 18, balance: '¥5,000', consume: '¥7,200' },
|
||||
{ id: 'c4', name: '张先生', initial: '张', avatarGradient: 'gradient-blue', heartEmoji: '💛', score: '7.8', scoreColor: 'warning', serviceCount: 12, balance: '¥3,800', consume: '¥5,600' },
|
||||
{ id: 'c5', name: '赵先生', initial: '赵', avatarGradient: 'gradient-purple', heartEmoji: '💛', score: '6.8', scoreColor: 'gray', serviceCount: 8, balance: '¥2,000', consume: '¥3,200' },
|
||||
]
|
||||
|
||||
const mockServiceRecords: ServiceRecord[] = [
|
||||
{ customerName: '王先生', type: '基础课', typeClass: 'basic', table: 'A12号台', duration: '2.5h', income: '¥200', date: '2026-02-07 21:30' },
|
||||
{ customerName: '李女士', type: '激励课', typeClass: 'incentive', table: 'VIP1号房', duration: '1.5h', income: '¥150', date: '2026-02-07 19:00', perfHours: '2h' },
|
||||
{ customerName: '陈女士', type: '基础课', typeClass: 'basic', table: '2号台', duration: '2h', income: '¥160', date: '2026-02-06 20:00' },
|
||||
{ customerName: '张先生', 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' },
|
||||
{ month: '上月', estimated: false, customers: '25人', hours: '92.0h', salary: '¥7,200' },
|
||||
{ month: '4月', estimated: false, customers: '20人', hours: '85.0h', salary: '¥6,600' },
|
||||
{ month: '3月', estimated: false, customers: '18人', hours: '78.5h', salary: '¥6,100' },
|
||||
{ month: '2月', estimated: false, customers: '15人', hours: '65.0h', salary: '¥5,200' },
|
||||
]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 页面状态:四态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'error' | 'normal',
|
||||
/** 助教 ID */
|
||||
coachId: '',
|
||||
/** 助教详情 */
|
||||
detail: null as CoachDetail | null,
|
||||
/** Banner 指标 */
|
||||
bannerMetrics: [] as Array<{ label: string; value: string }>,
|
||||
/** 绩效指标卡片 */
|
||||
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,
|
||||
/** 客户关系 TOP5 */
|
||||
topCustomers: [] as TopCustomer[],
|
||||
/** 近期服务明细 */
|
||||
serviceRecords: [] as ServiceRecord[],
|
||||
/** 更多信息 */
|
||||
historyMonths: [] as HistoryMonth[],
|
||||
/** 备注 */
|
||||
sortedNotes: [] as NoteItem[],
|
||||
/** 备注弹窗 */
|
||||
noteModalVisible: false,
|
||||
/** 备注列表弹窗 */
|
||||
notesPopupVisible: false,
|
||||
notesPopupName: '',
|
||||
notesPopupList: [] as Array<{ pinned?: boolean; text: string; date: string }>,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -110,40 +221,52 @@ Page({
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// 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
|
||||
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
|
||||
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 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,
|
||||
})
|
||||
|
||||
this.switchIncomeTab('this')
|
||||
} catch (_e) {
|
||||
this.setData({ pageState: 'error' })
|
||||
}
|
||||
|
||||
const bannerMetrics = [
|
||||
{ label: '工龄', value: `${detail.workYears}年` },
|
||||
{ label: '客户', value: `${detail.customerCount}人` },
|
||||
]
|
||||
|
||||
const perfCards = [
|
||||
{ label: '本月定档业绩', value: `${detail.performance.monthlyHours}`, unit: 'h', sub: '折算前 89.0h', bgClass: 'perf-blue', valueColor: 'text-primary' },
|
||||
{ label: '本月工资(预估)', value: `¥${detail.performance.monthlySalary.toLocaleString()}`, unit: '', sub: '含预估部分', bgClass: 'perf-green', valueColor: 'text-success' },
|
||||
{ label: '客源储值余额', value: `¥${detail.performance.customerBalance.toLocaleString()}`, unit: '', sub: `${detail.customerCount}位客户合计`, bgClass: 'perf-orange', valueColor: 'text-warning' },
|
||||
{ label: '本月任务完成', value: `${detail.performance.tasksCompleted}`, unit: '个', sub: '覆盖 22 位客户', bgClass: 'perf-purple', valueColor: 'text-purple' },
|
||||
]
|
||||
|
||||
const sorted = sortByTimestamp(detail.notes || [], 'timestamp') as NoteItem[]
|
||||
|
||||
this.setData({
|
||||
pageState: 'normal',
|
||||
detail,
|
||||
bannerMetrics,
|
||||
perfCards,
|
||||
sortedNotes: sorted,
|
||||
})
|
||||
|
||||
this.switchIncomeTab('this')
|
||||
}, 500)
|
||||
},
|
||||
|
||||
@@ -153,7 +276,6 @@ Page({
|
||||
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)
|
||||
@@ -172,6 +294,55 @@ Page({
|
||||
this.switchIncomeTab(tab)
|
||||
},
|
||||
|
||||
/** 展开/收起任务 */
|
||||
onToggleTasks() {
|
||||
this.setData({ tasksExpanded: !this.data.tasksExpanded })
|
||||
},
|
||||
|
||||
/** 点击任务备注图标 — 弹出备注列表 */
|
||||
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' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 查看更多服务记录 */
|
||||
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 })
|
||||
@@ -179,20 +350,19 @@ Page({
|
||||
|
||||
/** 备注弹窗确认 */
|
||||
onNoteConfirm(e: WechatMiniprogram.CustomEvent<{ score: number; content: string }>) {
|
||||
const { score, content } = e.detail
|
||||
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,
|
||||
score: 0,
|
||||
customerName: '我',
|
||||
tagLabel: '我',
|
||||
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
}
|
||||
const notes = [newNote, ...this.data.sortedNotes]
|
||||
this.setData({
|
||||
noteModalVisible: false,
|
||||
sortedNotes: notes,
|
||||
})
|
||||
this.setData({ noteModalVisible: false, sortedNotes: notes })
|
||||
wx.showToast({ title: '备注已保存', icon: 'success' })
|
||||
},
|
||||
|
||||
@@ -201,6 +371,12 @@ Page({
|
||||
this.setData({ noteModalVisible: false })
|
||||
},
|
||||
|
||||
/** 重试 */
|
||||
onRetry() {
|
||||
const id = this.data.coachId || ''
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
/** 返回 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
|
||||
Reference in New Issue
Block a user