feat(portal-shell): 管理域 §9.4 B5 全量迁移(24 + 21 补充批次共 44 页 + 42 features)
按 ARCHITECTURE.md §9.4 规划口径 + admin-NeedTodo.md §四补充批次完成管理域全量页面迁移: 【§9.4 规划 24 页(B5)】 - users(2) + roles(1) + permissions(1) + audit-logs(4) + invitation-codes(1) - school(6: redirect/schools/classes/departments/academic-year/grades) - announcements(1) + files(1) + ai-settings(1) + system(1) + viewports(1) - students(1) + teachers(1) + organization(1) + plugins(1, config-service) - 仪表盘已存在(/shell/admin/page.tsx) 【§四补充批次 21 页】 - course-plans(4) + elective(4) + questions(1) + lesson-plans(2) + error-book(1) - scheduling(3: auto/changes/rules) + attendance(1) + curriculum-map(1) - announcements 详情/编辑(2) + roles/[id] 详情(1) + users/import(1) 【实现要点】 - 全部使用 ListPageShell + loading/error/empty 三态规范(§11.3 DoD) - 走 lib/api hooks;未就绪契约走 MSW + @contract-pending 注释(§11.4) - 文案走 useTranslations(zh-CN + en 两份同步更新) - 42 个 features/<domain>/transformations.ts 纯函数 + 配套 vitest 单测 - catch 块统一 notify.error;无空 catch;lint:tokens 通过 - 路由全部登记到 route-permissions.ts(39 EXACT + 8 PREFIX) 【验收】 - tsc --noEmit: 0 errors - ESLint src: 0 errors (4 generated-files warnings, pre-existing) - lint:tokens: 0 errors - vitest: 1639/1639 passed (含 23 admin 测试文件 671 用例) - check:routes: PASS (143 routes, 4 ghost entries pre-existing) - check:pages: PASS (146 pages) - check:codegen: PASS - arch:scan: 24 modules, 8262 symbols 关联:ARCHITECTURE.md §9.4 / §10 P5 / §11.3 DoD / §11.6
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* AI Settings 数据变换工具(ARCHITECTURE.md §11.3 DoD - 纯函数单测)
|
||||
*
|
||||
* 所有格式化/映射函数均为纯函数,便于 vitest 单测。
|
||||
* 关联:ARCHITECTURE.md §11.3 DoD "数据变换/权限判断等纯函数有 vitest 单测"
|
||||
*/
|
||||
|
||||
/** AI Provider 类型标签映射 */
|
||||
export const PROVIDER_TYPE_LABEL: Record<string, string> = {
|
||||
openai: "OpenAI",
|
||||
anthropic: "Anthropic",
|
||||
azure: "Azure OpenAI",
|
||||
local: "本地模型",
|
||||
};
|
||||
|
||||
/** AI Provider 类型支持的取值列表 */
|
||||
export const PROVIDER_TYPES: readonly string[] = [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"azure",
|
||||
"local",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 将 Provider 类型代码映射为展示标签。未知值回退为原始值。
|
||||
*/
|
||||
export function formatProviderType(type: string): string {
|
||||
return PROVIDER_TYPE_LABEL[type] ?? type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为受支持的 Provider 类型。
|
||||
*/
|
||||
export function isValidProviderType(type: string): boolean {
|
||||
return PROVIDER_TYPES.includes(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 isActive 返回 Tailwind 徽章语义类名。
|
||||
*/
|
||||
export function activeToBadgeClass(isActive: boolean): string {
|
||||
return isActive
|
||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
||||
: "bg-muted text-muted-foreground";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 isActive 返回展示标签。
|
||||
*/
|
||||
export function formatActiveLabel(isActive: boolean): string {
|
||||
return isActive ? "active" : "inactive";
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 AI 用量费用(分)为元展示字符串。
|
||||
* 输入无效返回 "¥0.00"。
|
||||
*/
|
||||
export function formatCostCents(costCents: number): string {
|
||||
if (!Number.isFinite(costCents) || costCents < 0) return "¥0.00";
|
||||
const yuan = costCents / 100;
|
||||
return `¥${yuan.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字为千分位展示字符串。
|
||||
* 输入无效返回 "0"。
|
||||
*/
|
||||
export function formatNumber(value: number): string {
|
||||
if (!Number.isFinite(value) || value < 0) return "0";
|
||||
return value.toLocaleString("zh-CN");
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断 API Key 用于列表展示(仅保留前 4 + 后 4 字符,中间以 ... 占位)。
|
||||
* 输入为空返回占位符 "--"。
|
||||
*/
|
||||
export function maskApiKey(apiKey: string | null | undefined): string {
|
||||
if (!apiKey || apiKey.trim().length === 0) return "--";
|
||||
const text = apiKey.trim();
|
||||
if (text.length <= 8) return text;
|
||||
return `${text.slice(0, 4)}...${text.slice(-4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断 BaseUrl 用于列表展示(超过 maxLen 字符时截断并加省略号)。
|
||||
* maxLen 默认 40。
|
||||
*/
|
||||
export function truncateBaseUrl(
|
||||
url: string | null | undefined,
|
||||
maxLen = 40,
|
||||
): string {
|
||||
if (!url) return "--";
|
||||
const text = url.trim();
|
||||
if (text.length <= maxLen) return text;
|
||||
return `${text.slice(0, maxLen)}...`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 ISO 日期字符串为本地化展示(zh-CN,仅年月日)。
|
||||
* 输入无效时返回占位符。
|
||||
*/
|
||||
export function formatAiDate(isoDate: string | null | undefined): string {
|
||||
if (!isoDate) return "--";
|
||||
const d = new Date(isoDate);
|
||||
if (Number.isNaN(d.getTime())) return "--";
|
||||
return d.toLocaleDateString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user