feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs
This commit is contained in:
189
tmp/DEMO-miniprogram/miniprogram/utils/ai-color-manager.ts
Normal file
189
tmp/DEMO-miniprogram/miniprogram/utils/ai-color-manager.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* AI 图标配色管理器
|
||||
* 统一管理小程序前端的 AI 图标配色策略
|
||||
* 基于 VI-DESIGN-SYSTEM.md 6.6-6.7 节
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI 配色方案定义
|
||||
* 每种配色包含:主色、浅色、深色三个层级
|
||||
*/
|
||||
export const AI_COLOR_SCHEMES = {
|
||||
red: {
|
||||
name: 'red',
|
||||
label: 'Red',
|
||||
from: '#e74c3c',
|
||||
to: '#f39c9c',
|
||||
fromDeep: '#c0392b',
|
||||
toDeep: '#e74c3c',
|
||||
className: 'ai-color-red',
|
||||
},
|
||||
orange: {
|
||||
name: 'orange',
|
||||
label: 'Orange',
|
||||
from: '#e67e22',
|
||||
to: '#f5c77e',
|
||||
fromDeep: '#ca6c17',
|
||||
toDeep: '#e67e22',
|
||||
className: 'ai-color-orange',
|
||||
},
|
||||
yellow: {
|
||||
name: 'yellow',
|
||||
label: 'Yellow',
|
||||
from: '#d4a017',
|
||||
to: '#f7dc6f',
|
||||
fromDeep: '#b8860b',
|
||||
toDeep: '#d4a017',
|
||||
className: 'ai-color-yellow',
|
||||
},
|
||||
blue: {
|
||||
name: 'blue',
|
||||
label: 'Blue',
|
||||
from: '#2980b9',
|
||||
to: '#7ec8e3',
|
||||
fromDeep: '#1a5276',
|
||||
toDeep: '#2980b9',
|
||||
className: 'ai-color-blue',
|
||||
},
|
||||
indigo: {
|
||||
name: 'indigo',
|
||||
label: 'Indigo',
|
||||
from: '#667eea',
|
||||
to: '#a78bfa',
|
||||
fromDeep: '#4a5fc7',
|
||||
toDeep: '#667eea',
|
||||
className: 'ai-color-indigo',
|
||||
},
|
||||
purple: {
|
||||
name: 'purple',
|
||||
label: 'Purple',
|
||||
from: '#764ba2',
|
||||
to: '#c084fc',
|
||||
fromDeep: '#5b3080',
|
||||
toDeep: '#764ba2',
|
||||
className: 'ai-color-purple',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type AiColorName = keyof typeof AI_COLOR_SCHEMES;
|
||||
|
||||
/**
|
||||
* 页面级 AI 配色建议
|
||||
* 参考 VI-DESIGN-SYSTEM.md 6.7 节
|
||||
*/
|
||||
export const PAGE_AI_COLOR_RECOMMENDATIONS: Record<string, AiColorName | 'random'> = {
|
||||
'task-list': 'random', // 每日新鲜感
|
||||
'task-detail': 'indigo', // 通用默认
|
||||
'task-detail-callback': 'orange', // 与 banner 主题色呼应
|
||||
'task-detail-relationship': 'purple', // 与 banner 粉色系呼应
|
||||
'performance': 'blue', // 数据分析感
|
||||
'customer-detail': 'purple', // 黑金页面,紫色点缀
|
||||
'board-coach': 'red', // coral banner 配红色 AI
|
||||
'board-customer': 'yellow', // 黑金页面,黄色点缀
|
||||
'board-finance': 'purple', // 已正确应用
|
||||
'notes': 'indigo', // 通用默认
|
||||
'reviewing': 'orange', // 审核流程,橙色提示
|
||||
'apply': 'blue', // 申请流程,蓝色信息
|
||||
'login': 'indigo', // 登录页,靛色默认
|
||||
'no-permission': 'red', // 权限提示,红色警示
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取随机 AI 配色
|
||||
* @returns 随机选择的配色方案
|
||||
*/
|
||||
export function getRandomAiColor(): typeof AI_COLOR_SCHEMES[AiColorName] {
|
||||
const colors = Object.values(AI_COLOR_SCHEMES);
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定名称的 AI 配色
|
||||
* @param colorName - 配色名称
|
||||
* @returns 配色方案,如果不存在则返回 indigo(默认)
|
||||
*/
|
||||
export function getAiColor(colorName: AiColorName | string): typeof AI_COLOR_SCHEMES[AiColorName] {
|
||||
return AI_COLOR_SCHEMES[colorName as AiColorName] || AI_COLOR_SCHEMES.indigo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取页面推荐的 AI 配色
|
||||
* @param pageName - 页面名称(通常为 route.name 或自定义标识)
|
||||
* @returns 配色方案
|
||||
*/
|
||||
export function getPageAiColor(pageName: string): typeof AI_COLOR_SCHEMES[AiColorName] {
|
||||
const recommendation = PAGE_AI_COLOR_RECOMMENDATIONS[pageName];
|
||||
|
||||
if (recommendation === 'random') {
|
||||
return getRandomAiColor();
|
||||
}
|
||||
|
||||
if (recommendation) {
|
||||
return getAiColor(recommendation);
|
||||
}
|
||||
|
||||
// 未配置的页面默认使用 indigo
|
||||
return AI_COLOR_SCHEMES.indigo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面初始化 AI 配色
|
||||
* 在页面 onLoad 时调用,自动设置 data.aiColor 和页面类名
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Page({
|
||||
* onLoad() {
|
||||
* const { aiColor, className } = initPageAiColor('task-list');
|
||||
* this.setData({ aiColor });
|
||||
* // 可选:添加页面类名以应用 CSS 变量
|
||||
* wx.pageContainer?.classList.add(className);
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function initPageAiColor(pageName: string) {
|
||||
const colorScheme = getPageAiColor(pageName);
|
||||
return {
|
||||
aiColor: colorScheme.name,
|
||||
className: colorScheme.className,
|
||||
colorScheme,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 配色的 CSS 变量值
|
||||
* 用于动态设置样式
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cssVars = getAiColorCssVars('purple');
|
||||
* // { '--ai-from': '#764ba2', '--ai-to': '#c084fc', ... }
|
||||
* ```
|
||||
*/
|
||||
export function getAiColorCssVars(colorName: AiColorName | string): Record<string, string> {
|
||||
const color = getAiColor(colorName);
|
||||
return {
|
||||
'--ai-from': color.from,
|
||||
'--ai-to': color.to,
|
||||
'--ai-from-deep': color.fromDeep,
|
||||
'--ai-to-deep': color.toDeep,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的 AI 配色列表
|
||||
* @returns 配色名称数组
|
||||
*/
|
||||
export function getAllAiColors(): AiColorName[] {
|
||||
return Object.keys(AI_COLOR_SCHEMES) as AiColorName[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配色名称是否有效
|
||||
* @param colorName - 配色名称
|
||||
* @returns 是否有效
|
||||
*/
|
||||
export function isValidAiColor(colorName: string): colorName is AiColorName {
|
||||
return colorName in AI_COLOR_SCHEMES;
|
||||
}
|
||||
44
tmp/DEMO-miniprogram/miniprogram/utils/ai-color.ts
Normal file
44
tmp/DEMO-miniprogram/miniprogram/utils/ai-color.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* AI 图标随机配色工具
|
||||
* 6 种配色方案,页面 onLoad 时随机选取一种,统一应用到该页面所有 AI 标识
|
||||
* 色值来源:docs/h5_ui/css/ai-icons.css
|
||||
*/
|
||||
|
||||
export interface AiColorVars {
|
||||
'--ai-from': string
|
||||
'--ai-to': string
|
||||
'--ai-from-deep': string
|
||||
'--ai-to-deep': string
|
||||
}
|
||||
|
||||
export interface AiColorResult {
|
||||
className: string
|
||||
vars: AiColorVars
|
||||
}
|
||||
|
||||
type ColorKey = 'red' | 'orange' | 'yellow' | 'blue' | 'indigo' | 'purple'
|
||||
|
||||
const AI_COLOR_SCHEMES: Record<ColorKey, AiColorVars> = {
|
||||
red: { '--ai-from': '#e74c3c', '--ai-to': '#f39c9c', '--ai-from-deep': '#c0392b', '--ai-to-deep': '#e74c3c' },
|
||||
orange: { '--ai-from': '#e67e22', '--ai-to': '#f5c77e', '--ai-from-deep': '#ca6c17', '--ai-to-deep': '#e67e22' },
|
||||
yellow: { '--ai-from': '#d4a017', '--ai-to': '#f7dc6f', '--ai-from-deep': '#b8860b', '--ai-to-deep': '#d4a017' },
|
||||
blue: { '--ai-from': '#2980b9', '--ai-to': '#7ec8e3', '--ai-from-deep': '#1a5276', '--ai-to-deep': '#2980b9' },
|
||||
indigo: { '--ai-from': '#667eea', '--ai-to': '#a78bfa', '--ai-from-deep': '#4a5fc7', '--ai-to-deep': '#667eea' },
|
||||
purple: { '--ai-from': '#764ba2', '--ai-to': '#c084fc', '--ai-from-deep': '#5b3080', '--ai-to-deep': '#764ba2' },
|
||||
}
|
||||
|
||||
const COLOR_KEYS: ColorKey[] = ['red', 'orange', 'yellow', 'blue', 'indigo', 'purple']
|
||||
|
||||
/**
|
||||
* 随机选取一种 AI 配色方案
|
||||
* 返回 className(用于 WXML class 绑定)和 vars(CSS 变量值)
|
||||
*/
|
||||
export function getRandomAiColor(): AiColorResult {
|
||||
const key = COLOR_KEYS[Math.floor(Math.random() * COLOR_KEYS.length)]
|
||||
return {
|
||||
className: `ai-color-${key}`,
|
||||
vars: AI_COLOR_SCHEMES[key],
|
||||
}
|
||||
}
|
||||
|
||||
export { AI_COLOR_SCHEMES }
|
||||
51
tmp/DEMO-miniprogram/miniprogram/utils/avatar-color.ts
Normal file
51
tmp/DEMO-miniprogram/miniprogram/utils/avatar-color.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 头像颜色工具
|
||||
* 基于 VI 设计系统 §8 头像颜色系统
|
||||
*
|
||||
* 用法:
|
||||
* import { nameToAvatarColor } from '../../utils/avatar-color'
|
||||
* const avatarColor = nameToAvatarColor('王先生') // => 'blue'
|
||||
* // wxml: class="avatar-{{rec.avatarColor}}"
|
||||
*/
|
||||
|
||||
/** 24 色标准色板 key 列表(与 app.wxss .avatar-{key} 一一对应) */
|
||||
export const AVATAR_PALETTE = [
|
||||
'blue',
|
||||
'indigo',
|
||||
'violet',
|
||||
'purple',
|
||||
'fuchsia',
|
||||
'pink',
|
||||
'rose',
|
||||
'red',
|
||||
'orange',
|
||||
'amber',
|
||||
'yellow',
|
||||
'lime',
|
||||
'green',
|
||||
'emerald',
|
||||
'teal',
|
||||
'cyan',
|
||||
'sky',
|
||||
'slate',
|
||||
'coral',
|
||||
'mint',
|
||||
'lavender',
|
||||
'gold',
|
||||
'crimson',
|
||||
'ocean',
|
||||
] as const
|
||||
|
||||
export type AvatarColorKey = typeof AVATAR_PALETTE[number]
|
||||
|
||||
/**
|
||||
* 根据名字首字(或任意字符串)稳定映射到头像颜色 key。
|
||||
* 相同输入永远返回相同颜色,适合用于客户/助教头像。
|
||||
*
|
||||
* @param name 姓名或任意标识符(取第一个字符的 charCode)
|
||||
* @returns AvatarColorKey
|
||||
*/
|
||||
export function nameToAvatarColor(name: string): AvatarColorKey {
|
||||
const code = (name?.charCodeAt(0) ?? 0)
|
||||
return AVATAR_PALETTE[code % AVATAR_PALETTE.length]
|
||||
}
|
||||
49
tmp/DEMO-miniprogram/miniprogram/utils/chat.ts
Normal file
49
tmp/DEMO-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
tmp/DEMO-miniprogram/miniprogram/utils/config.ts
Normal file
26
tmp/DEMO-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
tmp/DEMO-miniprogram/miniprogram/utils/filter.ts
Normal file
23
tmp/DEMO-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])
|
||||
)
|
||||
}
|
||||
83
tmp/DEMO-miniprogram/miniprogram/utils/format.wxs
Normal file
83
tmp/DEMO-miniprogram/miniprogram/utils/format.wxs
Normal file
@@ -0,0 +1,83 @@
|
||||
// WXS 格式化工具 — WXML 中不能调用 JS 方法,需通过 WXS 桥接
|
||||
// 规范:docs/miniprogram-dev/design-system/DISPLAY-STANDARDS.md
|
||||
|
||||
/** 数字保留 N 位小数;空值返回 '--' */
|
||||
function toFixed(num, digits) {
|
||||
if (num === undefined || num === null) return '--'
|
||||
return num.toFixed(digits)
|
||||
}
|
||||
|
||||
/** 空值兜底;null/undefined/'' 统一返回 '--' */
|
||||
function safe(val) {
|
||||
if (val === undefined || val === null || val === '') return '--'
|
||||
return val
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额格式化(WXS 版)
|
||||
* ¥12,680 / -¥368 / ¥0 / --
|
||||
*/
|
||||
function money(value) {
|
||||
if (value === undefined || value === null) return '--'
|
||||
if (value === 0) return '¥0'
|
||||
var abs = Math.round(Math.abs(value))
|
||||
var s = abs.toString()
|
||||
var result = ''
|
||||
var count = 0
|
||||
for (var i = s.length - 1; i >= 0; i--) {
|
||||
if (count > 0 && count % 3 === 0) result = ',' + result
|
||||
result = s[i] + result
|
||||
count++
|
||||
}
|
||||
return (value < 0 ? '-¥' : '¥') + result
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数格式化(WXS 版)
|
||||
* 零值返回 '--';≥1000 加千分位;自动拼接单位
|
||||
*/
|
||||
function count(value, unit) {
|
||||
if (value === undefined || value === null || value === 0) return '--'
|
||||
var s = value.toString()
|
||||
if (value >= 1000) {
|
||||
var result = ''
|
||||
var c = 0
|
||||
for (var i = s.length - 1; i >= 0; i--) {
|
||||
if (c > 0 && c % 3 === 0) result = ',' + result
|
||||
result = s[i] + result
|
||||
c++
|
||||
}
|
||||
return result + unit
|
||||
}
|
||||
return s + unit
|
||||
}
|
||||
|
||||
/**
|
||||
* 百分比展示(WXS 版)
|
||||
* 保留1位小数;超过100%正常展示;空值返回 '--'
|
||||
*/
|
||||
function percent(value) {
|
||||
if (value === undefined || value === null) return '--'
|
||||
if (value === 0) return '0%'
|
||||
return value.toFixed(1) + '%'
|
||||
}
|
||||
|
||||
/**
|
||||
* 课时格式化(WXS 版)
|
||||
* 整数 → Nh;非整数 → N.Nh;0 → 0h;空 → --
|
||||
*/
|
||||
function hours(value) {
|
||||
if (value === undefined || value === null) return '--'
|
||||
if (value === 0) return '0h'
|
||||
if (value % 1 === 0) return value + 'h'
|
||||
return value.toFixed(1) + 'h'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toFixed: toFixed,
|
||||
safe: safe,
|
||||
money: money,
|
||||
count: count,
|
||||
percent: percent,
|
||||
hours: hours,
|
||||
}
|
||||
21
tmp/DEMO-miniprogram/miniprogram/utils/heart.ts
Normal file
21
tmp/DEMO-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 '💙'
|
||||
}
|
||||
755
tmp/DEMO-miniprogram/miniprogram/utils/mock-data.ts
Normal file
755
tmp/DEMO-miniprogram/miniprogram/utils/mock-data.ts
Normal file
@@ -0,0 +1,755 @@
|
||||
// TODO: 联调时删除此文件,改为真实 API 调用
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
export type TaskType = 'callback' | 'priority_recall' | 'relationship' | 'high_priority'
|
||||
|
||||
/** 备注评分记录 */
|
||||
export interface Note {
|
||||
id: string
|
||||
content: string
|
||||
tagType: 'customer' | 'coach' | 'system'
|
||||
tagLabel: string
|
||||
createdAt: string
|
||||
/** 满意度评分 0-10,0 表示未评分 */
|
||||
score?: number
|
||||
/** 格式化时间标签(由前端附加) */
|
||||
timeLabel?: string
|
||||
}
|
||||
|
||||
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 RetentionClue {
|
||||
id: string
|
||||
/** 线索大类 */
|
||||
category: 'customer_basic' | 'consumption' | 'play_pref' | 'promo_pref' | 'social' | 'feedback'
|
||||
/** 摘要 */
|
||||
summary: string
|
||||
/** 详情 */
|
||||
detail?: string
|
||||
/** 线索来源:manual=助教手动,ai_consumption=系统自动,ai_note=备注分析 */
|
||||
source: 'manual' | 'ai_consumption' | 'ai_note'
|
||||
/** 记录助教ID(用于判断是否为"我") */
|
||||
recordedByAssistantId?: string
|
||||
/** 记录助教姓名 */
|
||||
recordedByName?: 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: 'high_priority',
|
||||
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],
|
||||
customerName: '王先生',
|
||||
taskTypeLabel: '高优先召回',
|
||||
heartScore: 8.5,
|
||||
aiAnalysis: {
|
||||
summary: '最近 3 个月每周均有 1-2 次课程互动,客户反馈良好。上次服务评价 5 星,多次指定您为服务助教。',
|
||||
suggestions: ['询问近期是否有空,邀请体验新到的器材', '告知本周末有会员专属活动', '根据其偏好时段(晚间)推荐合适的时间'],
|
||||
},
|
||||
notes: [
|
||||
{ id: 'note-h5-1', content: '已通过微信联系王先生,表示对新到的斯诺克球桌感兴趣,周末可能来体验。', tagType: 'customer' as const, tagLabel: '客户:王先生', createdAt: '2026-02-05' },
|
||||
{ id: 'note-h5-2', content: '王先生最近出差较多,到店频率降低。建议等他回来后再约。', tagType: 'customer' as const, tagLabel: '客户:王先生', createdAt: '2026-01-20' },
|
||||
{ id: 'note-h5-3', content: '上次到店时推荐了会员续费活动,客户说考虑一下。', tagType: 'customer' as const, tagLabel: '客户:王先生', createdAt: '2026-01-08' },
|
||||
],
|
||||
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 数据
|
||||
// 说明:
|
||||
// - 后端已过滤,客户端直接使用
|
||||
// - 任务详情页:后端返回脱敏版本(移除 recordedByAssistantId 和 recordedByName)
|
||||
// - 客户详情页:后端返回完整版本(包含 recordedByAssistantId 和 recordedByName)
|
||||
//
|
||||
// 以下是任务详情页的 MOCK 数据(脱敏版本)
|
||||
export const mockRetentionClues: RetentionClue[] = [
|
||||
{
|
||||
id: 'clue-001',
|
||||
category: 'consumption',
|
||||
summary: '最近 3 个月每周均有 1-2 次课程互动,客户反馈良好',
|
||||
detail: '上次服务评价 5 星,多次指定您为服务助教。消费频次稳定,主要偏好中式台球 1v1 课程。',
|
||||
source: 'ai_consumption',
|
||||
createdAt: '2026-03-05 14:00',
|
||||
},
|
||||
{
|
||||
id: 'clue-002',
|
||||
category: 'play_pref',
|
||||
summary: '中式台球爱好者,技术水平中等偏上',
|
||||
detail: '对斯诺克也有兴趣,建议推荐进阶课程。',
|
||||
source: 'manual',
|
||||
recordedByAssistantId: 'assistant-001',
|
||||
recordedByName: '王芳',
|
||||
createdAt: '2026-03-01 10:30',
|
||||
},
|
||||
{
|
||||
id: 'clue-003',
|
||||
category: 'social',
|
||||
summary: '社交活跃,经常带朋友来店',
|
||||
detail: '可以发展为核心会员,推荐参加周末球友赛。',
|
||||
source: 'ai_note',
|
||||
// 后端脱敏:移除 recordedByAssistantId 和 recordedByName
|
||||
createdAt: '2026-02-28 16:45',
|
||||
},
|
||||
{
|
||||
id: 'clue-004',
|
||||
category: 'promo_pref',
|
||||
summary: '对储值活动敏感,倾向于大额充值',
|
||||
detail: '上次充值 2000 元,选择尊享套餐。建议定期推送专属优惠。',
|
||||
source: 'manual',
|
||||
// 后端脱敏:移除 recordedByAssistantId 和 recordedByName(因为不是当前用户)
|
||||
createdAt: '2026-02-25 09:15',
|
||||
},
|
||||
]
|
||||
|
||||
// 客户详情页的 MOCK 数据(完整版本,不脱敏)
|
||||
export const mockRetentionCluesForCustomerDetail: RetentionClue[] = [
|
||||
{
|
||||
id: 'clue-001',
|
||||
category: 'consumption',
|
||||
summary: '最近 3 个月每周均有 1-2 次课程互动,客户反馈良好',
|
||||
detail: '上次服务评价 5 星,多次指定您为服务助教。消费频次稳定,主要偏好中式台球 1v1 课程。',
|
||||
source: 'ai_consumption',
|
||||
createdAt: '2026-03-05 14:00',
|
||||
},
|
||||
{
|
||||
id: 'clue-002',
|
||||
category: 'play_pref',
|
||||
summary: '中式台球爱好者,技术水平中等偏上',
|
||||
detail: '对斯诺克也有兴趣,建议推荐进阶课程。',
|
||||
source: 'manual',
|
||||
recordedByAssistantId: 'assistant-001',
|
||||
recordedByName: '王芳',
|
||||
createdAt: '2026-03-01 10:30',
|
||||
},
|
||||
{
|
||||
id: 'clue-003',
|
||||
category: 'social',
|
||||
summary: '社交活跃,经常带朋友来店',
|
||||
detail: '可以发展为核心会员,推荐参加周末球友赛。',
|
||||
source: 'ai_note',
|
||||
recordedByAssistantId: 'assistant-003',
|
||||
recordedByName: '李明',
|
||||
createdAt: '2026-02-28 16:45',
|
||||
},
|
||||
{
|
||||
id: 'clue-004',
|
||||
category: 'promo_pref',
|
||||
summary: '对储值活动敏感,倾向于大额充值',
|
||||
detail: '上次充值 2000 元,选择尊享套餐。建议定期推送专属优惠。',
|
||||
source: 'manual',
|
||||
recordedByAssistantId: 'assistant-002',
|
||||
recordedByName: '刘强',
|
||||
createdAt: '2026-02-25 09:15',
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================
|
||||
// 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: 'Lucy最近工作状态很好,主动承担了培训新员工的任务。但需要注意她最近加班较多,避免过度疲劳。建议适当调整排班。',
|
||||
tagType: 'coach',
|
||||
tagLabel: '助教:Lucy',
|
||||
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: 'Lucy本周请假2天处理家事,已安排泡芙和小燕分担她的客户。回来后需要跟进客户交接情况。',
|
||||
tagType: 'coach',
|
||||
tagLabel: '助教:Lucy',
|
||||
createdAt: '2024-11-22 10:15',
|
||||
},
|
||||
{
|
||||
id: 'note-012',
|
||||
content: 'Lucy最近工作状态很好,主动承担了培训新员工的任务。但需要注意她最近加班较多,避免过度疲劳。',
|
||||
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',
|
||||
referenceCard: {
|
||||
type: 'customer',
|
||||
title: '张伟 — 消费概览',
|
||||
summary: '近 30 天消费 3 次,总额 ¥1,060',
|
||||
data: {
|
||||
'最近到店': '2026-03-05',
|
||||
'偏好项目': '中式台球 1v1',
|
||||
'常约助教': '王芳',
|
||||
'爱心评分': '9.2',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'msg-002',
|
||||
role: 'assistant',
|
||||
content: '张伟最近一个月消费了 3 次,总金额 ¥1,060。消费频次稳定,主要偏好中式台球 1v1 课程。',
|
||||
timestamp: '2026-03-05T14:00:05+08:00',
|
||||
},
|
||||
{
|
||||
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-coach.png',
|
||||
role: '助教',
|
||||
storeName: '朗朗桌球',
|
||||
}
|
||||
65
tmp/DEMO-miniprogram/miniprogram/utils/money.ts
Normal file
65
tmp/DEMO-miniprogram/miniprogram/utils/money.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 金额 / 计数 / 百分比格式化工具
|
||||
* 规范:docs/miniprogram-dev/design-system/DISPLAY-STANDARDS.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* 金额格式化
|
||||
* ¥12,680 / -¥368 / ¥0 / --
|
||||
* @param value 金额(元,整数)
|
||||
*/
|
||||
export function formatMoney(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '--'
|
||||
if (value === 0) return '¥0'
|
||||
const abs = Math.round(Math.abs(value))
|
||||
const formatted = abs.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return value < 0 ? `-¥${formatted}` : `¥${formatted}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数格式化(带单位)
|
||||
* 零值返回 '--';≥1000 加千分位
|
||||
* @param value 数值
|
||||
* @param unit 单位字符串,如 '笔' '次' '人'
|
||||
*/
|
||||
export function formatCount(
|
||||
value: number | null | undefined,
|
||||
unit: string,
|
||||
): string {
|
||||
if (value === null || value === undefined || value === 0) return '--'
|
||||
const n =
|
||||
value >= 1000
|
||||
? value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
: String(value)
|
||||
return `${n}${unit}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯数字千分位格式化(无单位,用于非金额大数字)
|
||||
* 零值返回 '--'
|
||||
*/
|
||||
export function formatNumber(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined || value === 0) return '--'
|
||||
return value >= 1000
|
||||
? value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
: String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 百分比展示(允许超过100%)
|
||||
* 保留1位小数;零值返回 '0%';空值返回 '--'
|
||||
*/
|
||||
export function formatPercent(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '--'
|
||||
if (value === 0) return '0%'
|
||||
return `${value.toFixed(1)}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度条 CSS 宽度字符串(截断至 [0, 100],防止溢出)
|
||||
* 用法:style="width: {{toProgressWidth(perfData.filledPct)}}"
|
||||
*/
|
||||
export function toProgressWidth(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '0%'
|
||||
return `${Math.min(100, Math.max(0, value)).toFixed(1)}%`
|
||||
}
|
||||
43
tmp/DEMO-miniprogram/miniprogram/utils/rating.ts
Normal file
43
tmp/DEMO-miniprogram/miniprogram/utils/rating.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 星级评分转换工具函数
|
||||
* 纯函数,无 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 0-10 分数四舍五入到最近 0.5 星
|
||||
* 用于后端评分 → 半星展示
|
||||
* @param score 0-10 分数
|
||||
* @returns 0-5 星(步长 0.5)
|
||||
*/
|
||||
export function scoreToHalfStar(score: number): number {
|
||||
return Math.round((score / 2) * 2) / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为未评分状态
|
||||
* score 为 null / undefined / 0 时视为未评分
|
||||
* @returns true 时展示 '--' 替代星星
|
||||
*/
|
||||
export function isUnrated(score: number | null | undefined): boolean {
|
||||
return score === null || score === undefined || score === 0
|
||||
}
|
||||
21
tmp/DEMO-miniprogram/miniprogram/utils/render.ts
Normal file
21
tmp/DEMO-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
tmp/DEMO-miniprogram/miniprogram/utils/request.ts
Normal file
204
tmp/DEMO-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
tmp/DEMO-miniprogram/miniprogram/utils/router.ts
Normal file
45
tmp/DEMO-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
tmp/DEMO-miniprogram/miniprogram/utils/sort.ts
Normal file
52
tmp/DEMO-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
|
||||
})
|
||||
}
|
||||
58
tmp/DEMO-miniprogram/miniprogram/utils/task-config.ts
Normal file
58
tmp/DEMO-miniprogram/miniprogram/utils/task-config.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 任务类型配置
|
||||
*/
|
||||
export const TASK_TYPE_CONFIG = {
|
||||
high_priority: {
|
||||
label: '高优先召回',
|
||||
color: 'red',
|
||||
bgColor: '#fee2e2',
|
||||
borderColor: '#fca5a5',
|
||||
textColor: '#b91c1c',
|
||||
bannerBg: '/assets/images/banner-bg-red-aurora.svg',
|
||||
},
|
||||
priority_recall: {
|
||||
label: '优先召回',
|
||||
color: 'orange',
|
||||
bgColor: '#fed7aa',
|
||||
borderColor: '#fdba74',
|
||||
textColor: '#b45309',
|
||||
bannerBg: '/assets/images/banner-bg-orange-aurora.svg',
|
||||
},
|
||||
relationship: {
|
||||
label: '关系构建',
|
||||
color: 'pink',
|
||||
bgColor: '#fce7f3',
|
||||
borderColor: '#f9a8d4',
|
||||
textColor: '#be185d',
|
||||
bannerBg: '/assets/images/banner-bg-pink-aurora.svg',
|
||||
},
|
||||
callback: {
|
||||
label: '客户回访',
|
||||
color: 'teal',
|
||||
bgColor: '#ccfbf1',
|
||||
borderColor: '#99f6e4',
|
||||
textColor: '#0d9488',
|
||||
bannerBg: '/assets/images/banner-bg-teal-aurora.svg',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 任务状态配置
|
||||
*/
|
||||
export const TASK_STATUS_CONFIG = {
|
||||
normal: {
|
||||
label: '进行中',
|
||||
icon: '📋',
|
||||
},
|
||||
pinned: {
|
||||
label: '已置顶',
|
||||
icon: '📌',
|
||||
},
|
||||
abandoned: {
|
||||
label: '已放弃',
|
||||
icon: '❌',
|
||||
},
|
||||
} as const
|
||||
|
||||
export type TaskType = keyof typeof TASK_TYPE_CONFIG
|
||||
export type TaskStatus = keyof typeof TASK_STATUS_CONFIG
|
||||
36
tmp/DEMO-miniprogram/miniprogram/utils/task.ts
Normal file
36
tmp/DEMO-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] ?? ''
|
||||
}
|
||||
126
tmp/DEMO-miniprogram/miniprogram/utils/time.ts
Normal file
126
tmp/DEMO-miniprogram/miniprogram/utils/time.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 时间展示工具
|
||||
* 规范:docs/miniprogram-dev/design-system/DATETIME-DISPLAY-STANDARD.md
|
||||
*
|
||||
* 规则(由近及远):
|
||||
* < 120s → 刚刚
|
||||
* 2min ~ 59min → N分钟前
|
||||
* 1h ~ 23h → N小时前
|
||||
* 1d ~ 3d → N天前
|
||||
* > 3d 同年 → MM-DD
|
||||
* > 3d 跨年 → YYYY-MM-DD
|
||||
*/
|
||||
|
||||
/**
|
||||
* 将时间戳或 ISO 字符串格式化为相对时间文案
|
||||
* @param value Unix 毫秒时间戳 或 ISO 8601 字符串(如 "2026-03-10T16:30:00Z")
|
||||
* @returns 格式化文案,如「刚刚」「2分钟前」「03-10」
|
||||
*/
|
||||
/**
|
||||
* 课时格式化
|
||||
* 整数 → Nh;非整数保留1位 → N.Nh;零值 → 0h;空值 → --
|
||||
*/
|
||||
export function formatHours(hours: number | null | undefined): string {
|
||||
if (hours === null || hours === undefined) return '--'
|
||||
if (hours === 0) return '0h'
|
||||
return hours % 1 === 0 ? `${hours}h` : `${hours.toFixed(1)}h`
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务截止日期语义化格式化
|
||||
* 规范:docs/miniprogram-dev/design-system/DISPLAY-STANDARDS-2.md §7
|
||||
*
|
||||
* @returns { text, style }
|
||||
* style: 'muted'(灰) | 'normal'(正常) | 'warning'(橙/今天) | 'danger'(红/逾期)
|
||||
*/
|
||||
export function formatDeadline(
|
||||
deadline: string | null | undefined,
|
||||
): { text: string; style: 'normal' | 'warning' | 'danger' | 'muted' } {
|
||||
if (!deadline) return { text: '--', style: 'muted' }
|
||||
const now = new Date()
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const target = new Date(deadline)
|
||||
const targetDay = new Date(target.getFullYear(), target.getMonth(), target.getDate())
|
||||
const diff = Math.round((targetDay.getTime() - today.getTime()) / 86400000)
|
||||
if (diff < 0) return { text: `逾期 ${Math.abs(diff)} 天`, style: 'danger' }
|
||||
if (diff === 0) return { text: '今天到期', style: 'warning' }
|
||||
if (diff <= 7) return { text: `还剩 ${diff} 天`, style: 'normal' }
|
||||
const m = String(target.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(target.getDate()).padStart(2, '0')
|
||||
return { text: `${m}-${d}`, style: 'muted' }
|
||||
}
|
||||
|
||||
export function formatRelativeTime(value: number | string | undefined | null): string {
|
||||
if (value === undefined || value === null || value === '') return '--'
|
||||
|
||||
const normalized = typeof value === 'string'
|
||||
? value.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2')
|
||||
: value
|
||||
const ts = typeof normalized === 'number' ? normalized : new Date(normalized).getTime()
|
||||
if (isNaN(ts)) return '--'
|
||||
|
||||
const now = Date.now()
|
||||
const diff = Math.floor((now - ts) / 1000) // 秒
|
||||
|
||||
// 未来时间(服务端时钟偏差)按「刚刚」处理
|
||||
if (diff < 120) return '刚刚'
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
|
||||
if (diff < 259200) return `${Math.floor(diff / 86400)}天前`
|
||||
|
||||
const date = new Date(ts)
|
||||
const nowDate = new Date(now)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
if (year === nowDate.getFullYear()) return `${month}-${day}`
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* IM 气泡内时间戳格式
|
||||
* 今天内 → HH:mm
|
||||
* 今年非今天 → MM-DD HH:mm
|
||||
* 跨年 → YYYY-MM-DD HH:mm
|
||||
*/
|
||||
export function formatIMTime(value: number | string | undefined | null): string {
|
||||
if (value === undefined || value === null || value === '') return ''
|
||||
const normalized = typeof value === 'string'
|
||||
? value.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2')
|
||||
: value
|
||||
const ts = typeof normalized === 'number' ? normalized : new Date(normalized).getTime()
|
||||
if (isNaN(ts)) return ''
|
||||
|
||||
const date = new Date(ts)
|
||||
const now = new Date()
|
||||
const hh = String(date.getHours()).padStart(2, '0')
|
||||
const mn = String(date.getMinutes()).padStart(2, '0')
|
||||
const timeStr = `${hh}:${mn}`
|
||||
const isSameDay =
|
||||
date.getFullYear() === now.getFullYear() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getDate() === now.getDate()
|
||||
if (isSameDay) return timeStr
|
||||
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
if (date.getFullYear() === now.getFullYear()) return `${month}-${day} ${timeStr}`
|
||||
return `${date.getFullYear()}-${month}-${day} ${timeStr}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断相邻消息是否需要显示时间分割线(间隔 >= 5 分钟,首条始终显示)
|
||||
*/
|
||||
export function shouldShowTimeDivider(
|
||||
prevTimestamp: number | string | null | undefined,
|
||||
currTimestamp: number | string | undefined | null,
|
||||
): boolean {
|
||||
if (!prevTimestamp) return true
|
||||
const normPrev = typeof prevTimestamp === 'string' ? prevTimestamp.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2') : prevTimestamp
|
||||
const normCurr = typeof currTimestamp === 'string' ? (currTimestamp as string).replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2') : currTimestamp
|
||||
const prev = typeof normPrev === 'number' ? normPrev : new Date(normPrev).getTime()
|
||||
const curr = typeof normCurr === 'number' ? normCurr : new Date(normCurr as string).getTime()
|
||||
if (isNaN(prev) || isNaN(curr)) return false
|
||||
return curr - prev >= 5 * 60 * 1000
|
||||
}
|
||||
86
tmp/DEMO-miniprogram/miniprogram/utils/time.wxs
Normal file
86
tmp/DEMO-miniprogram/miniprogram/utils/time.wxs
Normal file
@@ -0,0 +1,86 @@
|
||||
// WXS 时间格式化桥接 — WXML 中不能调用 TS/JS 模块,需通过 WXS 桥接
|
||||
// 规范:docs/miniprogram-dev/design-system/DATETIME-DISPLAY-STANDARD.md
|
||||
//
|
||||
// 规则:
|
||||
// < 120s -> 刚刚
|
||||
// 2~59min -> N分钟前
|
||||
// 1~23h -> N小时前
|
||||
// 1~3d -> N天前
|
||||
// >3d 同年 -> MM-DD
|
||||
// >3d 跨年 -> YYYY-MM-DD
|
||||
|
||||
function relativeTime(value) {
|
||||
if (value === undefined || value === null || value === '') return '--'
|
||||
|
||||
var ts
|
||||
if (typeof value === 'number') {
|
||||
ts = value
|
||||
} else {
|
||||
var parsed = getDate(value)
|
||||
ts = parsed.getTime()
|
||||
if (isNaN(ts)) return '--'
|
||||
}
|
||||
|
||||
var now = getDate().getTime()
|
||||
var diff = Math.floor((now - ts) / 1000)
|
||||
|
||||
if (diff < 120) return '刚刚'
|
||||
if (diff < 3600) return Math.floor(diff / 60) + '分钟前'
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + '小时前'
|
||||
if (diff < 259200) return Math.floor(diff / 86400) + '天前'
|
||||
|
||||
var date = getDate(ts)
|
||||
var nowDate = getDate(now)
|
||||
var year = date.getFullYear()
|
||||
var nowYear = nowDate.getFullYear()
|
||||
var month = date.getMonth() + 1
|
||||
var day = date.getDate()
|
||||
var mm = month < 10 ? '0' + month : '' + month
|
||||
var dd = day < 10 ? '0' + day : '' + day
|
||||
|
||||
if (year === nowYear) return mm + '-' + dd
|
||||
return year + '-' + mm + '-' + dd
|
||||
}
|
||||
|
||||
// IM 气泡内时间戳格式
|
||||
// 今天内 -> HH:mm
|
||||
// 今年非今天 -> MM-DD HH:mm
|
||||
// 跨年 -> YYYY-MM-DD HH:mm
|
||||
function imTime(value) {
|
||||
if (value === undefined || value === null || value === '') return ''
|
||||
|
||||
var ts
|
||||
if (typeof value === 'number') {
|
||||
ts = value
|
||||
} else {
|
||||
var parsed = getDate(value)
|
||||
ts = parsed.getTime()
|
||||
if (isNaN(ts)) return ''
|
||||
}
|
||||
|
||||
var date = getDate(ts)
|
||||
var now = getDate()
|
||||
var hh = date.getHours()
|
||||
var mn = date.getMinutes()
|
||||
var hhStr = hh < 10 ? '0' + hh : '' + hh
|
||||
var mnStr = mn < 10 ? '0' + mn : '' + mn
|
||||
var timeStr = hhStr + ':' + mnStr
|
||||
|
||||
var isSameDay =
|
||||
date.getFullYear() === now.getFullYear() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getDate() === now.getDate()
|
||||
if (isSameDay) return timeStr
|
||||
|
||||
var month = date.getMonth() + 1
|
||||
var day = date.getDate()
|
||||
var mm = month < 10 ? '0' + month : '' + month
|
||||
var dd = day < 10 ? '0' + day : '' + day
|
||||
if (date.getFullYear() === now.getFullYear()) return mm + '-' + dd + ' ' + timeStr
|
||||
return date.getFullYear() + '-' + mm + '-' + dd + ' ' + timeStr
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
relativeTime: relativeTime,
|
||||
imTime: imTime,
|
||||
}
|
||||
30
tmp/DEMO-miniprogram/miniprogram/utils/ui-constants.ts
Normal file
30
tmp/DEMO-miniprogram/miniprogram/utils/ui-constants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 全局 UI 常量
|
||||
* 集中管理 showModal / showToast 中重复使用的样式和文案
|
||||
*/
|
||||
|
||||
import { GLOBAL_COLORS } from './vi-colors'
|
||||
|
||||
// ============================================
|
||||
// Modal 样式
|
||||
// ============================================
|
||||
|
||||
/** 危险操作确认按钮颜色(删除、退出等) */
|
||||
export const CONFIRM_DANGER_COLOR = GLOBAL_COLORS.error // #e34d59
|
||||
|
||||
// ============================================
|
||||
// Toast 文案
|
||||
// ============================================
|
||||
|
||||
export const TOAST = {
|
||||
/** 页面跳转失败(9 处使用) */
|
||||
NAV_FAILED: '页面跳转失败',
|
||||
/** 已删除 */
|
||||
DELETED: '已删除',
|
||||
/** 备注已保存 */
|
||||
NOTE_SAVED: '备注已保存',
|
||||
/** 手机号码已复制 */
|
||||
PHONE_COPIED: '手机号码已复制',
|
||||
/** 获取状态失败 */
|
||||
STATUS_FAILED: '获取状态失败',
|
||||
} as const
|
||||
19
tmp/DEMO-miniprogram/miniprogram/utils/util.ts
Normal file
19
tmp/DEMO-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
|
||||
}
|
||||
346
tmp/DEMO-miniprogram/miniprogram/utils/vi-colors.ts
Normal file
346
tmp/DEMO-miniprogram/miniprogram/utils/vi-colors.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* VI 标准常量库
|
||||
* 微信小程序前端配色系统
|
||||
* 最后更新:2026-03-17
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// 1. 任务分类配色
|
||||
// ============================================
|
||||
export const TASK_TYPE_COLORS = {
|
||||
high_priority: {
|
||||
name: '高优先召回',
|
||||
priority: 0,
|
||||
borderColor: '#dc2626',
|
||||
gradientFrom: '#b91c1c',
|
||||
gradientTo: '#dc2626',
|
||||
condition: 'max(WBI,NCI) > 7',
|
||||
},
|
||||
priority_recall: {
|
||||
name: '优先召回',
|
||||
priority: 0,
|
||||
borderColor: '#f97316',
|
||||
gradientFrom: '#ea580c',
|
||||
gradientTo: '#f97316',
|
||||
condition: 'max(WBI,NCI) > 5',
|
||||
},
|
||||
callback: {
|
||||
name: '客户回访',
|
||||
priority: 1,
|
||||
borderColor: '#14b8a6',
|
||||
gradientFrom: '#0d9488',
|
||||
gradientTo: '#14b8a6',
|
||||
condition: '完成召回后未备注',
|
||||
},
|
||||
relationship: {
|
||||
name: '关系构建',
|
||||
priority: 2,
|
||||
borderColor: '#f472b6',
|
||||
gradientFrom: '#ec4899',
|
||||
gradientTo: '#f472b6',
|
||||
condition: 'RS < 6',
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 2. 客户标签配色(6种)
|
||||
// ============================================
|
||||
export const CUSTOMER_TAG_COLORS = {
|
||||
basic_info: {
|
||||
textColor: '#0052d9',
|
||||
backgroundColor: '#ecf2fe',
|
||||
borderColor: '#bfdbfe',
|
||||
psychology: '蓝色 - 信任、基础信息',
|
||||
},
|
||||
consumption: {
|
||||
textColor: '#00a870',
|
||||
backgroundColor: '#e6f7f0',
|
||||
borderColor: '#a7f3d0',
|
||||
psychology: '绿色 - 增长、消费行为',
|
||||
},
|
||||
play_pref: {
|
||||
textColor: '#ed7b2f',
|
||||
backgroundColor: '#fff3e6',
|
||||
borderColor: '#fed7aa',
|
||||
psychology: '橙色 - 活力、娱乐偏好',
|
||||
},
|
||||
promo_pref: {
|
||||
textColor: '#d4a017',
|
||||
backgroundColor: '#fffbeb',
|
||||
borderColor: '#fef3c7',
|
||||
psychology: '金色 - 价值、优惠敏感度',
|
||||
},
|
||||
social: {
|
||||
textColor: '#764ba2',
|
||||
backgroundColor: '#f3e8ff',
|
||||
borderColor: '#e9d5ff',
|
||||
psychology: '紫色 - 社交、人脉关系',
|
||||
},
|
||||
feedback: {
|
||||
textColor: '#e34d59',
|
||||
backgroundColor: '#ffe6e8',
|
||||
borderColor: '#fecdd3',
|
||||
psychology: '红色 - 警示、关键信息',
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 3. 关系等级配色(4种)
|
||||
// ============================================
|
||||
export const RELATIONSHIP_LEVELS = {
|
||||
excellent: {
|
||||
name: '很好',
|
||||
emoji: '💖',
|
||||
scoreRange: '> 8.5',
|
||||
gradientFrom: '#e91e63',
|
||||
gradientTo: '#f472b6',
|
||||
shadowColor: 'rgba(233,30,99,0.30)',
|
||||
label: '优质客户',
|
||||
},
|
||||
good: {
|
||||
name: '良好',
|
||||
emoji: '🧡',
|
||||
scoreRange: '6-8.5',
|
||||
gradientFrom: '#ea580c',
|
||||
gradientTo: '#fb923c',
|
||||
shadowColor: 'rgba(234,88,12,0.30)',
|
||||
label: '稳定客户',
|
||||
},
|
||||
normal: {
|
||||
name: '一般',
|
||||
emoji: '💛',
|
||||
scoreRange: '3.5-6',
|
||||
gradientFrom: '#eab308',
|
||||
gradientTo: '#fbbf24',
|
||||
shadowColor: 'rgba(234,179,8,0.30)',
|
||||
label: '普通客户',
|
||||
},
|
||||
poor: {
|
||||
name: '待发展',
|
||||
emoji: '💙',
|
||||
scoreRange: '< 3.5',
|
||||
gradientFrom: '#64748b',
|
||||
gradientTo: '#94a3b8',
|
||||
shadowColor: 'rgba(100,116,139,0.30)',
|
||||
label: '需维护客户',
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 4. 置顶/放弃状态
|
||||
// ============================================
|
||||
export const TASK_STATUS_COLORS = {
|
||||
pinned: {
|
||||
name: '置顶',
|
||||
glowColor: '#f59e0b',
|
||||
shadowLight: 'rgba(245, 158, 11, 0.12)',
|
||||
shadowGlow: 'rgba(245, 158, 11, 0.08)',
|
||||
shadowCSS: '0 5rpx 7rpx rgba(245, 158, 11, 0.12), 0 0 0 8rpx rgba(245, 158, 11, 0.08)',
|
||||
},
|
||||
abandoned: {
|
||||
name: '放弃',
|
||||
borderColor: '#d1d5db',
|
||||
textColor: '#9ca3af',
|
||||
opacity: 0.55,
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 5. 助教等级配色(4种 + 星级)
|
||||
// ============================================
|
||||
export const COACH_LEVEL_COLORS = {
|
||||
junior: {
|
||||
name: '初级',
|
||||
textColor: '#0052d9',
|
||||
backgroundColor: '#ecf2fe',
|
||||
borderColor: '#bfdbfe',
|
||||
},
|
||||
middle: {
|
||||
name: '中级',
|
||||
textColor: '#ed7b2f',
|
||||
backgroundColor: '#fff3e6',
|
||||
borderColor: '#fed7aa',
|
||||
},
|
||||
senior: {
|
||||
name: '高级',
|
||||
textColor: '#e91e63',
|
||||
backgroundColor: '#ffe6e8',
|
||||
borderColor: '#fecdd3',
|
||||
},
|
||||
star: {
|
||||
name: '⭐ 星级',
|
||||
textColor: '#fbbf24',
|
||||
backgroundColor: '#fffef0',
|
||||
borderColor: '#fef3c7',
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 6. 星星评分配色
|
||||
// ============================================
|
||||
export const STAR_RATING_COLORS = {
|
||||
low: {
|
||||
scoreRange: '1-2',
|
||||
stars: '0.5-1',
|
||||
color: '#e34d59',
|
||||
label: '低满意度',
|
||||
},
|
||||
mediumLow: {
|
||||
scoreRange: '3-4',
|
||||
stars: '1.5-2',
|
||||
color: '#ed7b2f',
|
||||
label: '一般满意度',
|
||||
},
|
||||
medium: {
|
||||
scoreRange: '5-6',
|
||||
stars: '2.5-3',
|
||||
color: '#eab308',
|
||||
label: '中等满意度',
|
||||
},
|
||||
mediumHigh: {
|
||||
scoreRange: '7-8',
|
||||
stars: '3.5-4',
|
||||
color: '#00a870',
|
||||
label: '高满意度',
|
||||
},
|
||||
high: {
|
||||
scoreRange: '9-10',
|
||||
stars: '4.5-5',
|
||||
color: '#e91e63',
|
||||
label: '非常满意',
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 7. AI 图标随机配色库(6种)
|
||||
// ============================================
|
||||
export const AI_COLOR_SCHEMES = [
|
||||
{
|
||||
name: 'red',
|
||||
from: '#e74c3c',
|
||||
to: '#f39c9c',
|
||||
fromDeep: '#c0392b',
|
||||
toDeep: '#e74c3c',
|
||||
className: 'ai-color-red',
|
||||
},
|
||||
{
|
||||
name: 'orange',
|
||||
from: '#e67e22',
|
||||
to: '#f5c77e',
|
||||
fromDeep: '#ca6c17',
|
||||
toDeep: '#e67e22',
|
||||
className: 'ai-color-orange',
|
||||
},
|
||||
{
|
||||
name: 'yellow',
|
||||
from: '#d4a017',
|
||||
to: '#f7dc6f',
|
||||
fromDeep: '#b8860b',
|
||||
toDeep: '#d4a017',
|
||||
className: 'ai-color-yellow',
|
||||
},
|
||||
{
|
||||
name: 'blue',
|
||||
from: '#2980b9',
|
||||
to: '#7ec8e3',
|
||||
fromDeep: '#1a5276',
|
||||
toDeep: '#2980b9',
|
||||
className: 'ai-color-blue',
|
||||
},
|
||||
{
|
||||
name: 'indigo',
|
||||
from: '#667eea',
|
||||
to: '#a78bfa',
|
||||
fromDeep: '#4a5fc7',
|
||||
toDeep: '#667eea',
|
||||
className: 'ai-color-indigo',
|
||||
},
|
||||
{
|
||||
name: 'purple',
|
||||
from: '#764ba2',
|
||||
to: '#c084fc',
|
||||
fromDeep: '#5b3080',
|
||||
toDeep: '#764ba2',
|
||||
className: 'ai-color-purple',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 8. 全局颜色变量(CSS Variables)
|
||||
// ============================================
|
||||
export const GLOBAL_COLORS = {
|
||||
// 主色系
|
||||
primary: '#0052d9',
|
||||
primaryLight: '#ecf2fe',
|
||||
success: '#00a870',
|
||||
warning: '#ed7b2f',
|
||||
error: '#e34d59',
|
||||
|
||||
// 灰度系(13级)
|
||||
gray1: '#f3f3f3',
|
||||
gray2: '#eeeeee',
|
||||
gray3: '#e7e7e7',
|
||||
gray4: '#dcdcdc',
|
||||
gray5: '#c5c5c5',
|
||||
gray6: '#a6a6a6',
|
||||
gray7: '#8b8b8b',
|
||||
gray8: '#777777',
|
||||
gray9: '#5e5e5e',
|
||||
gray10: '#4b4b4b',
|
||||
gray11: '#393939',
|
||||
gray12: '#2c2c2c',
|
||||
gray13: '#242424',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 工具函数
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 获取随机AI配色
|
||||
*/
|
||||
export function getRandomAiColor() {
|
||||
return AI_COLOR_SCHEMES[Math.floor(Math.random() * AI_COLOR_SCHEMES.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任务类型获取颜色
|
||||
*/
|
||||
export function getTaskTypeColor(taskType: keyof typeof TASK_TYPE_COLORS) {
|
||||
return TASK_TYPE_COLORS[taskType];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据客户标签获取颜色
|
||||
*/
|
||||
export function getCustomerTagColor(tagName: keyof typeof CUSTOMER_TAG_COLORS) {
|
||||
return CUSTOMER_TAG_COLORS[tagName];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关系分数获取等级和颜色
|
||||
*/
|
||||
export function getRelationshipLevel(score: number) {
|
||||
if (score > 8.5) return RELATIONSHIP_LEVELS.excellent;
|
||||
if (score >= 6) return RELATIONSHIP_LEVELS.good;
|
||||
if (score >= 3.5) return RELATIONSHIP_LEVELS.normal;
|
||||
return RELATIONSHIP_LEVELS.poor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据评分获取星星颜色
|
||||
*/
|
||||
export function getStarRatingColor(score: number) {
|
||||
if (score <= 2) return STAR_RATING_COLORS.low;
|
||||
if (score <= 4) return STAR_RATING_COLORS.mediumLow;
|
||||
if (score <= 6) return STAR_RATING_COLORS.medium;
|
||||
if (score <= 8) return STAR_RATING_COLORS.mediumHigh;
|
||||
return STAR_RATING_COLORS.high;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据助教等级获取颜色
|
||||
*/
|
||||
export function getCoachLevelColor(level: keyof typeof COACH_LEVEL_COLORS) {
|
||||
return COACH_LEVEL_COLORS[level];
|
||||
}
|
||||
Reference in New Issue
Block a user