/** * Questions 数据变换工具(ARCHITECTURE.md §11.3 DoD - 纯函数单测) * * 所有格式化/映射函数均为纯函数,便于 vitest 单测。 * 关联:ARCHITECTURE.md §11.3 DoD "数据变换/权限判断等纯函数有 vitest 单测" */ import type { Question, QuestionListItem } from "@/lib/api"; /** 题型中文标签映射(对齐 schema Question.type 字符串语义) */ export const QUESTION_TYPE_LABEL: Record = { single_choice: "单选题", multiple_choice: "多选题", fill_blank: "填空题", short_answer: "简答题", essay: "论述题", true_false: "判断题", }; /** 题目状态中文标签映射 */ export const QUESTION_STATUS_LABEL: Record = { DRAFT: "草稿", PUBLISHED: "已发布", ARCHIVED: "已归档", }; /** * 将题型映射为中文标签。未知题型回退为原始值。 */ export function formatQuestionType(type: string): string { return QUESTION_TYPE_LABEL[type] ?? type; } /** * 将题目状态映射为中文标签。未知状态回退为原始值。 */ export function formatQuestionStatus(status: string): string { return QUESTION_STATUS_LABEL[status] ?? status; } /** * 格式化难度。 * * schema Question.difficulty 是 Float,但业务列表 mock 数据常用字符串枚举 * (easy/medium/hard)。本函数同时支持两种输入: * - 字符串枚举:直接映射("easy" → "简单") * - 数值:0~0.4 → 简单,0.4~0.7 → 中等,0.7~1.0 → 困难 * - 其他:回退原始字符串 */ export function formatDifficulty(difficulty: string | number): string { if (typeof difficulty === "number") { if (!Number.isFinite(difficulty)) return "--"; if (difficulty <= 0.4) return "简单"; if (difficulty <= 0.7) return "中等"; return "困难"; } switch (difficulty) { case "easy": return "简单"; case "medium": return "中等"; case "hard": return "困难"; default: return difficulty; } } /** * 格式化 ISO 日期字符串为本地化展示(zh-CN,含年月日时分)。 * 输入无效时返回占位符。 */ export function formatQuestionDate(isoDate: string | null | undefined): string { if (!isoDate) return "--"; const d = new Date(isoDate); if (Number.isNaN(d.getTime())) return "--"; return d.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } /** * 截断题干文本用于列表展示。 * - 超过 maxLen 字符时截断并加省略号 * - 移除换行符(列表单行展示) * - maxLen 默认 60 */ export function truncateContent(content: string, maxLen = 60): string { const text = content.replace(/\s+/g, " ").trim(); if (text.length <= maxLen) return text; return `${text.slice(0, maxLen)}...`; } /** * 从题目详情中提取列表项视图模型(裁剪字段)。 * * 注:Question 详情无 subjectId/textbookId 字段(schema 未提供), * 列表项的这两个字段由 MSW mock 扩展,详情→列表裁剪时置为 null。 */ export function toQuestionListItem(question: Question): QuestionListItem { return { id: question.id, type: question.type, content: question.content, difficulty: String(question.difficulty), status: question.status, source: question.source, knowledgePointId: question.knowledgePointId, subjectId: null, textbookId: null, createdAt: question.createdAt, }; } /** * 根据题型返回 Tailwind 徽章语义类名。 */ export function questionTypeToBadgeClass(type: string): string { switch (type) { case "single_choice": case "multiple_choice": return "bg-blue-500/10 text-blue-600 dark:text-blue-400"; case "fill_blank": return "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"; case "short_answer": case "essay": return "bg-amber-500/10 text-amber-600 dark:text-amber-400"; case "true_false": return "bg-purple-500/10 text-purple-600 dark:text-purple-400"; default: return "bg-muted text-muted-foreground"; } } /** * 根据难度返回 Tailwind 文本语义类名。 * 支持字符串枚举与数值输入。 */ export function difficultyToColorClass(difficulty: string | number): string { const label = formatDifficulty(difficulty); switch (label) { case "简单": return "text-emerald-600"; case "中等": return "text-amber-600"; case "困难": return "text-destructive"; default: return "text-muted-foreground"; } } /** * 根据题目状态返回 Tailwind 徽章语义类名。 */ export function questionStatusToBadgeClass(status: string): string { switch (status) { case "DRAFT": return "bg-muted text-muted-foreground"; case "PUBLISHED": return "bg-primary/10 text-primary"; case "ARCHIVED": return "bg-muted text-muted-foreground"; default: return "bg-muted text-muted-foreground"; } } /** * 判断题目是否处于可编辑状态(DRAFT)。 */ export function isQuestionEditable(status: string): boolean { return status === "DRAFT"; } /** * 判断题目是否已发布(PUBLISHED)。 */ export function isQuestionPublished(status: string): boolean { return status === "PUBLISHED"; }