微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
49
apps/miniprogram - 副本/miniprogram/utils/chat.ts
Normal file
49
apps/miniprogram - 副本/miniprogram/utils/chat.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 对话工具函数
|
||||
* simulateStreamOutput 为异步函数(使用 setTimeout)
|
||||
* simulateStreamOutputSync 为同步纯函数(用于测试)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 模拟流式输出:逐字追加调用 callback
|
||||
* @param text 完整文本
|
||||
* @param callback 每次追加后的回调,参数为当前已输出的子串
|
||||
* @returns Promise,输出完成后 resolve
|
||||
*/
|
||||
export function simulateStreamOutput(
|
||||
text: string,
|
||||
callback: (partial: string) => void
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (text.length === 0) {
|
||||
callback('')
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let index = 0
|
||||
const tick = () => {
|
||||
index++
|
||||
callback(text.slice(0, index))
|
||||
if (index < text.length) {
|
||||
setTimeout(tick, 50)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
setTimeout(tick, 50)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步版流式输出:返回逐字追加的子串数组(用于测试)
|
||||
* @param text 完整文本
|
||||
* @returns 逐步增长的子串数组,如 "abc" → ["a", "ab", "abc"]
|
||||
*/
|
||||
export function simulateStreamOutputSync(text: string): string[] {
|
||||
const result: string[] = []
|
||||
for (let i = 1; i <= text.length; i++) {
|
||||
result.push(text.slice(0, i))
|
||||
}
|
||||
return result
|
||||
}
|
||||
26
apps/miniprogram - 副本/miniprogram/utils/config.ts
Normal file
26
apps/miniprogram - 副本/miniprogram/utils/config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 环境配置
|
||||
*
|
||||
* 根据小程序运行环境自动切换 API 地址:
|
||||
* - develop(开发版)→ 本机
|
||||
* - trial(体验版)→ 测试环境
|
||||
* - release(正式版)→ 正式环境
|
||||
*/
|
||||
|
||||
function getApiBase(): string {
|
||||
const accountInfo = wx.getAccountInfoSync()
|
||||
const envVersion = accountInfo.miniProgram.envVersion
|
||||
|
||||
switch (envVersion) {
|
||||
case "develop":
|
||||
return "http://127.0.0.1:8000"
|
||||
case "trial":
|
||||
return "https://api.langlangzhuoqiu.cn"
|
||||
case "release":
|
||||
return "https://api.langlangzhuoqiu.cn"
|
||||
default:
|
||||
return "https://api.langlangzhuoqiu.cn"
|
||||
}
|
||||
}
|
||||
|
||||
export const API_BASE = getApiBase()
|
||||
23
apps/miniprogram - 副本/miniprogram/utils/filter.ts
Normal file
23
apps/miniprogram - 副本/miniprogram/utils/filter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 筛选工具函数
|
||||
* 纯函数,无 wx.* 依赖
|
||||
*/
|
||||
|
||||
/**
|
||||
* 按条件对象筛选列表
|
||||
* condition 中每个 key-value 对都必须匹配,即 AND 逻辑
|
||||
* @param list 对象数组
|
||||
* @param condition 筛选条件,key-value 对
|
||||
* @returns 满足所有条件的子数组
|
||||
*/
|
||||
export function filterByCondition<T extends Record<string, any>>(
|
||||
list: T[],
|
||||
condition: Record<string, any>
|
||||
): T[] {
|
||||
const keys = Object.keys(condition)
|
||||
if (keys.length === 0) return [...list]
|
||||
|
||||
return list.filter((item) =>
|
||||
keys.every((key) => item[key] === condition[key])
|
||||
)
|
||||
}
|
||||
9
apps/miniprogram - 副本/miniprogram/utils/format.wxs
Normal file
9
apps/miniprogram - 副本/miniprogram/utils/format.wxs
Normal file
@@ -0,0 +1,9 @@
|
||||
// WXS 格式化工具 — WXML 中不能调用 JS 方法,需通过 WXS 桥接
|
||||
function toFixed(num, digits) {
|
||||
if (num === undefined || num === null) return '--'
|
||||
return num.toFixed(digits)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toFixed: toFixed,
|
||||
}
|
||||
21
apps/miniprogram - 副本/miniprogram/utils/heart.ts
Normal file
21
apps/miniprogram - 副本/miniprogram/utils/heart.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 爱心评分映射工具函数
|
||||
* 纯函数,无 wx.* 依赖
|
||||
*/
|
||||
|
||||
/**
|
||||
* 将 0-10 分数映射为爱心 Emoji
|
||||
* >8.5 → 💖, >7 → 🧡, >5 → 💛, ≤5 → 💙
|
||||
* 超出 [0,10] 范围的值会被 clamp
|
||||
* @param score 评分 0-10
|
||||
* @returns 爱心 Emoji
|
||||
*/
|
||||
export function getHeartEmoji(score: number): string {
|
||||
// clamp 到 [0, 10]
|
||||
const clamped = Math.min(10, Math.max(0, score))
|
||||
|
||||
if (clamped > 8.5) return '💖'
|
||||
if (clamped > 7) return '🧡'
|
||||
if (clamped > 5) return '💛'
|
||||
return '💙'
|
||||
}
|
||||
624
apps/miniprogram - 副本/miniprogram/utils/mock-data.ts
Normal file
624
apps/miniprogram - 副本/miniprogram/utils/mock-data.ts
Normal file
@@ -0,0 +1,624 @@
|
||||
// TODO: 联调时删除此文件,改为真实 API 调用
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
export type TaskType = 'callback' | 'priority_recall' | 'relationship'
|
||||
|
||||
export interface Task {
|
||||
id: string
|
||||
customerName: string
|
||||
customerAvatar: string
|
||||
taskType: TaskType
|
||||
taskTypeLabel: string
|
||||
deadline: string
|
||||
heartScore: number
|
||||
hobbies: string[]
|
||||
isPinned: boolean
|
||||
hasNote: boolean
|
||||
status: 'pending' | 'completed' | 'abandoned'
|
||||
}
|
||||
|
||||
export interface TaskDetail extends Task {
|
||||
aiAnalysis: { summary: string; suggestions: string[] }
|
||||
notes: Note[]
|
||||
lastVisitDate?: string
|
||||
lastSpendAmount?: number
|
||||
callbackReason?: string
|
||||
daysAbsent?: number
|
||||
spendTrend?: 'up' | 'down' | 'flat'
|
||||
churnRisk?: 'high' | 'medium' | 'low'
|
||||
preferences?: string[]
|
||||
consumptionHabits?: string
|
||||
socialPreference?: string
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
id: string
|
||||
/** 备注正文 */
|
||||
content: string
|
||||
/** 标签类型:customer=客户备注,coach=助教备注 */
|
||||
tagType: 'customer' | 'coach'
|
||||
/** 标签文案,如"客户:王先生"、"助教:小燕" */
|
||||
tagLabel: string
|
||||
/** 创建时间,展示用格式 */
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface PerformanceData {
|
||||
monthlyIncome: number
|
||||
incomeChange: number
|
||||
currentTier: string
|
||||
nextTierGap: number
|
||||
todayServiceCount: number
|
||||
weekServiceCount: number
|
||||
monthServiceCount: number
|
||||
}
|
||||
|
||||
export interface PerformanceRecord {
|
||||
id: string
|
||||
customerName: string
|
||||
amount: number
|
||||
date: string
|
||||
type: string
|
||||
category: string
|
||||
}
|
||||
|
||||
export interface FinanceMetric {
|
||||
title: string
|
||||
value: number
|
||||
unit: string
|
||||
trend: 'up' | 'down' | 'flat'
|
||||
trendValue: string
|
||||
helpText?: string
|
||||
}
|
||||
|
||||
export interface BoardFinanceData {
|
||||
metrics: FinanceMetric[]
|
||||
timeRange: string
|
||||
filterOptions: Array<{ value: string; text: string }>
|
||||
}
|
||||
|
||||
export interface CustomerCard {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
heartScore: number
|
||||
tags: string[]
|
||||
keyMetric: { label: string; value: string }
|
||||
}
|
||||
|
||||
export interface CoachCard {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
level: string
|
||||
keyMetrics: Array<{ label: string; value: string }>
|
||||
}
|
||||
|
||||
export interface CustomerDetail {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
tags: string[]
|
||||
heartScore: number
|
||||
phone: string
|
||||
spiIndex: number
|
||||
consumptionRecords: ConsumptionRecord[]
|
||||
}
|
||||
|
||||
export interface ConsumptionRecord {
|
||||
id: string
|
||||
date: string
|
||||
project: string
|
||||
duration: number
|
||||
amount: number
|
||||
coachName: string
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
timestamp: string
|
||||
referenceCard?: {
|
||||
type: 'customer' | 'record'
|
||||
title: string
|
||||
summary: string
|
||||
data: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChatHistoryItem {
|
||||
id: string
|
||||
title: string
|
||||
lastMessage: string
|
||||
timestamp: string
|
||||
customerName?: string
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
name: string
|
||||
avatar: string
|
||||
role: string
|
||||
storeName: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mock 数据 — 任务模块
|
||||
// ============================================================
|
||||
|
||||
export const mockTasks: Task[] = [
|
||||
{
|
||||
id: 'task-001',
|
||||
customerName: '张伟',
|
||||
customerAvatar: '/assets/images/avatar-default.png',
|
||||
taskType: 'callback',
|
||||
taskTypeLabel: '回访',
|
||||
deadline: '2026-03-10',
|
||||
heartScore: 9.2,
|
||||
hobbies: ['chinese', 'karaoke'],
|
||||
isPinned: true,
|
||||
hasNote: true,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'task-002',
|
||||
customerName: '李娜',
|
||||
customerAvatar: '/assets/images/avatar-default.png',
|
||||
taskType: 'priority_recall',
|
||||
taskTypeLabel: '优先召回',
|
||||
deadline: '2026-03-08',
|
||||
heartScore: 6.5,
|
||||
hobbies: ['snooker'],
|
||||
isPinned: false,
|
||||
hasNote: false,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'task-003',
|
||||
customerName: '王磊',
|
||||
customerAvatar: '/assets/images/avatar-default.png',
|
||||
taskType: 'relationship',
|
||||
taskTypeLabel: '关系构建',
|
||||
deadline: '2026-03-12',
|
||||
heartScore: 7.8,
|
||||
hobbies: ['chinese', 'mahjong'],
|
||||
isPinned: false,
|
||||
hasNote: true,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'task-004',
|
||||
customerName: '赵敏',
|
||||
customerAvatar: '/assets/images/avatar-default.png',
|
||||
taskType: 'callback',
|
||||
taskTypeLabel: '回访',
|
||||
deadline: '2026-03-09',
|
||||
heartScore: 8.8,
|
||||
hobbies: ['chinese'],
|
||||
isPinned: false,
|
||||
hasNote: false,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'task-005',
|
||||
customerName: '陈浩',
|
||||
customerAvatar: '/assets/images/avatar-default.png',
|
||||
taskType: 'priority_recall',
|
||||
taskTypeLabel: '优先召回',
|
||||
deadline: '2026-03-07',
|
||||
heartScore: 4.2,
|
||||
hobbies: ['snooker', 'karaoke'],
|
||||
isPinned: true,
|
||||
hasNote: true,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'task-006',
|
||||
customerName: '刘洋',
|
||||
customerAvatar: '/assets/images/avatar-default.png',
|
||||
taskType: 'relationship',
|
||||
taskTypeLabel: '关系构建',
|
||||
deadline: '2026-03-15',
|
||||
heartScore: 5.0,
|
||||
hobbies: ['mahjong'],
|
||||
isPinned: false,
|
||||
hasNote: false,
|
||||
status: 'completed',
|
||||
},
|
||||
]
|
||||
|
||||
export const mockTaskDetails: TaskDetail[] = [
|
||||
{
|
||||
...mockTasks[0],
|
||||
aiAnalysis: {
|
||||
summary: '张伟是高价值回访客户,近期消费频次稳定,建议维持关系并推荐新课程。',
|
||||
suggestions: ['推荐斯诺克进阶课程', '赠送体验券增强粘性', '了解近期打球偏好变化'],
|
||||
},
|
||||
notes: [],
|
||||
lastVisitDate: '2026-03-01',
|
||||
lastSpendAmount: 380,
|
||||
callbackReason: '上次体验课后未续费,需跟进意向',
|
||||
},
|
||||
{
|
||||
...mockTasks[1],
|
||||
aiAnalysis: {
|
||||
summary: '李娜已 23 天未到店,消费趋势下降,存在流失风险。',
|
||||
suggestions: ['电话关怀了解原因', '发送专属优惠券', '推荐闺蜜同行活动'],
|
||||
},
|
||||
notes: [],
|
||||
daysAbsent: 23,
|
||||
spendTrend: 'down',
|
||||
churnRisk: 'high',
|
||||
},
|
||||
{
|
||||
...mockTasks[2],
|
||||
aiAnalysis: {
|
||||
summary: '王磊是中台球爱好者,社交活跃,适合发展为核心会员。',
|
||||
suggestions: ['邀请参加周末球友赛', '推荐 VIP 会员权益', '介绍同水平球友'],
|
||||
},
|
||||
notes: [],
|
||||
preferences: ['中式台球', '麻将'],
|
||||
consumptionHabits: '周末下午为主,偏好包厢',
|
||||
socialPreference: '喜欢组队打球,社交型消费者',
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================
|
||||
// Mock 数据 — 备注模块
|
||||
// ============================================================
|
||||
|
||||
// mock 数据贴近 H5 原型 notes.html 中的 12 条备注
|
||||
export const mockNotes: Note[] = [
|
||||
{
|
||||
id: 'note-001',
|
||||
content: '小燕本月表现优秀,课时完成率达到120%,客户评价全部5星。建议下月提升课时费标准,同时安排更多VIP客户给她。',
|
||||
tagType: 'coach',
|
||||
tagLabel: '助教:小燕',
|
||||
createdAt: '2024-11-27 16:00',
|
||||
},
|
||||
{
|
||||
id: 'note-002',
|
||||
content: '客户今天表示下周有朋友生日聚会,想预约包厢。已告知包厢需要提前3天预约,客户说会尽快确定时间再联系。',
|
||||
tagType: 'customer',
|
||||
tagLabel: '客户:王先生',
|
||||
createdAt: '2024-11-27 15:30',
|
||||
},
|
||||
{
|
||||
id: 'note-003',
|
||||
content: '完成高优先召回任务。客户反馈最近工作太忙,这周末会来店里。已提醒客户储值卡还有2000元余额,下月到期需要续费。',
|
||||
tagType: 'customer',
|
||||
tagLabel: '客户:王先生',
|
||||
createdAt: '2024-11-26 18:45',
|
||||
},
|
||||
{
|
||||
id: 'note-004',
|
||||
content: 'Amy最近工作状态很好,主动承担了培训新员工的任务。但需要注意她最近加班较多,避免过度疲劳。建议适当调整排班。',
|
||||
tagType: 'coach',
|
||||
tagLabel: '助教:Amy',
|
||||
createdAt: '2024-11-26 11:30',
|
||||
},
|
||||
{
|
||||
id: 'note-005',
|
||||
content: '泡芙本月表现优秀,课时完成率达到120%,客户评价全部5星。建议下月提升课时费标准。',
|
||||
tagType: 'customer',
|
||||
tagLabel: '客户:陈女士',
|
||||
createdAt: '2024-11-25 10:20',
|
||||
},
|
||||
{
|
||||
id: 'note-006',
|
||||
content: '泡芙的斯诺克教学水平有明显提升,最近几位客户反馈都很好。可以考虑让她带更多斯诺克方向的课程。',
|
||||
tagType: 'coach',
|
||||
tagLabel: '助教:泡芙',
|
||||
createdAt: '2024-11-25 14:20',
|
||||
},
|
||||
{
|
||||
id: 'note-007',
|
||||
content: '客户对今天的服务非常满意,特别提到小燕的教学很专业。客户表示会推荐朋友来店里体验。',
|
||||
tagType: 'customer',
|
||||
tagLabel: '客户:李女士',
|
||||
createdAt: '2024-11-24 21:15',
|
||||
},
|
||||
{
|
||||
id: 'note-008',
|
||||
content: '小燕反馈近期有几位客户希望增加晚间时段的课程,建议协调排班,增加21:00-23:00时段的助教配置。',
|
||||
tagType: 'coach',
|
||||
tagLabel: '助教:小燕',
|
||||
createdAt: '2024-11-24 09:00',
|
||||
},
|
||||
{
|
||||
id: 'note-009',
|
||||
content: '关系构建任务完成。与客户进行了30分钟的深入交流,了解到客户是企业高管,经常需要商务宴请场地。已记录客户需求,后续可以推荐团建套餐。',
|
||||
tagType: 'customer',
|
||||
tagLabel: '客户:张先生',
|
||||
createdAt: '2024-11-23 19:30',
|
||||
},
|
||||
{
|
||||
id: 'note-010',
|
||||
content: '客户今天充值了5000元,选择的是尊享套餐。客户提到喜欢安静的环境,以后尽量安排包厢。',
|
||||
tagType: 'customer',
|
||||
tagLabel: '客户:张先生',
|
||||
createdAt: '2024-11-22 14:00',
|
||||
},
|
||||
{
|
||||
id: 'note-011',
|
||||
content: 'Amy本周请假2天处理家事,已安排泡芙和小燕分担她的客户。回来后需要跟进客户交接情况。',
|
||||
tagType: 'coach',
|
||||
tagLabel: '助教:Amy',
|
||||
createdAt: '2024-11-22 10:15',
|
||||
},
|
||||
{
|
||||
id: 'note-012',
|
||||
content: 'Amy最近工作状态很好,主动承担了培训新员工的任务。但需要注意她最近加班较多,避免过度疲劳。',
|
||||
tagType: 'customer',
|
||||
tagLabel: '客户:李女士',
|
||||
createdAt: '2024-11-21 09:45',
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================
|
||||
// Mock 数据 — 绩效模块
|
||||
// ============================================================
|
||||
|
||||
export const mockPerformance: PerformanceData = {
|
||||
monthlyIncome: 12680,
|
||||
incomeChange: 15.3,
|
||||
currentTier: '银牌助教',
|
||||
nextTierGap: 3320,
|
||||
todayServiceCount: 4,
|
||||
weekServiceCount: 18,
|
||||
monthServiceCount: 67,
|
||||
}
|
||||
|
||||
export const mockPerformanceRecords: PerformanceRecord[] = [
|
||||
{ id: 'pr-001', customerName: '张伟', amount: 380, date: '2026-03-05', type: '课时', category: '中式台球' },
|
||||
{ id: 'pr-002', customerName: '李娜', amount: 200, date: '2026-03-04', type: '散客', category: '斯诺克' },
|
||||
{ id: 'pr-003', customerName: '王磊', amount: 1500, date: '2026-03-03', type: '储值', category: '会员充值' },
|
||||
{ id: 'pr-004', customerName: '赵敏', amount: 280, date: '2026-03-02', type: '课时', category: '中式台球' },
|
||||
{ id: 'pr-005', customerName: '陈浩', amount: 150, date: '2026-03-01', type: '散客', category: '中式台球' },
|
||||
{ id: 'pr-006', customerName: '刘洋', amount: 320, date: '2026-02-28', type: '课时', category: '斯诺克' },
|
||||
{ id: 'pr-007', customerName: '张伟', amount: 2000, date: '2026-02-25', type: '储值', category: '会员充值' },
|
||||
]
|
||||
|
||||
// ============================================================
|
||||
// Mock 数据 — 看板模块
|
||||
// ============================================================
|
||||
|
||||
export const mockBoardFinance: BoardFinanceData = {
|
||||
metrics: [
|
||||
{
|
||||
title: '本月营收',
|
||||
value: 86500,
|
||||
unit: '元',
|
||||
trend: 'up',
|
||||
trendValue: '+12.5%',
|
||||
helpText: '包含课时费、散客消费、储值充值等所有收入',
|
||||
},
|
||||
{
|
||||
title: '本月支出',
|
||||
value: 34200,
|
||||
unit: '元',
|
||||
trend: 'down',
|
||||
trendValue: '-3.2%',
|
||||
helpText: '包含人工、水电、耗材等运营成本',
|
||||
},
|
||||
{
|
||||
title: '净利润',
|
||||
value: 52300,
|
||||
unit: '元',
|
||||
trend: 'up',
|
||||
trendValue: '+22.1%',
|
||||
},
|
||||
{
|
||||
title: '客单价',
|
||||
value: 186,
|
||||
unit: '元',
|
||||
trend: 'flat',
|
||||
trendValue: '+0.5%',
|
||||
helpText: '本月平均每位客户消费金额',
|
||||
},
|
||||
],
|
||||
timeRange: '2026-03',
|
||||
filterOptions: [
|
||||
{ value: 'today', text: '今日' },
|
||||
{ value: 'week', text: '本周' },
|
||||
{ value: 'month', text: '本月' },
|
||||
{ value: 'quarter', text: '本季度' },
|
||||
],
|
||||
}
|
||||
|
||||
export const mockCustomers: CustomerCard[] = [
|
||||
{
|
||||
id: 'cust-001',
|
||||
name: '张伟',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
heartScore: 9.2,
|
||||
tags: ['VIP', '中式台球'],
|
||||
keyMetric: { label: '本月消费', value: '¥2,380' },
|
||||
},
|
||||
{
|
||||
id: 'cust-002',
|
||||
name: '李娜',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
heartScore: 6.5,
|
||||
tags: ['斯诺克'],
|
||||
keyMetric: { label: '本月消费', value: '¥680' },
|
||||
},
|
||||
{
|
||||
id: 'cust-003',
|
||||
name: '王磊',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
heartScore: 7.8,
|
||||
tags: ['中式台球', '麻将'],
|
||||
keyMetric: { label: '本月消费', value: '¥1,500' },
|
||||
},
|
||||
{
|
||||
id: 'cust-004',
|
||||
name: '赵敏',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
heartScore: 8.8,
|
||||
tags: ['中式台球', '新客'],
|
||||
keyMetric: { label: '本月消费', value: '¥280' },
|
||||
},
|
||||
{
|
||||
id: 'cust-005',
|
||||
name: '陈浩',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
heartScore: 4.2,
|
||||
tags: ['斯诺克', 'K歌'],
|
||||
keyMetric: { label: '本月消费', value: '¥150' },
|
||||
},
|
||||
]
|
||||
|
||||
export const mockCoaches: CoachCard[] = [
|
||||
{
|
||||
id: 'coach-001',
|
||||
name: '李明',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
level: '金牌助教',
|
||||
keyMetrics: [
|
||||
{ label: '本月课时', value: '86 节' },
|
||||
{ label: '本月收入', value: '¥18,600' },
|
||||
{ label: '服务客户', value: '32 人' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'coach-002',
|
||||
name: '王芳',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
level: '银牌助教',
|
||||
keyMetrics: [
|
||||
{ label: '本月课时', value: '67 节' },
|
||||
{ label: '本月收入', value: '¥12,680' },
|
||||
{ label: '服务客户', value: '25 人' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'coach-003',
|
||||
name: '刘强',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
level: '铜牌助教',
|
||||
keyMetrics: [
|
||||
{ label: '本月课时', value: '45 节' },
|
||||
{ label: '本月收入', value: '¥8,200' },
|
||||
{ label: '服务客户', value: '18 人' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================
|
||||
// Mock 数据 — 客户详情模块
|
||||
// ============================================================
|
||||
|
||||
export const mockCustomerDetail: CustomerDetail = {
|
||||
id: 'cust-001',
|
||||
name: '张伟',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
tags: ['VIP', '中式台球', '老客户'],
|
||||
heartScore: 9.2,
|
||||
phone: '138****6789',
|
||||
spiIndex: 87,
|
||||
consumptionRecords: [
|
||||
{ id: 'cr-001', date: '2026-03-05', project: '中式台球 1v1', duration: 90, amount: 380, coachName: '王芳' },
|
||||
{ id: 'cr-002', date: '2026-03-01', project: '斯诺克练习', duration: 60, amount: 200, coachName: '李明' },
|
||||
{ id: 'cr-003', date: '2026-02-25', project: '中式台球 1v1', duration: 120, amount: 480, coachName: '王芳' },
|
||||
{ id: 'cr-004', date: '2026-02-20', project: '会员充值', duration: 0, amount: 2000, coachName: '-' },
|
||||
{ id: 'cr-005', date: '2026-02-15', project: '中式台球小组课', duration: 90, amount: 280, coachName: '刘强' },
|
||||
],
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mock 数据 — 对话模块
|
||||
// ============================================================
|
||||
|
||||
export const mockChatMessages: ChatMessage[] = [
|
||||
{
|
||||
id: 'msg-001',
|
||||
role: 'user',
|
||||
content: '帮我看看张伟最近的消费情况',
|
||||
timestamp: '2026-03-05T14:00:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 'msg-002',
|
||||
role: 'assistant',
|
||||
content: '张伟最近一个月消费了 3 次,总金额 ¥1,060。消费频次稳定,主要偏好中式台球 1v1 课程。',
|
||||
timestamp: '2026-03-05T14:00:05+08:00',
|
||||
referenceCard: {
|
||||
type: 'customer',
|
||||
title: '张伟 — 消费概览',
|
||||
summary: '近 30 天消费 3 次,总额 ¥1,060',
|
||||
data: {
|
||||
'最近到店': '2026-03-05',
|
||||
'偏好项目': '中式台球 1v1',
|
||||
'常约助教': '王芳',
|
||||
'爱心评分': '9.2',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'msg-003',
|
||||
role: 'user',
|
||||
content: '他适合推荐什么课程?',
|
||||
timestamp: '2026-03-05T14:01:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 'msg-004',
|
||||
role: 'assistant',
|
||||
content: '根据张伟的消费习惯和技术水平,建议推荐以下课程:\n1. 斯诺克进阶课 — 拓展球类兴趣\n2. 中式台球高级技巧班 — 提升现有水平\n3. 周末球友赛 — 增强社交粘性',
|
||||
timestamp: '2026-03-05T14:01:08+08:00',
|
||||
},
|
||||
{
|
||||
id: 'msg-005',
|
||||
role: 'user',
|
||||
content: '好的,帮我记一下,下次回访时推荐斯诺克进阶课',
|
||||
timestamp: '2026-03-05T14:02:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 'msg-006',
|
||||
role: 'assistant',
|
||||
content: '已记录。下次回访张伟时,我会提醒你推荐斯诺克进阶课程。',
|
||||
timestamp: '2026-03-05T14:02:03+08:00',
|
||||
},
|
||||
]
|
||||
|
||||
export const mockChatHistory: ChatHistoryItem[] = [
|
||||
{
|
||||
id: 'chat-001',
|
||||
title: '张伟消费分析',
|
||||
lastMessage: '已记录。下次回访张伟时,我会提醒你推荐斯诺克进阶课程。',
|
||||
timestamp: '2026-03-05T14:02:03+08:00',
|
||||
customerName: '张伟',
|
||||
},
|
||||
{
|
||||
id: 'chat-002',
|
||||
title: '本周业绩汇总',
|
||||
lastMessage: '本周总收入 ¥18,600,环比上周增长 8.3%。',
|
||||
timestamp: '2026-03-04T09:30:00+08:00',
|
||||
},
|
||||
{
|
||||
id: 'chat-003',
|
||||
title: '李娜召回策略',
|
||||
lastMessage: '建议发送专属优惠券并电话关怀,了解未到店原因。',
|
||||
timestamp: '2026-03-03T16:00:00+08:00',
|
||||
customerName: '李娜',
|
||||
},
|
||||
{
|
||||
id: 'chat-004',
|
||||
title: '新客户接待建议',
|
||||
lastMessage: '首次到店客户建议安排体验课,重点介绍会员权益。',
|
||||
timestamp: '2026-03-02T11:20:00+08:00',
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================
|
||||
// Mock 数据 — 用户信息
|
||||
// ============================================================
|
||||
|
||||
export const mockUserProfile: UserProfile = {
|
||||
name: '小李',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
role: '助教',
|
||||
storeName: '星辰台球俱乐部',
|
||||
}
|
||||
24
apps/miniprogram - 副本/miniprogram/utils/rating.ts
Normal file
24
apps/miniprogram - 副本/miniprogram/utils/rating.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 星级评分转换工具函数
|
||||
* 纯函数,无 wx.* 依赖
|
||||
*
|
||||
* API 使用 0-10 分制,UI 使用 0-5 星制(支持半星)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 将 0-10 分数转换为 0-5 星级
|
||||
* @param score 0-10 分数
|
||||
* @returns 0-5 星级值
|
||||
*/
|
||||
export function scoreToStar(score: number): number {
|
||||
return score / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 0-5 星级转换为 0-10 分数
|
||||
* @param star 0-5 星级值
|
||||
* @returns 0-10 分数
|
||||
*/
|
||||
export function starToScore(star: number): number {
|
||||
return star * 2
|
||||
}
|
||||
21
apps/miniprogram - 副本/miniprogram/utils/render.ts
Normal file
21
apps/miniprogram - 副本/miniprogram/utils/render.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 渲染辅助工具函数
|
||||
* 纯函数,无 wx.* 依赖
|
||||
*/
|
||||
|
||||
/**
|
||||
* 从对象中提取指定字段,缺失字段填充 null
|
||||
* @param item 数据对象
|
||||
* @param fieldList 需要的字段名数组
|
||||
* @returns 仅包含指定字段的新对象
|
||||
*/
|
||||
export function getRequiredFields(
|
||||
item: Record<string, any>,
|
||||
fieldList: string[]
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
for (const field of fieldList) {
|
||||
result[field] = field in item ? item[field] : null
|
||||
}
|
||||
return result
|
||||
}
|
||||
204
apps/miniprogram - 副本/miniprogram/utils/request.ts
Normal file
204
apps/miniprogram - 副本/miniprogram/utils/request.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 统一请求封装
|
||||
*
|
||||
* - 自动附加 Authorization: Bearer <token>
|
||||
* - 401 时自动尝试 refresh_token 刷新,刷新成功后重试原请求
|
||||
* - 刷新失败时清除 token 并跳转 login 页面
|
||||
* - BASE_URL 从 config.ts 读取(按运行环境自动切换)
|
||||
*/
|
||||
|
||||
import { API_BASE } from "./config"
|
||||
|
||||
export interface RequestOptions {
|
||||
url: string
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE"
|
||||
data?: any
|
||||
header?: Record<string, string>
|
||||
/** 是否需要认证,默认 true */
|
||||
needAuth?: boolean
|
||||
}
|
||||
|
||||
/** 是否正在刷新 token,防止并发刷新 */
|
||||
let isRefreshing = false
|
||||
/** 刷新期间排队的请求(等刷新完成后统一重试) */
|
||||
let pendingQueue: Array<{
|
||||
resolve: (value: any) => void
|
||||
reject: (reason: any) => void
|
||||
options: RequestOptions
|
||||
}> = []
|
||||
|
||||
/**
|
||||
* 从 globalData 或 Storage 获取 access_token
|
||||
*/
|
||||
function getAccessToken(): string | undefined {
|
||||
const app = getApp<IAppOption>()
|
||||
return app.globalData.token || wx.getStorageSync("token") || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 globalData 或 Storage 获取 refresh_token
|
||||
*/
|
||||
function getRefreshToken(): string | undefined {
|
||||
const app = getApp<IAppOption>()
|
||||
return app.globalData.refreshToken || wx.getStorageSync("refreshToken") || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除本地存储的所有 token
|
||||
*/
|
||||
function clearTokens(): void {
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.token = undefined
|
||||
app.globalData.refreshToken = undefined
|
||||
wx.removeStorageSync("token")
|
||||
wx.removeStorageSync("refreshToken")
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 token 到 globalData 和 Storage
|
||||
*/
|
||||
function saveTokens(accessToken: string, refreshToken: string): void {
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.token = accessToken
|
||||
app.globalData.refreshToken = refreshToken
|
||||
wx.setStorageSync("token", accessToken)
|
||||
wx.setStorageSync("refreshToken", refreshToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到登录页(使用 reLaunch 清空页面栈)
|
||||
*/
|
||||
function redirectToLogin(): void {
|
||||
wx.reLaunch({ url: "/pages/login/login" })
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行底层 wx.request,返回 Promise
|
||||
*/
|
||||
function wxRequest(options: RequestOptions): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: `${API_BASE}${options.url}`,
|
||||
method: options.method || "GET",
|
||||
data: options.data,
|
||||
header: options.header || {},
|
||||
success(res) {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data)
|
||||
} else {
|
||||
reject({ statusCode: res.statusCode, data: res.data })
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
reject({ statusCode: 0, data: err })
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试用 refresh_token 刷新令牌
|
||||
*
|
||||
* 成功 → 保存新 token 并返回 true
|
||||
* 失败 → 清除 token、跳转登录页、返回 false
|
||||
*/
|
||||
async function tryRefreshToken(): Promise<boolean> {
|
||||
const rt = getRefreshToken()
|
||||
if (!rt) {
|
||||
clearTokens()
|
||||
redirectToLogin()
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await wxRequest({
|
||||
url: "/api/xcx/refresh",
|
||||
method: "POST",
|
||||
data: { refresh_token: rt },
|
||||
header: { "Content-Type": "application/json" },
|
||||
needAuth: false,
|
||||
})
|
||||
if (data.access_token && data.refresh_token) {
|
||||
saveTokens(data.access_token, data.refresh_token)
|
||||
return true
|
||||
}
|
||||
// 响应格式异常,视为刷新失败
|
||||
clearTokens()
|
||||
redirectToLogin()
|
||||
return false
|
||||
} catch {
|
||||
clearTokens()
|
||||
redirectToLogin()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理排队中的请求:刷新成功后全部重试,失败则全部拒绝
|
||||
*/
|
||||
function flushPendingQueue(success: boolean): void {
|
||||
const queue = [...pendingQueue]
|
||||
pendingQueue = []
|
||||
for (const item of queue) {
|
||||
if (success) {
|
||||
// 重试时会自动附加新 token
|
||||
request(item.options).then(item.resolve, item.reject)
|
||||
} else {
|
||||
item.reject({ statusCode: 401, data: { detail: "刷新令牌失败" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一请求入口
|
||||
*
|
||||
* @param options 请求配置
|
||||
* @returns 响应数据(已解析的 JSON)
|
||||
*/
|
||||
export function request(options: RequestOptions): Promise<any> {
|
||||
const needAuth = options.needAuth !== false
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...options.header,
|
||||
}
|
||||
|
||||
// 自动附加 Authorization header
|
||||
if (needAuth) {
|
||||
const token = getAccessToken()
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
const finalOptions: RequestOptions = { ...options, header: headers }
|
||||
|
||||
return wxRequest(finalOptions).catch(async (err) => {
|
||||
// 非 401 或不需要认证的请求,直接抛出
|
||||
if (err.statusCode !== 401 || !needAuth) {
|
||||
throw err
|
||||
}
|
||||
|
||||
// 401 → 尝试刷新 token
|
||||
if (isRefreshing) {
|
||||
// 已有刷新请求在进行中,排队等待
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingQueue.push({ resolve, reject, options })
|
||||
})
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
try {
|
||||
const ok = await tryRefreshToken()
|
||||
flushPendingQueue(ok)
|
||||
if (!ok) {
|
||||
throw { statusCode: 401, data: { detail: "刷新令牌失败" } }
|
||||
}
|
||||
// 刷新成功,用新 token 重试原请求
|
||||
return request(options)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default request
|
||||
45
apps/miniprogram - 副本/miniprogram/utils/router.ts
Normal file
45
apps/miniprogram - 副本/miniprogram/utils/router.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 路由辅助工具函数
|
||||
* getMenuRoute 为纯函数,navigateTo/switchTab/navigateBack 封装 wx.* API
|
||||
*/
|
||||
|
||||
/** 菜单 key → 页面路径映射 */
|
||||
const MENU_ROUTE_MAP: Record<string, string> = {
|
||||
'chat-history': '/pages/chat-history/chat-history',
|
||||
'performance': '/pages/performance/performance',
|
||||
'notes': '/pages/notes/notes',
|
||||
'settings': '',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单项对应的页面路径
|
||||
* @param menuKey 菜单 key
|
||||
* @returns 页面路径,未知 key 返回空字符串
|
||||
*/
|
||||
export function getMenuRoute(menuKey: string): string {
|
||||
return MENU_ROUTE_MAP[menuKey] ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到普通页面
|
||||
* @param url 页面路径
|
||||
*/
|
||||
export function navigateTo(url: string): void {
|
||||
wx.navigateTo({ url })
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 TabBar 页面
|
||||
* @param url TabBar 页面路径
|
||||
*/
|
||||
export function switchTab(url: string): void {
|
||||
wx.switchTab({ url })
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
* @param delta 返回层数,默认 1
|
||||
*/
|
||||
export function navigateBack(delta: number = 1): void {
|
||||
wx.navigateBack({ delta })
|
||||
}
|
||||
52
apps/miniprogram - 副本/miniprogram/utils/sort.ts
Normal file
52
apps/miniprogram - 副本/miniprogram/utils/sort.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 排序工具函数
|
||||
* 纯函数,无 wx.* 依赖,可在 Node.js 测试环境中使用
|
||||
*/
|
||||
|
||||
/**
|
||||
* 按时间戳字段降序排序(最新在前)
|
||||
* @param list 带时间戳字段的对象数组
|
||||
* @param field 时间戳字段名,默认 'timestamp'
|
||||
* @returns 排序后的新数组(不修改原数组)
|
||||
*/
|
||||
export function sortByTimestamp<T extends Record<string, any>>(
|
||||
list: T[],
|
||||
field: string = 'timestamp'
|
||||
): T[] {
|
||||
return [...list].sort((a, b) => {
|
||||
const ta = new Date(a[field]).getTime()
|
||||
const tb = new Date(b[field]).getTime()
|
||||
// NaN 排到末尾
|
||||
if (isNaN(ta) && isNaN(tb)) return 0
|
||||
if (isNaN(ta)) return 1
|
||||
if (isNaN(tb)) return -1
|
||||
return tb - ta
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按指定字段排序
|
||||
* @param list 对象数组
|
||||
* @param field 排序字段名
|
||||
* @param order 排序方向,'asc' 升序 / 'desc' 降序(默认 'desc')
|
||||
* @returns 排序后的新数组
|
||||
*/
|
||||
export function sortByField<T extends Record<string, any>>(
|
||||
list: T[],
|
||||
field: string,
|
||||
order: 'asc' | 'desc' = 'desc'
|
||||
): T[] {
|
||||
return [...list].sort((a, b) => {
|
||||
const va = a[field]
|
||||
const vb = b[field]
|
||||
|
||||
let cmp: number
|
||||
if (typeof va === 'number' && typeof vb === 'number') {
|
||||
cmp = va - vb
|
||||
} else {
|
||||
cmp = String(va ?? '').localeCompare(String(vb ?? ''))
|
||||
}
|
||||
|
||||
return order === 'asc' ? cmp : -cmp
|
||||
})
|
||||
}
|
||||
36
apps/miniprogram - 副本/miniprogram/utils/task.ts
Normal file
36
apps/miniprogram - 副本/miniprogram/utils/task.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 任务类型映射工具函数
|
||||
* 纯函数,无 wx.* 依赖
|
||||
*/
|
||||
|
||||
export type TaskType = 'callback' | 'priority_recall' | 'relationship'
|
||||
|
||||
const TASK_TYPE_COLOR_MAP: Record<TaskType, string> = {
|
||||
callback: '#0052d9',
|
||||
priority_recall: '#e34d59',
|
||||
relationship: '#00a870',
|
||||
}
|
||||
|
||||
const TASK_TYPE_LABEL_MAP: Record<TaskType, string> = {
|
||||
callback: '回访',
|
||||
priority_recall: '优先召回',
|
||||
relationship: '关系构建',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务类型对应的颜色
|
||||
* @param type 任务类型
|
||||
* @returns 颜色 hex 值,未知类型返回灰色
|
||||
*/
|
||||
export function getTaskTypeColor(type: TaskType): string {
|
||||
return TASK_TYPE_COLOR_MAP[type] ?? '#8b8b8b'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务类型对应的中文标签
|
||||
* @param type 任务类型
|
||||
* @returns 中文标签,未知类型返回空字符串
|
||||
*/
|
||||
export function getTaskTypeLabel(type: TaskType): string {
|
||||
return TASK_TYPE_LABEL_MAP[type] ?? ''
|
||||
}
|
||||
19
apps/miniprogram - 副本/miniprogram/utils/util.ts
Normal file
19
apps/miniprogram - 副本/miniprogram/utils/util.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const formatTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return (
|
||||
[year, month, day].map(formatNumber).join('/') +
|
||||
' ' +
|
||||
[hour, minute, second].map(formatNumber).join(':')
|
||||
)
|
||||
}
|
||||
|
||||
const formatNumber = (n: number) => {
|
||||
const s = n.toString()
|
||||
return s[1] ? s : '0' + s
|
||||
}
|
||||
Reference in New Issue
Block a user