refactor(mobile): extract types, static data, utils, styles from App.vue

Pure moves only — zero behavior change, template untouched:
- types.ts (447), utils.ts (51), data/{auth,navigation,practice,mentor,
  home,supervisor,icons}.ts, styles/app.css (4690, still global non-scoped
  via <style src>)
- App.vue 9006 → 3559 lines; 98 moved declarations verbatim except
  export/import syntax
- Built CSS hash identical to pre-refactor (byte-equal styles); reactive
  state, computed, watchers, lifecycle and templates all stay in App.vue
- Component-level split deferred to phase two after 2026-07-10 Alpha
This commit is contained in:
2026-07-08 21:07:45 +08:00
parent 15cd789da6
commit e4070c8b8c
11 changed files with 5649 additions and 5528 deletions
+81 -5528
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
export const tokenKey = 'aihr_mobile_access_token';
export const clientIdKey = 'aihr_mobile_client_id';
export const phoneKey = 'aihr_mobile_phone';
export const phonePattern = /^1[3-9]\d{9}$/;
+127
View File
@@ -0,0 +1,127 @@
import type { RoleHome, RoleKey, TabItem } from '../types';
export const employeeTabs: TabItem[] = [
{ label: '今日', icon: 'home', active: true },
{ label: '练', icon: 'graduation', active: false },
{ label: '问', icon: 'book', active: false },
{ label: '我', icon: 'user', active: false }
];
export const fallbackHomes: Record<RoleKey, RoleHome> = {
user: {
role: 'user',
roleLabel: '员工端',
avatarTone: 'avatar-woman',
greeting: '早上好,王敏',
location: '星河湾一期 · 客服管家',
toolTitle: '学习与工具',
recommend: '本周推荐:催费沟通专项',
primary: {
badge: '重点训练',
title: '投诉接待三角色对练',
desc: '3分钟语音练习 · 完成后自动评分',
progressText: '1/1',
progress: 100,
cta: '开始训练',
icon: 'headset'
},
review: {
title: '岗前SOP复习',
desc: '今日需完成 · 住宅投诉处理流程'
},
tools: [
{ icon: 'mic', title: '每日一练', desc: '每日3分钟\n提升服务能力', tone: 'teal' },
{ icon: 'book-search', title: '查SOP', desc: '随时查找\n标准流程', tone: 'blue' },
{ icon: 'folder', title: '案例学习', desc: '优秀案例\n对标提升', tone: 'orange' },
{ icon: 'pen', title: '提交素材', desc: '记录工作场景\n供审核入库', tone: 'purple' }
],
metrics: [
{ icon: 'bars', title: '完成', value: '6/10', desc: '今日任务进度', tone: 'teal', progress: 60 },
{ icon: 'target', title: '能力', value: '82', desc: '综合能力分', tone: 'blue', progress: 82 },
{ icon: 'clipboard', title: '待复盘', value: '2', desc: '待复盘任务', tone: 'orange', progress: 48 }
],
tabs: [...employeeTabs]
},
candidate: {
role: 'candidate',
roleLabel: '候选人端',
avatarTone: 'avatar-candidate',
greeting: '早上好,李娜',
location: '星河湾一期 · 客服管家候选人',
toolTitle: '面试准备',
recommend: '本周推荐:投诉接待基础',
primary: {
badge: '面试任务',
title: 'AI结构化面试',
desc: '8分钟问答 · 完成后生成面试记录',
progressText: '0/1',
progress: 22,
cta: '开始面试',
icon: 'briefcase'
},
review: {
title: '岗位SOP预习',
desc: '今日需完成 · 客服接待基础流程'
},
tools: [
{ icon: 'mic', title: '面试练习', desc: '模拟问答\n熟悉岗位', tone: 'teal' },
{ icon: 'book-search', title: '查岗位SOP', desc: '快速了解\n标准流程', tone: 'blue' },
{ icon: 'folder', title: '案例学习', desc: '典型场景\n提前预习', tone: 'orange' },
{ icon: 'upload', title: '补充资料', desc: '提交证书\n与经历', tone: 'purple' }
],
metrics: [
{ icon: 'bars', title: '进度', value: '2/4', desc: '候选任务', tone: 'teal', progress: 50 },
{ icon: 'target', title: '匹配度', value: '86', desc: '岗位画像分', tone: 'blue', progress: 86 },
{ icon: 'clipboard', title: '待补充', value: '1', desc: '资料项', tone: 'orange', progress: 35 }
],
tabs: [
{ label: '首页', icon: 'home', active: true },
{ label: '面试', icon: 'briefcase', active: false },
{ label: '知识', icon: 'book', active: false },
{ label: '我的', icon: 'user', active: false }
]
},
supervisor: {
role: 'supervisor',
roleLabel: '主管端',
avatarTone: 'avatar-man',
greeting: '早上好,李主管',
location: '星河湾一期 · 项目主管',
toolTitle: '快捷操作',
tools: [
{ icon: 'presentation', title: '指派训练', desc: '为员工分配训练任务', tone: 'teal' },
{ icon: 'target', title: '团队短板', desc: '查看短板分布\n与改进建议', tone: 'blue' },
{ icon: 'upload', title: '提交案例素材', desc: '上传案例素材\n供审核与沉淀', tone: 'orange' },
{ icon: 'book-search', title: '查SOP', desc: '快速搜索制度与\n标准流程', tone: 'purple' }
],
tabs: [
{ label: '首页', icon: 'home', active: true },
{ label: '团队', icon: 'users', active: false },
{ label: '案例', icon: 'folder', active: false },
{ label: '我的', icon: 'user', active: false }
],
team: {
title: '客服团队训练进度',
progressText: '12人已完成 · 3人待跟进',
progress: 80,
completionLabel: '完训率',
detailCta: '查看详情',
stats: [
{ icon: 'users', title: '完训率', value: '80%', tone: 'teal' },
{ icon: 'bars', title: '平均能力', value: '78', tone: 'teal' },
{ icon: 'warning', title: '高频短板', value: '3', tone: 'orange' }
]
},
todos: [
{ icon: 'presentation', title: '待复盘对练', desc: '查看对练记录,辅导与点评', count: 3, tone: 'teal' },
{ icon: 'folder', title: '待提交本周案例', desc: '提交优质案例素材,供审核入库', count: 1, tone: 'orange' },
{ icon: 'clipboard', title: '待指派专项', desc: '为低分或薄弱项员工指派训练专项', count: 2, tone: 'blue' }
],
suggestion: {
icon: 'bulb',
title: '建议:给催费沟通低分员工指派专项',
desc: '基于团队短板分析,建议开展「催费沟通技巧」专项训练',
tone: 'amber'
}
}
};
+97
View File
@@ -0,0 +1,97 @@
import type { IconName } from '../types';
export const iconPaths: Record<IconName, Array<[string, Record<string, string | number>?]>> = {
bars: [
['rect', { x: 5, y: 14, width: 3, height: 5, rx: 1 }],
['rect', { x: 11, y: 10, width: 3, height: 9, rx: 1 }],
['rect', { x: 17, y: 5, width: 3, height: 14, rx: 1 }]
],
book: [
['path', { d: 'M4 6.5A3.5 3.5 0 0 1 7.5 3H20v16H7.5A3.5 3.5 0 0 0 4 22V6.5Z' }],
['path', { d: 'M4 19a3.5 3.5 0 0 1 3.5-3.5H20' }]
],
'book-search': [
['path', { d: 'M4 6.5A3.5 3.5 0 0 1 7.5 3H18v10' }],
['path', { d: 'M4 19a3.5 3.5 0 0 1 3.5-3.5H13' }],
['circle', { cx: 17, cy: 17, r: 3 }],
['path', { d: 'm19.5 19.5 2 2' }]
],
briefcase: [
['path', { d: 'M9 6V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1' }],
['rect', { x: 4, y: 6, width: 16, height: 13, rx: 2 }],
['path', { d: 'M4 11h16M10 11v2h4v-2' }]
],
bulb: [
['path', { d: 'M9 18h6M10 22h4M8 14a6 6 0 1 1 8 0c-.8.7-1.2 1.6-1.3 2.5H9.3C9.2 15.6 8.8 14.7 8 14Z' }]
],
'chevron-down': [['path', { d: 'm6 9 6 6 6-6' }]],
'chevron-right': [['path', { d: 'm9 5 7 7-7 7' }]],
clipboard: [
['rect', { x: 6, y: 5, width: 12, height: 16, rx: 2 }],
['path', { d: 'M9 5a3 3 0 0 1 6 0M9 11h6M9 15h6' }]
],
document: [
['path', { d: 'M7 3h7l4 4v14H7V3Z' }],
['path', { d: 'M14 3v5h5M10 12h5M10 16h5' }]
],
folder: [
['path', { d: 'M3 7h7l2 2h9v9.5A2.5 2.5 0 0 1 18.5 21h-13A2.5 2.5 0 0 1 3 18.5V7Z' }],
['path', { d: 'M3 9.5h18' }]
],
graduation: [
['path', { d: 'm3 8 9-4 9 4-9 4-9-4Z' }],
['path', { d: 'M7 10.5V15c2.5 2 7.5 2 10 0v-4.5M21 8v5' }]
],
headset: [
['path', { d: 'M4 14a8 8 0 0 1 16 0' }],
['rect', { x: 3, y: 13, width: 4, height: 7, rx: 2 }],
['rect', { x: 17, y: 13, width: 4, height: 7, rx: 2 }],
['path', { d: 'M19 20c0 2-2 3-5 3h-2M13 23v-2h3' }]
],
home: [
['path', { d: 'm3 11 9-8 9 8' }],
['path', { d: 'M5 10.5V21h5v-6h4v6h5V10.5' }]
],
'map-pin': [
['path', { d: 'M12 22s7-6.1 7-12a7 7 0 0 0-14 0c0 5.9 7 12 7 12Z' }],
['circle', { cx: 12, cy: 10, r: 2.5 }]
],
megaphone: [
['path', { d: 'M4 12h4l10-5v10L8 12H4Z' }],
['path', { d: 'M8 12v6a2 2 0 0 0 2 2h1' }]
],
mic: [
['rect', { x: 9, y: 3, width: 6, height: 11, rx: 3 }],
['path', { d: 'M5 11a7 7 0 0 0 14 0M12 18v4M8 22h8' }]
],
pen: [
['path', { d: 'M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4Z' }],
['path', { d: 'm13.5 6.5 4 4' }]
],
presentation: [
['rect', { x: 4, y: 4, width: 16, height: 12, rx: 2 }],
['path', { d: 'M8 21h8M12 16v5M9 11l2 2 4-5' }]
],
target: [
['circle', { cx: 12, cy: 12, r: 8 }],
['circle', { cx: 12, cy: 12, r: 3 }],
['path', { d: 'm16 8 4-4M20 4v4M20 4h-4' }]
],
upload: [
['path', { d: 'M12 16V4M7 9l5-5 5 5' }],
['path', { d: 'M5 16v4h14v-4' }]
],
user: [
['circle', { cx: 12, cy: 8, r: 4 }],
['path', { d: 'M4 21a8 8 0 0 1 16 0' }]
],
users: [
['circle', { cx: 9, cy: 8, r: 3 }],
['circle', { cx: 17, cy: 10, r: 3 }],
['path', { d: 'M3 21a6 6 0 0 1 12 0M13 21a5 5 0 0 1 8 0' }]
],
warning: [
['path', { d: 'M12 3 22 20H2L12 3Z' }],
['path', { d: 'M12 9v5M12 17h.01' }]
]
};
+18
View File
@@ -0,0 +1,18 @@
import type { MentorMapActionKey } from '../types';
import { mapIconPath } from '../utils';
export const mentorImageSrc = `${import.meta.env.BASE_URL}mentor.png`;
export const mentorQuickChips = [
{ label: '催费话术', query: '物业费催缴怎么开口' },
{ label: '投诉应对', query: '业主投诉时第一句话怎么说' },
{ label: '收费标准', query: '物业费收费标准怎么向业主解释' },
{ label: '上门带什么', query: '生活顾问上门处理问题需要带什么' }
];
export const mentorMapActions: Array<{ key: MentorMapActionKey; label: string; desc: string; tone: 'blue' | 'green' | 'orange' | 'red'; icon: string }> = [
{ key: 'ask', label: '问', desc: '查SOP / 求助话术', tone: 'blue', icon: mapIconPath('ask') },
{ key: 'practice', label: '练', desc: '场景模拟 / 错题重练', tone: 'green', icon: mapIconPath('practice') },
{ key: 'learn', label: '学', desc: '岗前课程 / 每日小训', tone: 'orange', icon: mapIconPath('learn') },
{ key: 'submit', label: '报', desc: '上报案例 / 拍照记录', tone: 'red', icon: mapIconPath('report') }
];
+49
View File
@@ -0,0 +1,49 @@
import type { RoleKey, SupervisorPanelKey, UserPanelKey } from '../types';
export const aliases: Record<string, RoleKey> = {
h5: 'user',
user: 'user',
employee: 'user',
candidate: 'candidate',
supervisor: 'supervisor',
admin: 'supervisor',
manager: 'supervisor'
};
export const rolePaths: Record<RoleKey, string> = {
user: '/h5/user',
candidate: '/h5/candidate',
supervisor: '/h5/supervisor'
};
export const userPanelSegments: Record<Exclude<UserPanelKey, ''>, string> = {
tasks: 'training',
practice: 'practice',
sop: 'knowledge',
cases: 'cases',
submit: 'submit',
profile: 'profile',
history: 'history',
recommend: 'recommend'
};
export const candidatePanelSegments: Record<Exclude<UserPanelKey, ''>, string> = {
tasks: 'interview',
practice: 'interview',
sop: 'sop',
cases: 'cases',
submit: 'materials',
profile: 'profile',
history: 'history',
recommend: 'recommend'
};
export const supervisorPanelSegments: Record<Exclude<SupervisorPanelKey, ''>, string> = {
team: 'team',
reviews: 'reviews',
assign: 'assign',
weakness: 'weakness',
case: 'cases',
sop: 'sop',
profile: 'profile'
};
+72
View File
@@ -0,0 +1,72 @@
import type { PracticeRecentItem, PracticeScenario, PracticeStyleKey } from '../types';
export const practiceScenarios: PracticeScenario[] = [
{
id: 'complaint-water',
title: '投诉接待',
badge: '高频',
desc: '业主投诉服务或设施问题的接待与处置',
progressText: '2/3',
progress: '66%'
},
{
id: 'fee-collection',
title: '催费沟通',
badge: '高频',
desc: '逾期费用提醒与关系维护',
progressText: '1/3',
progress: '33%'
},
{
id: 'repair-elevator-delay',
title: '电梯故障',
badge: '应急',
desc: '电梯故障期间的安抚、解释与反馈',
progressText: '0/3',
progress: '0%'
},
{
id: 'fee-parking',
title: '停车费催缴',
badge: '高频',
desc: '停车费到期提醒、异议解释与关系维护',
progressText: '1/3',
progress: '33%'
},
{
id: 'emergency-elder-fall',
title: '突发求助',
badge: '基础',
desc: '老人摔倒等突发情况的响应与协同',
progressText: '0/3',
progress: '0%'
}
];
export const practiceRecentFallback: PracticeRecentItem[] = [
{
scenarioId: 'complaint-water',
title: '投诉接待 · 电梯故障投诉',
time: '今天 09:18',
score: '4.0'
},
{
scenarioId: 'fee-collection',
title: '催费沟通 · 逾期费用提醒',
time: '昨天 14:32',
score: '3.5'
}
];
export const practiceTotalRounds = 3;
export const waveBars = [12, 20, 28, 16, 34, 24, 18, 32, 22, 30, 14, 26, 18, 35, 20, 12];
export const practiceStyleOptions: { key: PracticeStyleKey; label: string }[] = [
{ key: 'serious', label: '再严肃' },
{ key: 'gentle', label: '再温柔' },
{ key: 'detailed', label: '更细致' },
{ key: 'professional', label: '更专业' }
];
export const todayPracticeTarget = 1;
+10
View File
@@ -0,0 +1,10 @@
import type { SupervisorPanelKey } from '../types';
export const supervisorHeatmapItems: Array<{ title: string; value: string; desc: string; tone: string; target: SupervisorPanelKey }> = [
{ title: '催费开场白', value: '78%', desc: '中', tone: 'heat-low', target: 'assign' },
{ title: '收费异议处理', value: '62%', desc: '低', tone: 'heat-high', target: 'assign' },
{ title: '投诉安抚共情', value: '67%', desc: '低', tone: 'heat-mid', target: 'reviews' },
{ title: '费用减免说明', value: '71%', desc: '中', tone: 'heat-mid', target: 'sop' },
{ title: '法规风险告知', value: '59%', desc: '低', tone: 'heat-high', target: 'sop' },
{ title: '催费跟进话术', value: '81%', desc: '高', tone: 'heat-low', target: 'assign' }
];
File diff suppressed because it is too large Load Diff
+447
View File
@@ -0,0 +1,447 @@
export type RoleKey = 'user' | 'candidate' | 'supervisor';
export type Tone = 'teal' | 'blue' | 'orange' | 'purple' | 'amber';
export type UserPanelKey = '' | 'tasks' | 'practice' | 'sop' | 'cases' | 'submit' | 'profile' | 'history' | 'recommend';
export type SupervisorPanelKey = '' | 'team' | 'reviews' | 'assign' | 'weakness' | 'case' | 'sop' | 'profile';
export type FlowTarget = UserPanelKey | SupervisorPanelKey;
export type MentorState = 'idle' | 'listening' | 'thinking' | 'speaking';
export type MentorMapActionKey = 'ask' | 'practice' | 'learn' | 'submit';
export type IconName =
| 'bars'
| 'book'
| 'book-search'
| 'briefcase'
| 'bulb'
| 'chevron-down'
| 'chevron-right'
| 'clipboard'
| 'document'
| 'folder'
| 'graduation'
| 'headset'
| 'home'
| 'map-pin'
| 'megaphone'
| 'mic'
| 'pen'
| 'presentation'
| 'target'
| 'upload'
| 'user'
| 'users'
| 'warning';
export interface ToolItem {
icon: IconName;
title: string;
desc: string;
tone: Tone;
}
export interface MetricItem {
icon: IconName;
title: string;
value: string;
desc: string;
tone: Tone;
progress: number;
}
export interface PrimaryTask {
badge: string;
title: string;
desc: string;
progressText: string;
progress: number;
cta: string;
icon: IconName;
}
export interface ReviewTask {
title: string;
desc: string;
}
export interface TabItem {
label: string;
icon: IconName;
active: boolean;
}
export interface FlowStep {
phase: string;
title: string;
desc: string;
target: FlowTarget;
}
export interface TeamStat {
icon: IconName;
title: string;
value: string;
tone: Tone;
}
export interface TeamOverview {
title: string;
progressText: string;
progress: number;
completionLabel: string;
detailCta: string;
stats: TeamStat[];
}
export interface TodoItem extends ToolItem {
count: number;
}
export interface SuggestionItem {
icon: IconName;
title: string;
desc: string;
tone: Tone;
}
export interface RoleHome {
role: RoleKey;
roleLabel: string;
avatarTone: string;
greeting: string;
location: string;
toolTitle?: string;
recommend?: string;
primary?: PrimaryTask;
review?: ReviewTask;
tools: ToolItem[];
metrics?: MetricItem[];
tabs: TabItem[];
team?: TeamOverview;
todos?: TodoItem[];
suggestion?: SuggestionItem;
}
export type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
export type InterviewStatus = PracticeStatus;
export type PracticeRole = 'customer' | 'trainee' | 'coach';
export type PracticeStyleKey = 'serious' | 'gentle' | 'detailed' | 'professional';
export interface PracticeTurn {
role: PracticeRole;
label: string;
text: string;
roundIndex?: number;
styleNote?: string;
}
export interface PracticeStartResponse {
sessionId: string;
customerText: string;
emotion?: number;
trust?: number;
}
export interface PracticePrepCardResponse {
keyPoints: string[];
redLines: string[];
scripts: string[];
promptVersion: string;
}
export interface PracticeTurnResponse {
customerText: string;
coachHint: string;
message?: string;
emotion?: number;
trust?: number;
redFlag?: boolean;
roundIndex: number;
finished: boolean;
}
export interface PracticeFinishResponse {
totalScore: number;
mentorRewrite: string;
aiComment: string;
}
export interface PracticeRecord {
id?: number;
sessionId?: string;
time: string;
trainee: string;
scene: string;
score: number;
status: string;
summary: string;
}
export interface PracticeScenario {
id: string;
title: string;
badge: string;
desc: string;
progressText: string;
progress: string;
}
export interface PracticeRecentItem {
scenarioId: string;
title: string;
time: string;
score: string;
record?: PracticeRecord;
}
export interface PracticeDialogue {
role: PracticeRole;
label: string;
text: string;
audioUrl?: string;
}
export interface PracticeAnnotation {
turnIndex: number;
sourceText: string;
issue: string;
rewrite: string;
sopPoint: string;
}
export interface PracticeReviewDetail extends PracticeRecord {
mentorRewrite: string;
aiComment: string;
reviewAdvice?: string;
incentivePoint?: number;
scoreItems: CompetencyDimension[];
dialogue: PracticeDialogue[];
annotations: PracticeAnnotation[];
}
export interface InterviewQuestion {
questionId: string;
seq: number;
questionText: string;
point: string;
scenario: string;
sampleAnswer: string;
}
export interface InterviewStartResponse {
sessionId: string;
candidateId: string;
candidateName: string;
positionCode: string;
questions: InterviewQuestion[];
source: string;
}
export interface InterviewRecord {
time: string;
candidate: string;
position: string;
score: number;
result: string;
summary: string;
}
export interface InterviewFinishResponse {
totalScore: number;
suggestion: string;
result: string;
aiSummary: string;
evidence: string;
dimensions: CompetencyDimension[];
records: InterviewRecord[];
source: string;
}
export interface CompetencyDimension {
label: string;
value: number | string;
note: string;
}
export interface CompetencyProfile {
score: number;
completed: number;
pendingReview: number;
trainingMinutes?: number;
contribution?: number;
assessmentScore?: number;
aiLevel?: string;
dimensions: CompetencyDimension[];
growthPath?: Array<{ level: string; progress: number; status: string; evidence: string }>;
}
export interface PracticeAlert {
type: string;
title: string;
desc: string;
severity: string;
extPartyId: string;
}
export interface PracticeAssignment {
id?: number;
extPartyId: string;
traineeName: string;
scenarioId: string;
scenarioName: string;
source: string;
reason: string;
status: string;
createTime: string;
question?: string;
referenceAnswer?: string;
userAnswer?: string;
feedback?: string;
correct?: boolean;
}
export interface DailyDrillAnswerResponse {
id: number;
status: string;
correct: boolean;
score: number;
feedback: string;
referenceAnswer: string;
}
export interface KnowledgeDoc {
title: string;
desc: string;
hit: string;
type: string;
}
export interface KnowledgeSnippet {
title: string;
text: string;
fragmentId?: number;
}
export interface KnowledgeRecord {
title: string;
category: string;
version: string;
status: string;
owner: string;
summary: string;
}
export interface KnowledgeSearchResponse {
queryText: string;
category: string;
answer: string;
reference: string;
docs: KnowledgeDoc[];
snippets: KnowledgeSnippet[];
training: string[];
records: KnowledgeRecord[];
keyPoints?: string[];
cautions?: string[];
}
export type AnswerFeedbackVerdict = 'up' | 'down';
export interface SummaryCardStep {
label: string;
desc: string;
}
export interface SummaryCardObjection {
concern: string;
response: string;
}
export interface SummaryCardResponse {
title: string;
steps: SummaryCardStep[];
objections: SummaryCardObjection[];
scripts: string[];
reminders: string[];
}
export interface CaseTag {
text: string;
type: string;
}
export interface CaseSummaryItem {
label: string;
text: string;
}
export interface CaseUploadResponse {
caseId: string;
fileName: string;
transcript: string;
tags: CaseTag[];
source: string;
}
export interface CaseOrganizeResponse {
caseId: string;
summary: CaseSummaryItem[];
tags: CaseTag[];
aiSummary: string;
source: string;
}
export interface CaseRecord {
title: string;
type: string;
status: string;
time: string;
owner: string;
summary: string;
}
export interface CaseCurateResponse {
selectedCaseIds: string[];
records: CaseRecord[];
sampleHint: string;
}
export interface MaterialResult {
upload: CaseUploadResponse;
organize: CaseOrganizeResponse;
curate: CaseCurateResponse;
}
export interface CandidateMaterialResponse {
id: number | string;
ossId: number | string;
fileName: string;
materialType: string;
fileSize: number;
status: string;
time: string;
}
export interface MobileRoute {
role: RoleKey;
page: string;
detail: string;
}
export interface ApiPayload<T> {
code?: number;
msg?: string;
data?: T;
}
export interface LoginResult {
access_token: string;
expire_in: number;
client_id: string;
}
+51
View File
@@ -0,0 +1,51 @@
import type { MobileRoute, PracticeRole, RoleKey, SupervisorPanelKey, UserPanelKey } from './types';
import { aliases, candidatePanelSegments, rolePaths, supervisorPanelSegments, userPanelSegments } from './data/navigation';
export const pathParts = () => window.location.pathname.split('/').filter(Boolean);
export const readRole = (): RoleKey => {
const parts = pathParts();
const part = parts[0] === 'h5' ? parts[1] : parts.find((item) => aliases[item]);
return aliases[part || 'user'] || 'user';
};
export const readRoute = (): MobileRoute => {
const parts = pathParts();
const offset = parts[0] === 'h5' ? 2 : 1;
return { role: readRole(), page: parts[offset] || '', detail: parts[offset + 1] || '' };
};
export const clampPercent = (value: number) => `${Math.max(0, Math.min(value, 100))}%`;
export const readStoredCredential = (key: string) => {
const value = localStorage.getItem(key)?.trim() || '';
return value === 'undefined' || value === 'null' ? '' : value;
};
export const practiceAvatarSrc = (role: PracticeRole) => `${import.meta.env.BASE_URL}practice/${role === 'customer' ? 'customer' : role === 'trainee' ? 'trainee' : 'coach'}-avatar.svg`;
export const practiceTurnDuration = (role: PracticeRole, index: number) => (role === 'coach' ? '00:15' : role === 'trainee' ? '已发送' : index > 0 ? '00:12' : '00:18');
export const formatPracticeScore = (score: number | string) => {
const value = Number(score);
return Number.isFinite(value) ? value.toFixed(1) : String(score || '4.0');
};
export const scoreStars = (score: number | string) => {
const value = Number(score);
const filled = Number.isFinite(value) ? Math.max(0, Math.min(5, Math.round(value))) : 4;
return `${'★ '.repeat(filled)}${'☆ '.repeat(5 - filled)}`.trim();
};
export const mapIconPath = (name: string) => `${import.meta.env.BASE_URL}map-icons/${name}.png`;
export const userPanelPath = (panel: UserPanelKey) =>
panel ? `${rolePaths.user}/${userPanelSegments[panel]}` : rolePaths.user;
export const candidatePanelPath = (panel: UserPanelKey) =>
panel ? `${rolePaths.candidate}/${candidatePanelSegments[panel]}` : rolePaths.candidate;
export const supervisorPanelPath = (panel: SupervisorPanelKey) =>
panel ? `${rolePaths.supervisor}/${supervisorPanelSegments[panel]}` : rolePaths.supervisor;
export const ringStyle = (value: number) => ({ '--value': value } as Record<string, string | number>);