小程序迁移 静态页面完成!!!
This commit is contained in:
@@ -1,6 +1,40 @@
|
||||
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
|
||||
@@ -71,7 +105,10 @@ interface TopCustomer {
|
||||
}
|
||||
|
||||
interface ServiceRecord {
|
||||
customerId?: string
|
||||
customerName: string
|
||||
initial: string
|
||||
avatarGradient: string
|
||||
type: string
|
||||
typeClass: string
|
||||
table: string
|
||||
@@ -87,15 +124,17 @@ interface HistoryMonth {
|
||||
customers: string
|
||||
hours: string
|
||||
salary: string
|
||||
callbackDone: number
|
||||
recallDone: number
|
||||
}
|
||||
|
||||
/** Mock 数据 */
|
||||
const mockCoachDetail: CoachDetail = {
|
||||
id: 'coach-001',
|
||||
name: '小燕',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
avatar: '/assets/images/avatar-coach.png',
|
||||
level: '星级',
|
||||
skills: ['中🎱', '🎯 斯诺克'],
|
||||
skills: ['中🎱', '🎯斯诺克'],
|
||||
workYears: 3,
|
||||
customerCount: 68,
|
||||
hireDate: '2023-03-15',
|
||||
@@ -122,9 +161,9 @@ const mockCoachDetail: CoachDetail = {
|
||||
],
|
||||
},
|
||||
notes: [
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -149,26 +188,41 @@ const mockAbandonedTasks: AbandonedTask[] = [
|
||||
]
|
||||
|
||||
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' },
|
||||
{ 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[] = [
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
{ 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({
|
||||
@@ -196,8 +250,9 @@ Page({
|
||||
hiddenTasks: [] as TaskItem[],
|
||||
abandonedTasks: [] as AbandonedTask[],
|
||||
tasksExpanded: false,
|
||||
/** 客户关系 TOP5 */
|
||||
/** 客户关系 TOP20 */
|
||||
topCustomers: [] as TopCustomer[],
|
||||
topCustomersExpanded: false,
|
||||
/** 近期服务明细 */
|
||||
serviceRecords: [] as ServiceRecord[],
|
||||
/** 更多信息 */
|
||||
@@ -209,14 +264,70 @@ Page({
|
||||
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,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
_longPressed: false,
|
||||
_animTimer: null as ReturnType<typeof setTimeout> | 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' })
|
||||
|
||||
@@ -244,6 +355,18 @@ Page({
|
||||
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({
|
||||
@@ -261,9 +384,17 @@ Page({
|
||||
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' })
|
||||
}
|
||||
@@ -299,6 +430,18 @@ Page({
|
||||
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
|
||||
@@ -334,6 +477,15 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
/** 近期服务明细 — 点击跳转客户详情 */
|
||||
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 || ''
|
||||
@@ -377,11 +529,6 @@ Page({
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
/** 返回 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
/** 问问助手 */
|
||||
onStartChat() {
|
||||
const id = this.data.coachId || this.data.detail?.id || ''
|
||||
|
||||
Reference in New Issue
Block a user