Files
Edu/apps/portal-shell/src/features/teacher/questions/transformations.ts
SpecialX 33ebb9a652 feat(portal-shell): questions + textbooks 模块 3 页迁移(教师域 §9.1 B2)
§9.1 line 630-631 教师域:
- /shell/teacher/questions (列表,1 页)
- /shell/teacher/textbooks + /shell/teacher/textbooks/[id] (列表+详情,2 页)
契约:🟡 混合
- question(id)  真实单查(schema 第 775-778 行确认)
- textbook(id)  真实单查
- 列表查询  schema 无 → MSW 兜底 + @contract-pending
- textbookChapters(textbookId)  schema 无 → MSW 兜底

新增文件:
- src/lib/api/questions.ts (5 hooks)
- src/lib/api/textbooks.ts (5 hooks)
- src/lib/api/operations/{questions,textbooks}.graphql.ts (10 documents)
- src/features/teacher/questions/ (clients + transformations + tests)
- src/features/teacher/textbooks/ (clients + transformations + tests)
- src/app/shell/teacher/{questions,textbooks}/ (3 page.tsx + 2 loading + 2 error)

修改文件:
- src/lib/api/teacher.ts + operations/teacher.graphql.ts
  → 重命名 legacy widget API 以解决命名冲突:
    Question → QuestionBankItem
    Textbook → LegacyTextbook
    Chapter → LegacyChapter
    TextbookFilter → LegacyTextbookFilter
    useTextbooks → useLegacyTextbooks
    GET_QUESTIONS_DOC → GET_QUESTION_BANK_DOC
    GET_TEXTBOOKS_DOC → GET_LEGACY_TEXTBOOKS_DOC
- src/widgets/teacher/{question-bank,textbook-manager}/index.tsx
  → 更新引用为重命名后的 legacy API
- src/mocks/graphql-data.ts
  → 添加 questions/textbooks mock + GetQuestionBank/GetLegacyTextbooks handler
- src/messages/{zh-CN,en}.json (questions + textbooks i18n)
- src/lib/api/{index,operations/index}.ts (导出 questions + textbooks)
- src/shared/lib/route-permissions.ts (questions + textbooks 路由权限)
- scripts/check-page-count.ts (baseline 37 → 40)

DoD 验收(§11.3 11 项):
- typecheck 0 errors
- lint 0 errors
- vitest 469 tests passed (新增 64 tests)
- lint:tokens 0 errors
- check:pages 40 PASS
- route-permissions 已声明
- 三态齐备
- @contract-pending + MSW 兜底
- i18n zh-CN + en 同步

关联:ARCHITECTURE.md §5.3 / §5.4 / §5.5 / §9.1 / §10 P2 / §11.3 / §11.4
契约工单:docs/architecture/issues/contracts/core-edu_contract.md
2026-07-22 20:29:59 +08:00

186 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, string> = {
single_choice: "单选题",
multiple_choice: "多选题",
fill_blank: "填空题",
short_answer: "简答题",
essay: "论述题",
true_false: "判断题",
};
/** 题目状态中文标签映射 */
export const QUESTION_STATUS_LABEL: Record<string, string> = {
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";
}