- 替换 41 处原生 select 为 Select 组件封装 - 替换 5 处 window.confirm 为 shadcn AlertDialog - 修复 lesson-plans delete-confirm-dialog 为 AlertDialog - 修复 5 处 Tailwind 任意值 text-[10px] - 修复 graphql-data.ts mutation case 缺少 id 定义 - 修复 use-position-persistence.ts eslint 规则引用
135 lines
3.7 KiB
TypeScript
135 lines
3.7 KiB
TypeScript
/**
|
||
* AI Settings 数据变换工具(ARCHITECTURE.md §11.3 DoD - 纯函数单测)
|
||
*
|
||
* 所有格式化/映射函数均为纯函数,便于 vitest 单测。
|
||
* 关联:ARCHITECTURE.md §11.3 DoD "数据变换/权限判断等纯函数有 vitest 单测"
|
||
*/
|
||
|
||
/** AI Provider 类型标签映射(对齐 CICD:zhipu/openai/gemini/ollama/custom) */
|
||
export const PROVIDER_TYPE_LABEL: Record<string, string> = {
|
||
zhipu: "智谱 AI",
|
||
openai: "OpenAI",
|
||
gemini: "Google Gemini",
|
||
ollama: "Ollama(本地)",
|
||
custom: "自定义",
|
||
};
|
||
|
||
/** AI Provider 类型支持的取值列表 */
|
||
export const PROVIDER_TYPES: readonly string[] = [
|
||
"zhipu",
|
||
"openai",
|
||
"gemini",
|
||
"ollama",
|
||
"custom",
|
||
] as const;
|
||
|
||
/** AI Provider 可见性枚举值列表(@contract-pending,MSW 兜底) */
|
||
export const PROVIDER_VISIBILITY_OPTIONS: readonly string[] = [
|
||
"private",
|
||
"shared",
|
||
"public",
|
||
] as const;
|
||
|
||
/** AI Provider 可见性标签映射 */
|
||
export const PROVIDER_VISIBILITY_LABEL: Record<string, string> = {
|
||
private: "仅自己可见",
|
||
shared: "组织共享",
|
||
public: "全员可见",
|
||
};
|
||
|
||
/**
|
||
* 将 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);
|
||
}
|
||
|
||
/**
|
||
* 将 Provider 可见性代码映射为展示标签。未知值回退为原始值。
|
||
*/
|
||
export function formatProviderVisibility(visibility: string): string {
|
||
return PROVIDER_VISIBILITY_LABEL[visibility] ?? visibility;
|
||
}
|
||
|
||
/**
|
||
* 根据 isActive 返回 Tailwind 徽章语义类名。
|
||
*/
|
||
export function activeToBadgeClass(isActive: boolean): string {
|
||
return isActive
|
||
? "bg-success/10 text-success"
|
||
: "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",
|
||
});
|
||
}
|