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
+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>);