@@ -137,14 +210,26 @@ export function InvitationCodesListClient(): React.ReactElement {
description={t("description")}
icon={}
actions={
-
+ <>
+
+ {selectedIds.size > 0 ? (
+
+ ) : null}
+ >
}
filters={
@@ -169,142 +254,97 @@ export function InvitationCodesListClient(): React.ReactElement {
emptyNode={emptyNode}
errorNode={errorNode}
pagination={
-
- {t("total", { count: items.length })}
-
+ updateQuery("page", String(p))}
+ />
}
>
-
- {showGenerateForm ? (
- {
- try {
- await createInvitation.run({ role, maxUses, ttlHours });
- notify.success(t("generateButton"));
- setShowGenerateForm(false);
- } catch (e) {
- notify.error(
- tCommon("error.loadFailed", { message: String(e) }),
- );
- }
- }}
- onCancel={() => setShowGenerateForm(false)}
- />
- ) : null}
-
+
+
+
+
+
+
+
+ setShowGenerateDialog(false)}
+ onGenerated={() => void refetch()}
+ />
+
+
+
+ {t("deleteConfirmTitle")}
+
+ {t("deleteConfirmDesc", { count: selectedIds.size })}
+
+
+
+ {tCommon("cancel")}
+ void handleDeleteSelected()}
+ disabled={deleteCodes.loading}
+ >
+ {t("confirmDelete")}
+
+
+
+
);
}
-/**
- * 生成邀请码表单(内联展开,对齐 §7.3 列表页 + 行内操作)。
- */
-function GenerateForm({
- loading,
- onSubmit,
- onCancel,
-}: {
- loading: boolean;
- onSubmit: (role: string, maxUses: number, ttlHours: number) => Promise;
- onCancel: () => void;
-}): React.ReactElement {
- const tForm = useTranslations("admin.invitationCodes.generateForm");
- const [role, setRole] = useState("teacher");
- const [maxUses, setMaxUses] = useState(DEFAULT_MAX_USES);
- const [ttlHours, setTtlHours] = useState(DEFAULT_TTL_HOURS);
-
- const handleSubmit = (e: React.FormEvent): void => {
- e.preventDefault();
- void onSubmit(role, maxUses, ttlHours);
- };
-
- return (
-
-
- {tForm("title")}
-
-
-
- );
-}
-
/**
* 邀请码列表表格(纯展示组件,对齐 §8.2 排版规范)。
*/
function InvitationCodesTable({
items,
pendingRevokeId,
+ selectedIds,
+ onToggleSelect,
+ onToggleSelectAll,
onCopy,
onRevoke,
+ now,
}: {
items: InvitationCode[];
pendingRevokeId: string | null;
+ selectedIds: Set;
+ onToggleSelect: (id: string) => void;
+ onToggleSelectAll: () => void;
onCopy: (code: string) => void;
onRevoke: (id: string) => void;
+ now: number;
}): React.ReactElement {
const t = useTranslations("admin.invitationCodes.list");
return (
@@ -312,6 +352,15 @@ function InvitationCodesTable({
+ |
+ 0 && selectedIds.size === items.length}
+ onChange={onToggleSelectAll}
+ aria-label="select all"
+ className="size-4 rounded border-input"
+ />
+ |
{t("colCode")} |
{t("colRole")} |
{t("colStatus")} |
@@ -324,10 +373,19 @@ function InvitationCodesTable({
{items.map((item) => {
- const effectiveStatus = getEffectiveStatus(item);
- const canRevoke = isInvitationRevocable(item);
+ const effectiveStatus = getEffectiveStatus(item, now);
+ const canRevoke = isInvitationRevocable(item, now);
return (
+ |
+ onToggleSelect(item.id)}
+ aria-label="select"
+ className="size-4 rounded border-input"
+ />
+ |
{item.code} |
{roleToLabel(item.role)}
@@ -391,3 +449,92 @@ function StatusBadge({ status }: { status: string }): React.ReactElement {
);
}
+
+/**
+ * 分页条(页码按钮 + 上一页/下一页 + 总数)。
+ * 总页数 > 7 时使用窗口策略(首末页 + 当前页 ±1 + 省略号)。
+ */
+function PaginationBar({
+ total,
+ page,
+ pageSize,
+ onNavigate,
+}: {
+ total: number;
+ page: number;
+ pageSize: number;
+ onNavigate: (page: number) => void;
+}): React.ReactElement {
+ const t = useTranslations("admin.invitationCodes.list");
+ const tCommon = useTranslations("common");
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+ const currentPage = Math.min(Math.max(1, page), totalPages);
+
+ const pages: Array = (() => {
+ if (totalPages <= 7) {
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
+ }
+ const result: Array = [1];
+ const start = Math.max(2, currentPage - 1);
+ const end = Math.min(totalPages - 1, currentPage + 1);
+ if (start > 2) result.push("ellipsis");
+ for (let i = start; i <= end; i++) result.push(i);
+ if (end < totalPages - 1) result.push("ellipsis");
+ result.push(totalPages);
+ return result;
+ })();
+
+ if (total === 0) {
+ return (
+
+ {t("total", { count: 0 })}
+
+ );
+ }
+
+ return (
+
+ {t("total", { count: total })}
+
+
+ {pages.map((p, idx) =>
+ p === "ellipsis" ? (
+
+ …
+
+ ) : (
+
+ ),
+ )}
+
+
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/invitation-codes/transformations.ts b/apps/portal-shell/src/features/admin/invitation-codes/transformations.ts
index f2bf9e6..331f9ae 100644
--- a/apps/portal-shell/src/features/admin/invitation-codes/transformations.ts
+++ b/apps/portal-shell/src/features/admin/invitation-codes/transformations.ts
@@ -92,22 +92,36 @@ export function invitationStatusToBadgeClass(status: string): string {
/**
* 判断邀请码是否已过期(基于 expiresAt 与当前时间比较)。
* 已撤销或已用完的码不算过期。
+ *
+ * @param code 邀请码对象
+ * @param now 当前时间戳(ms),由调用方注入以避免纯函数内调用 Date.now(),
+ * 避免 SSR/CSR 时间漂移导致 hydration mismatch。
+ * 默认值 Date.now() 仅为兼容旧调用方,新代码应显式注入。
*/
-export function isInvitationExpired(code: InvitationCode): boolean {
+export function isInvitationExpired(
+ code: InvitationCode,
+ now: number = Date.now(),
+): boolean {
if (code.status === "expired") return true;
if (code.status === "used" || code.status === "revoked") return false;
if (!code.expiresAt) return false;
const d = new Date(code.expiresAt);
if (Number.isNaN(d.getTime())) return false;
- return d.getTime() < Date.now();
+ return d.getTime() < now;
}
/**
* 获取邀请码的有效显示状态。
* 优先返回数据库 status,若数据库为 active 但已过期则返回 "expired"。
+ *
+ * @param code 邀请码对象
+ * @param now 当前时间戳(ms),由调用方注入(推荐从 server page.tsx 传入避免 client Date.now)
*/
-export function getEffectiveStatus(code: InvitationCode): string {
- if (code.status === "active" && isInvitationExpired(code)) {
+export function getEffectiveStatus(
+ code: InvitationCode,
+ now: number = Date.now(),
+): string {
+ if (code.status === "active" && isInvitationExpired(code, now)) {
return "expired";
}
return code.status;
@@ -135,9 +149,15 @@ export function isInvitationUsedUp(code: InvitationCode): boolean {
/**
* 判断邀请码是否可撤销(仅 active 状态可撤销)。
+ *
+ * @param code 邀请码对象
+ * @param now 当前时间戳(ms),由调用方注入(推荐从 server page.tsx 传入避免 client Date.now)
*/
-export function isInvitationRevocable(code: InvitationCode): boolean {
- return getEffectiveStatus(code) === "active";
+export function isInvitationRevocable(
+ code: InvitationCode,
+ now: number = Date.now(),
+): boolean {
+ return getEffectiveStatus(code, now) === "active";
}
// ============================================================
diff --git a/apps/portal-shell/src/features/admin/lesson-plans/__tests__/transformations.test.ts b/apps/portal-shell/src/features/admin/lesson-plans/__tests__/transformations.test.ts
index 340391f..6c8a7b5 100644
--- a/apps/portal-shell/src/features/admin/lesson-plans/__tests__/transformations.test.ts
+++ b/apps/portal-shell/src/features/admin/lesson-plans/__tests__/transformations.test.ts
@@ -5,130 +5,35 @@
*/
import { describe, expect, it } from "vitest";
-import {
- LESSON_PLAN_STATUS_LABEL,
- formatLessonPlanDate,
- formatLessonPlanStatus,
- isLessonPlanArchived,
- isLessonPlanEditable,
- isLessonPlanPublished,
- lessonPlanStatusToBadgeClass,
- toAdminLessonPlanListItem,
-} from "../transformations";
+import { formatCount } from "../transformations";
-describe("formatLessonPlanStatus", () => {
- it("maps known statuses to Chinese labels", () => {
- expect(formatLessonPlanStatus("DRAFT")).toBe("草稿");
- expect(formatLessonPlanStatus("PUBLISHED")).toBe("已发布");
- expect(formatLessonPlanStatus("ARCHIVED")).toBe("已归档");
- expect(formatLessonPlanStatus("SUBMITTED")).toBe("已提交");
+describe("formatCount", () => {
+ it("returns string representation for non-negative finite numbers", () => {
+ expect(formatCount(0)).toBe("0");
+ expect(formatCount(1)).toBe("1");
+ expect(formatCount(42)).toBe("42");
+ expect(formatCount(1000)).toBe("1000");
});
- it("returns original value for unknown status", () => {
- expect(formatLessonPlanStatus("UNKNOWN")).toBe("UNKNOWN");
- expect(formatLessonPlanStatus("")).toBe("");
+ it("returns '0' for null", () => {
+ expect(formatCount(null)).toBe("0");
});
- it("LESSON_PLAN_STATUS_LABEL covers 4 standard statuses", () => {
- expect(Object.keys(LESSON_PLAN_STATUS_LABEL)).toHaveLength(4);
- });
-});
-
-describe("formatLessonPlanDate", () => {
- it("formats valid ISO date string", () => {
- const result = formatLessonPlanDate("2026-07-22T10:30:00Z");
- expect(result).toContain("2026");
- expect(result).toContain("07");
- });
-
- it("returns placeholder for null/undefined/empty", () => {
- expect(formatLessonPlanDate(null)).toBe("--");
- expect(formatLessonPlanDate(undefined)).toBe("--");
- expect(formatLessonPlanDate("")).toBe("--");
- });
-
- it("returns placeholder for invalid date", () => {
- expect(formatLessonPlanDate("not-a-date")).toBe("--");
- });
-});
-
-describe("isLessonPlanEditable", () => {
- it("returns true for DRAFT and PUBLISHED", () => {
- expect(isLessonPlanEditable("DRAFT")).toBe(true);
- expect(isLessonPlanEditable("PUBLISHED")).toBe(true);
- });
-
- it("returns false for ARCHIVED and unknown", () => {
- expect(isLessonPlanEditable("ARCHIVED")).toBe(false);
- expect(isLessonPlanEditable("UNKNOWN")).toBe(false);
- });
-});
-
-describe("isLessonPlanPublished", () => {
- it("returns true only for PUBLISHED", () => {
- expect(isLessonPlanPublished("PUBLISHED")).toBe(true);
- expect(isLessonPlanPublished("DRAFT")).toBe(false);
- expect(isLessonPlanPublished("ARCHIVED")).toBe(false);
- });
-});
-
-describe("isLessonPlanArchived", () => {
- it("returns true only for ARCHIVED", () => {
- expect(isLessonPlanArchived("ARCHIVED")).toBe(true);
- expect(isLessonPlanArchived("PUBLISHED")).toBe(false);
- expect(isLessonPlanArchived("DRAFT")).toBe(false);
- });
-});
-
-describe("toAdminLessonPlanListItem", () => {
- it("extracts list fields from full detail and drops extra fields", () => {
- const detail = {
- id: "lp-001",
- title: "集合的概念",
- subjectId: "sub-math",
- subjectName: "数学",
- teacherId: "usr-001",
- teacherName: "张老师",
- classId: "cls-001",
- className: "高三(1)班",
- status: "PUBLISHED",
- createdAt: "2026-07-10T00:00:00Z",
- updatedAt: "2026-07-15T00:00:00Z",
- };
-
- const item = toAdminLessonPlanListItem(detail);
- expect(item.id).toBe("lp-001");
- expect(item.title).toBe("集合的概念");
- expect(item.subjectName).toBe("数学");
- expect(item.teacherName).toBe("张老师");
- expect(item.className).toBe("高三(1)班");
- expect(item.status).toBe("PUBLISHED");
- expect(item).not.toHaveProperty("textbookId");
- expect(item).not.toHaveProperty("content");
- });
-});
-
-describe("lessonPlanStatusToBadgeClass", () => {
- it("returns primary class for PUBLISHED", () => {
- expect(lessonPlanStatusToBadgeClass("PUBLISHED")).toContain("primary");
- });
-
- it("returns amber class for SUBMITTED", () => {
- expect(lessonPlanStatusToBadgeClass("SUBMITTED")).toContain("amber");
- });
-
- it("returns muted class for DRAFT and ARCHIVED", () => {
- expect(lessonPlanStatusToBadgeClass("DRAFT")).toBe(
- "bg-muted text-muted-foreground",
- );
- expect(lessonPlanStatusToBadgeClass("ARCHIVED")).toBe(
- "bg-muted text-muted-foreground",
- );
- });
-
- it("returns muted class for unknown status", () => {
- expect(lessonPlanStatusToBadgeClass("UNKNOWN")).toBe(
- "bg-muted text-muted-foreground",
- );
+ it("returns '0' for undefined", () => {
+ expect(formatCount(undefined)).toBe("0");
+ });
+
+ it("returns '0' for NaN", () => {
+ expect(formatCount(Number.NaN)).toBe("0");
+ });
+
+ it("returns '0' for Infinity", () => {
+ expect(formatCount(Number.POSITIVE_INFINITY)).toBe("0");
+ expect(formatCount(Number.NEGATIVE_INFINITY)).toBe("0");
+ });
+
+ it("returns '0' for negative numbers", () => {
+ expect(formatCount(-1)).toBe("0");
+ expect(formatCount(-100)).toBe("0");
});
});
diff --git a/apps/portal-shell/src/features/admin/lesson-plans/delete-confirm-dialog.tsx b/apps/portal-shell/src/features/admin/lesson-plans/delete-confirm-dialog.tsx
new file mode 100644
index 0000000..f0d2700
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/lesson-plans/delete-confirm-dialog.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+/**
+ * 教案软删除确认对话框(轻量自实现模态,ARCHITECTURE.md §7.3 / §9.4)
+ *
+ * 用于 admin/lesson-plans 列表页与详情页的删除确认。
+ * 结构:fixed inset-0 + bg-black/50 + 居中卡片。
+ * 交互:ESC 关闭、点击遮罩关闭、确认按钮 destructive 变体。
+ *
+ * 关联:ARCHITECTURE.md §7.3 详情/列表页 / §9.4 / §11.3
+ */
+import { AlertTriangle, Loader2 } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useEffect } from "react";
+
+import { Button } from "@/shared/components/ui/button";
+
+export interface DeleteConfirmDialogProps {
+ /** 是否打开 */
+ open: boolean;
+ /** 确认删除回调 */
+ onConfirm: () => void;
+ /** 取消回调(点击遮罩 / ESC / 取消按钮) */
+ onCancel: () => void;
+ /** 删除进行中(禁用按钮、隐藏 spinner) */
+ loading?: boolean;
+}
+
+/**
+ * 教案软删除确认对话框。
+ *
+ * 文案来自 admin.lessonPlans.delete.* 命名空间。
+ */
+export function DeleteConfirmDialog({
+ open,
+ onConfirm,
+ onCancel,
+ loading = false,
+}: DeleteConfirmDialogProps): React.ReactElement | null {
+ const t = useTranslations("admin.lessonPlans.delete");
+
+ useEffect(() => {
+ if (!open) return;
+ const handler = (e: KeyboardEvent): void => {
+ if (e.key === "Escape" && !loading) {
+ onCancel();
+ }
+ };
+ window.addEventListener("keydown", handler);
+ return () => window.removeEventListener("keydown", handler);
+ }, [open, loading, onCancel]);
+
+ if (!open) return null;
+
+ return (
+
+ e.stopPropagation()}
+ >
+
+
+
+
+
+ {t("title")}
+
+ {t("description")}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/lesson-plans/lesson-plan-view-client.tsx b/apps/portal-shell/src/features/admin/lesson-plans/lesson-plan-view-client.tsx
index ae06b03..0345fe3 100644
--- a/apps/portal-shell/src/features/admin/lesson-plans/lesson-plan-view-client.tsx
+++ b/apps/portal-shell/src/features/admin/lesson-plans/lesson-plan-view-client.tsx
@@ -13,21 +13,25 @@
*
* 关联:ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
*/
-import { BookOpen } from "lucide-react";
-import { useParams } from "next/navigation";
+import { BookOpen, Trash2 } from "lucide-react";
+import Link from "next/link";
+import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import {
useAdminLessonPlan,
+ useSoftDeleteLessonPlan,
type AdminLessonPlan as AdminLessonPlanData,
} from "@/lib/api";
+import { Button } from "@/shared/components/ui/button";
import {
DetailPageShell,
DetailPageSkeleton,
DetailSection,
DetailField,
} from "@/shared/components/page-templates";
+import { DeleteConfirmDialog } from "@/features/admin/lesson-plans/delete-confirm-dialog";
import { notify } from "@/shared/lib/notify";
import {
formatLessonPlanDate,
@@ -41,18 +45,47 @@ import {
export function AdminLessonPlanViewClient(): React.ReactElement {
const t = useTranslations("admin.lessonPlans.detail");
const tCommon = useTranslations("common");
+ const tDelete = useTranslations("admin.lessonPlans.delete");
+ const router = useRouter();
const params = useParams<{ planId: string }>();
const planId = params?.planId ?? "";
// @contract-pending:MSW 兜底
const { data, loading, error } = useAdminLessonPlan(planId);
+ // 软删除 mutation(@contract-pending,MSW 兜底)
+ const { run: runSoftDelete, loading: deleteLoading } =
+ useSoftDeleteLessonPlan();
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+
useEffect(() => {
if (error) {
notify.error(tCommon("error.loadFailed", { message: String(error) }));
}
}, [error, tCommon]);
+ const handleDeleteClick = (): void => {
+ setDeleteDialogOpen(true);
+ };
+
+ const handleDeleteConfirm = async (): Promise => {
+ if (!planId) return;
+ try {
+ await runSoftDelete(planId);
+ notify.success(tDelete("success"));
+ setDeleteDialogOpen(false);
+ // 删除成功后返回列表页
+ router.push("/shell/admin/lesson-plans");
+ } catch {
+ notify.error(tDelete("error"));
+ }
+ };
+
+ const handleDeleteCancel = (): void => {
+ if (deleteLoading) return;
+ setDeleteDialogOpen(false);
+ };
+
const errorNode = error ? (
@@ -68,18 +101,47 @@ export function AdminLessonPlanViewClient(): React.ReactElement {
) : undefined;
+ const isArchived = data?.status === "ARCHIVED";
+
+ const actions = (
+
+
+
+
+ );
+
return (
- }
- backHref="/shell/admin/lesson-plans"
- loading={loading}
- loadingNode={}
- errorNode={errorNode}
- emptyNode={emptyNode}
- >
- {data ? : null}
-
+ <>
+ }
+ backHref="/shell/admin/lesson-plans"
+ actions={actions}
+ loading={loading}
+ loadingNode={}
+ errorNode={errorNode}
+ emptyNode={emptyNode}
+ >
+ {data ? : null}
+
+
+ >
);
}
diff --git a/apps/portal-shell/src/features/admin/lesson-plans/lesson-plans-list-client.tsx b/apps/portal-shell/src/features/admin/lesson-plans/lesson-plans-list-client.tsx
index 1c7a6e4..5722f98 100644
--- a/apps/portal-shell/src/features/admin/lesson-plans/lesson-plans-list-client.tsx
+++ b/apps/portal-shell/src/features/admin/lesson-plans/lesson-plans-list-client.tsx
@@ -13,21 +13,29 @@
*
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
-import { BookOpen } from "lucide-react";
+import { Archive, BookOpen, CheckCircle, FileEdit, Trash2 } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
-import { useMemo, useTransition } from "react";
+import { useMemo, useState, useTransition } from "react";
import { useTranslations } from "next-intl";
-import { useAdminLessonPlans, type AdminLessonPlanListItem } from "@/lib/api";
+import {
+ useAdminLessonPlans,
+ useSoftDeleteLessonPlan,
+ type AdminLessonPlanListItem,
+} from "@/lib/api";
import { Button } from "@/shared/components/ui/button";
+import { DeleteConfirmDialog } from "@/features/admin/lesson-plans/delete-confirm-dialog";
+import { notify } from "@/shared/lib/notify";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { FilterSearchInput } from "@/shared/components/ui/filter-bar";
+import { StatCard } from "@/shared/components/ui/stat-card";
import {
ListPageShell,
ListPageSkeleton,
} from "@/shared/components/page-templates";
import {
+ formatCount,
formatLessonPlanDate,
formatLessonPlanStatus,
lessonPlanStatusToBadgeClass,
@@ -37,6 +45,11 @@ import {
const STATUS_OPTIONS = ["DRAFT", "PUBLISHED", "ARCHIVED", "SUBMITTED"] as const;
type StatusOption = (typeof STATUS_OPTIONS)[number];
+/** 每页条数 */
+const PAGE_SIZE = 10;
+/** 分页组件最多展示的页码按钮数(奇数,便于左右对称) */
+const MAX_PAGE_BUTTONS = 7;
+
/**
* 列表客户端主体。需由 server page 包裹在 中
* (useSearchParams 要求 Suspense 边界,Next.js 15 强制)。
@@ -53,10 +66,18 @@ export function AdminLessonPlansListClient(): React.ReactElement {
? (statusParam as StatusOption)
: "";
const q = searchParams.get("q") ?? "";
+ const page = Number(searchParams.get("page") ?? "1") || 1;
// @contract-pending:MSW 兜底
const { data, loading, error } = useAdminLessonPlans();
+ // 软删除 mutation(@contract-pending,MSW 兜底)
+ const { run: runSoftDelete, loading: deleteLoading } =
+ useSoftDeleteLessonPlan();
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [pendingDeleteId, setPendingDeleteId] = useState(null);
+ const tDelete = useTranslations("admin.lessonPlans.delete");
+
// 客户端二次筛选(status + q)—— 后端补齐列表查询后改服务端筛选
const filteredItems = useMemo(() => {
const items = data?.items ?? [];
@@ -71,6 +92,39 @@ export function AdminLessonPlansListClient(): React.ReactElement {
});
}, [data, status, q]);
+ const total = filteredItems.length;
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
+ const safePage = Math.min(Math.max(1, page), totalPages);
+ const pagedItems = useMemo(() => {
+ const start = (safePage - 1) * PAGE_SIZE;
+ return filteredItems.slice(start, start + PAGE_SIZE);
+ }, [filteredItems, safePage]);
+
+ const handleDeleteClick = (planId: string): void => {
+ setPendingDeleteId(planId);
+ setDeleteDialogOpen(true);
+ };
+
+ const handleDeleteConfirm = async (): Promise => {
+ if (!pendingDeleteId) return;
+ try {
+ await runSoftDelete(pendingDeleteId);
+ notify.success(tDelete("success"));
+ setDeleteDialogOpen(false);
+ setPendingDeleteId(null);
+ // 刷新列表(MSW 已就地修改 mock 数据,重新拉取即可反映状态变化)
+ router.refresh();
+ } catch {
+ notify.error(tDelete("error"));
+ }
+ };
+
+ const handleDeleteCancel = (): void => {
+ if (deleteLoading) return;
+ setDeleteDialogOpen(false);
+ setPendingDeleteId(null);
+ };
+
const updateQuery = (key: string, value: string): void => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
@@ -78,8 +132,8 @@ export function AdminLessonPlansListClient(): React.ReactElement {
} else {
params.delete(key);
}
- // 切换筛选时重置页码(暂无分页,保留兼容入口)
- if (key === "status") {
+ // 切换筛选时重置页码
+ if (key !== "page") {
params.delete("page");
}
startTransition(() => {
@@ -109,49 +163,121 @@ export function AdminLessonPlansListClient(): React.ReactElement {
);
return (
- }
- actions={
-
- }
- filters={
- <>
- updateQuery("q", v)}
+ <>
+ }
+ actions={
+
+ }
+ filters={
+ <>
+ updateQuery("q", v)}
+ />
+
+ >
+ }
+ loading={loading}
+ loadingNode={}
+ empty={filteredItems.length === 0 && !loading}
+ emptyNode={emptyNode}
+ errorNode={errorNode}
+ pagination={
+ updateQuery("page", String(p))}
+ />
+ }
+ >
+
+
+
-
- >
- }
- loading={loading}
- loadingNode={ }
- empty={filteredItems.length === 0 && !loading}
- emptyNode={emptyNode}
- errorNode={errorNode}
- pagination={
-
- {t("total", { count: filteredItems.length })}
- }
- >
-
-
+
+
+ >
+ );
+}
+
+/**
+ * 教案统计概览卡片组(总数 / 已发布 / 草稿 / 已归档)。
+ *
+ * 对齐 CICD admin/lesson-plans/page.tsx 的"4 卡"契约。
+ * 三态:loading 时 StatCard isLoading=true 显示骨架。
+ */
+function LessonPlansStatsCards({
+ items,
+ loading,
+}: {
+ items: AdminLessonPlanListItem[];
+ loading: boolean;
+}): React.ReactElement {
+ const t = useTranslations("admin.lessonPlans.list");
+
+ const total = items.length;
+ const published = items.filter((i) => i.status === "PUBLISHED").length;
+ const draft = items.filter((i) => i.status === "DRAFT").length;
+ const archived = items.filter((i) => i.status === "ARCHIVED").length;
+
+ return (
+
+
+ {t("statsTitle")}
+
+
+
+
+
+
+
+
);
}
@@ -160,10 +286,13 @@ export function AdminLessonPlansListClient(): React.ReactElement {
*/
function AdminLessonPlansTable({
items,
+ onDelete,
}: {
items: AdminLessonPlanListItem[];
+ onDelete: (planId: string) => void;
}): React.ReactElement {
const t = useTranslations("admin.lessonPlans.list");
+ const tDelete = useTranslations("admin.lessonPlans.delete");
return (
@@ -201,12 +330,25 @@ function AdminLessonPlansTable({
{formatLessonPlanDate(plan.updatedAt)}
|
-
- {t("viewDetail")}
-
+
+
+ {t("viewDetail")}
+
+
+
|
))}
@@ -234,3 +376,111 @@ function LessonPlanStatusBadge({
);
}
+
+/**
+ * 分页组件(页码列表 + 跳转按钮 + total/totalPages 显示)。
+ * 依赖 URL ?page=N 状态,由父组件控制路由跳转。
+ * 页码按钮策略:当 totalPages ≤ MAX_PAGE_BUTTONS 时全量展示;
+ * 超过时展示首尾页 + 当前页附近的页码(含省略号占位)。
+ */
+function Pagination({
+ page,
+ pageSize,
+ total,
+ onJump,
+}: {
+ page: number;
+ pageSize: number;
+ total: number;
+ onJump: (page: number) => void;
+}): React.ReactElement {
+ const t = useTranslations("admin.lessonPlans.list");
+ const tCommon = useTranslations("common");
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+ const canPrev = page > 1;
+ const canNext = page < totalPages;
+ const pages = buildPageList(page, totalPages, MAX_PAGE_BUTTONS);
+
+ return (
+
+
+ {t("total", { count: total })}
+
+ {page} / {totalPages}
+
+
+
+
+ {pages.map((p, idx) =>
+ p === "..." ? (
+
+ …
+
+ ) : (
+
+ ),
+ )}
+
+
+
+ );
+}
+
+/**
+ * 构造页码列表:当总页数不超过 maxButtons 时全部展示;
+ * 否则展示首尾页与当前页附近页码,省略位置用 "..." 占位。
+ */
+function buildPageList(
+ current: number,
+ total: number,
+ maxButtons: number,
+): Array {
+ if (total <= maxButtons) {
+ return Array.from({ length: total }, (_, i) => i + 1);
+ }
+ const half = Math.floor(maxButtons / 2);
+ const start = Math.max(2, current - half + 1);
+ const end = Math.min(total - 1, start + maxButtons - 4);
+ const adjustedStart =
+ end - start < maxButtons - 4 ? Math.max(2, end - (maxButtons - 5)) : start;
+ const result: Array = [1];
+ if (adjustedStart > 2) {
+ result.push("...");
+ }
+ for (let p = adjustedStart; p <= end; p += 1) {
+ result.push(p);
+ }
+ if (end < total - 1) {
+ result.push("...");
+ }
+ result.push(total);
+ return result;
+}
diff --git a/apps/portal-shell/src/features/admin/lesson-plans/transformations.ts b/apps/portal-shell/src/features/admin/lesson-plans/transformations.ts
index 2f8f00e..c25964a 100644
--- a/apps/portal-shell/src/features/admin/lesson-plans/transformations.ts
+++ b/apps/portal-shell/src/features/admin/lesson-plans/transformations.ts
@@ -96,6 +96,16 @@ export function toAdminLessonPlanListItem(detail: {
};
}
+/**
+ * 格式化数量为展示字符串。
+ * 输入无效(null/undefined/NaN/负数)返回 "0"。
+ */
+export function formatCount(count: number | null | undefined): string {
+ if (count === null || count === undefined) return "0";
+ if (!Number.isFinite(count) || count < 0) return "0";
+ return `${count}`;
+}
+
/**
* 根据教案状态返回 Tailwind 徽章语义类名。
*/
diff --git a/apps/portal-shell/src/features/admin/organization/organization-tree-client.tsx b/apps/portal-shell/src/features/admin/organization/organization-tree-client.tsx
index 810062c..4d8bed5 100644
--- a/apps/portal-shell/src/features/admin/organization/organization-tree-client.tsx
+++ b/apps/portal-shell/src/features/admin/organization/organization-tree-client.tsx
@@ -11,17 +11,18 @@
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
import { Building2 } from "lucide-react";
-import Link from "next/link";
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { useOrganizationTree } from "@/lib/api/admin-p5";
import type { OrgNode } from "@/lib/api/admin-p5";
+import { Button } from "@/shared/components/ui/button";
import { EmptyState } from "@/shared/components/ui/empty-state";
import {
ListPageShell,
ListPageSkeleton,
} from "@/shared/components/page-templates";
+import { notify } from "@/shared/lib/notify";
import {
collectNodeIds,
formatMemberCount,
@@ -205,12 +206,13 @@ function OrgNodeRow({
{node.children.length} |
- notify.info(t("list.mswNotice"))}
>
{t("list.viewDetail")}
-
+
|
{!isCollapsed &&
diff --git a/apps/portal-shell/src/features/admin/permissions/permissions-list-client.tsx b/apps/portal-shell/src/features/admin/permissions/permissions-list-client.tsx
index e41b6c7..584c9b6 100644
--- a/apps/portal-shell/src/features/admin/permissions/permissions-list-client.tsx
+++ b/apps/portal-shell/src/features/admin/permissions/permissions-list-client.tsx
@@ -156,6 +156,9 @@ function PermissionsGrouped({
{t("list.colPermission")}
|
+
+ {t("list.colValue")}
+ |
{t("list.colAction")}
|
@@ -173,6 +176,9 @@ function PermissionsGrouped({
className="hover:bg-muted/30"
>
{perm.name} |
+
+ {perm.value ?? "--"}
+ |
{perm.action}
|
diff --git a/apps/portal-shell/src/features/admin/questions/batch-operations.tsx b/apps/portal-shell/src/features/admin/questions/batch-operations.tsx
new file mode 100644
index 0000000..239086a
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/questions/batch-operations.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+/**
+ * 题库批量操作工具栏(迁移自 CICD questions 模块)
+ *
+ * 流程:
+ * 1. 父组件维护 selectedIds 集合,传入本组件
+ * 2. 当 selectedIds 非空时,本组件展示工具栏(含计数 + 批量删除按钮)
+ * 3. 用户点击「批量删除」→ 弹出确认对话框 → 调用 useBatchDeleteQuestions
+ * 4. 成功后通知 + 触发父组件刷新 + 清空选中
+ *
+ * 设计:
+ * - 轻量模态确认(对齐 create-question-dialog.tsx 的轻量模式)
+ * - 工具栏样式对齐 ListPageShell actions 区
+ *
+ * 数据契约:
+ * - batchDeleteQuestions(ids) ❌ schema 无 → MSW 兜底(@contract-pending)
+ * - 契约工单:docs/architecture/issues/contracts/core-edu_contract.md#batch-delete-questions-mutation
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §7.3 / §9.4 / §10 P5 / §11.4
+ */
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+
+import { useBatchDeleteQuestions } from "@/lib/api";
+import { Button } from "@/shared/components/ui/button";
+import { notify } from "@/shared/lib/notify";
+
+interface BatchOperationsProps {
+ selectedIds: string[];
+ onClear: () => void;
+ onDeleted: () => void;
+}
+
+/**
+ * 批量操作工具栏。
+ *
+ * - selectedIds 为空时不渲染
+ * - 非空时展示:选中数量 + 批量删除按钮 + 清空选择按钮
+ * - 点击批量删除弹出确认模态
+ *
+ * @param selectedIds 当前选中的题目 id 列表
+ * @param onClear 清空选中(父组件清空 selectedIds)
+ * @param onDeleted 删除成功后的回调(父组件刷新列表 + 清空选中)
+ */
+export function BatchOperations({
+ selectedIds,
+ onClear,
+ onDeleted,
+}: BatchOperationsProps): React.ReactElement | null {
+ const t = useTranslations("admin.questions.batch");
+ const tCommon = useTranslations("common");
+ const deleteMutation = useBatchDeleteQuestions();
+ const [confirmOpen, setConfirmOpen] = useState(false);
+
+ if (selectedIds.length === 0) return null;
+
+ const handleDeleteClick = (): void => {
+ setConfirmOpen(true);
+ };
+
+ const handleConfirmDelete = async (): Promise => {
+ try {
+ const result = await deleteMutation.run(selectedIds);
+ notify.success(
+ t("deleteSuccess", { deleted: result.deleted, failed: result.failed }),
+ );
+ onDeleted();
+ setConfirmOpen(false);
+ } catch (err) {
+ notify.error(tCommon("error.loadFailed", { message: String(err) }));
+ }
+ };
+
+ const handleCancelDelete = (): void => {
+ setConfirmOpen(false);
+ };
+
+ return (
+ <>
+
+
+ {t("selectedCount", { count: selectedIds.length })}
+
+
+
+
+
+
+
+ {confirmOpen ? (
+
+ ) : null}
+ >
+ );
+}
+
+/**
+ * 批量删除确认对话框(轻量模态,对齐 create-question-dialog 的轻量模式)。
+ */
+function ConfirmDeleteDialog({
+ count,
+ loading,
+ onConfirm,
+ onCancel,
+}: {
+ count: number;
+ loading: boolean;
+ onConfirm: () => void;
+ onCancel: () => void;
+}): React.ReactElement {
+ const t = useTranslations("admin.questions.batch");
+ return (
+
+ e.stopPropagation()}
+ >
+ {t("confirmTitle")}
+
+ {t("confirmDescription", { count })}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/questions/create-question-dialog.tsx b/apps/portal-shell/src/features/admin/questions/create-question-dialog.tsx
new file mode 100644
index 0000000..f51d127
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/questions/create-question-dialog.tsx
@@ -0,0 +1,266 @@
+"use client";
+
+/**
+ * 创建题目对话框(迁移自 CICD questions 模块)
+ *
+ * 流程:
+ * 1. 用户填写表单(题型、内容、答案、解析、难度、知识点)
+ * 2. 提交调用 useCreateQuestion
+ * 3. 成功后通知 + 触发父组件刷新 + 关闭对话框
+ *
+ * 设计:
+ * - 轻量模态(portal-shell 自实现,无 radix Dialog 依赖)
+ * - 对齐 generate-invitation-codes-dialog.tsx 的 FormField / 轻量模态模式
+ *
+ * 数据契约:
+ * - createQuestion(input) ❌ schema 无 → MSW 兜底(@contract-pending)
+ * - 契约工单:docs/architecture/issues/contracts/core-edu_contract.md#create-question-mutation
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §7.3 / §9.4 / §10 P5 / §11.4
+ */
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+
+import { useCreateQuestion } from "@/lib/api";
+import { Button } from "@/shared/components/ui/button";
+import { Input } from "@/shared/components/ui/input";
+import { notify } from "@/shared/lib/notify";
+
+/** 题型选项(与 schema Question.type 字符串语义对齐) */
+const TYPE_OPTIONS = [
+ "single_choice",
+ "multiple_choice",
+ "fill_blank",
+ "short_answer",
+ "essay",
+ "true_false",
+] as const;
+
+/** 难度选项(数值与 schema Question.difficulty Float 对齐,0~1) */
+const DIFFICULTY_OPTIONS = [
+ { value: 0.3, labelKey: "difficultyEasy" },
+ { value: 0.5, labelKey: "difficultyMedium" },
+ { value: 0.8, labelKey: "difficultyHard" },
+] as const;
+
+const DEFAULT_TYPE = "single_choice";
+const DEFAULT_DIFFICULTY = 0.5;
+
+interface CreateQuestionDialogProps {
+ open: boolean;
+ onClose: () => void;
+ onCreated: () => void;
+}
+
+/**
+ * 创建题目对话框。open 控制显隐,onCreated 在成功后触发父组件刷新。
+ */
+export function CreateQuestionDialog({
+ open,
+ onClose,
+ onCreated,
+}: CreateQuestionDialogProps): React.ReactElement | null {
+ const t = useTranslations("admin.questions.createDialog");
+ const tCommon = useTranslations("common");
+ const createMutation = useCreateQuestion();
+
+ const [type, setType] = useState(DEFAULT_TYPE);
+ const [content, setContent] = useState("");
+ const [answer, setAnswer] = useState("");
+ const [explanation, setExplanation] = useState("");
+ const [difficulty, setDifficulty] = useState(DEFAULT_DIFFICULTY);
+ const [knowledgePointId, setKnowledgePointId] = useState("");
+ const [source, setSource] = useState("");
+
+ if (!open) return null;
+
+ const resetForm = (): void => {
+ setType(DEFAULT_TYPE);
+ setContent("");
+ setAnswer("");
+ setExplanation("");
+ setDifficulty(DEFAULT_DIFFICULTY);
+ setKnowledgePointId("");
+ setSource("");
+ };
+
+ const handleClose = (): void => {
+ onClose();
+ };
+
+ const handleSubmit = async (
+ e: React.FormEvent,
+ ): Promise => {
+ e.preventDefault();
+ const trimmedContent = content.trim();
+ const trimmedAnswer = answer.trim();
+ if (!trimmedContent) {
+ notify.error(t("errorContentRequired"));
+ return;
+ }
+ if (!trimmedAnswer) {
+ notify.error(t("errorAnswerRequired"));
+ return;
+ }
+ if (!knowledgePointId.trim()) {
+ notify.error(t("errorKnowledgePointRequired"));
+ return;
+ }
+ try {
+ await createMutation.run({
+ type,
+ content: trimmedContent,
+ answer: trimmedAnswer,
+ explanation: explanation.trim() || undefined,
+ difficulty,
+ knowledgePointId: knowledgePointId.trim(),
+ source: source.trim() || undefined,
+ });
+ notify.success(t("success"));
+ onCreated();
+ resetForm();
+ onClose();
+ } catch (err) {
+ notify.error(tCommon("error.loadFailed", { message: String(err) }));
+ }
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ {t("title")}
+ {t("description")}
+
+
+
+ );
+}
+
+/**
+ * 表单字段容器(label + children)。对齐 generate-invitation-codes-dialog.tsx 的 FormField 模式。
+ */
+function FormField({
+ label,
+ required,
+ children,
+}: {
+ label: string;
+ required?: boolean;
+ children: React.ReactNode;
+}): React.ReactElement {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/questions/import-export-buttons.tsx b/apps/portal-shell/src/features/admin/questions/import-export-buttons.tsx
new file mode 100644
index 0000000..b1b3833
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/questions/import-export-buttons.tsx
@@ -0,0 +1,214 @@
+"use client";
+
+/**
+ * 题库导入/导出按钮(迁移自 CICD questions 模块)
+ *
+ * 流程:
+ * - 导入:选择 CSV/JSON 文件 → 读取文件内容 → 调用 useImportQuestions → 展示结果
+ * - 导出:调用 useExportQuestions 触发 refetch → 将 items 转为 CSV 下载
+ *
+ * 数据契约:
+ * - importQuestions(input) ❌ schema 无 → MSW 兜底(@contract-pending)
+ * - exportQuestions(filter) ❌ schema 无 → MSW 兜底(@contract-pending)
+ * - 契约工单:docs/architecture/issues/contracts/core-edu_contract.md
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §7.3 / §9.4 / §10 P5 / §11.4
+ */
+import { Download, Loader2, Upload } from "lucide-react";
+import { useRef, useState } from "react";
+import { useTranslations } from "next-intl";
+
+import {
+ useExportQuestions,
+ useImportQuestions,
+ type ExportQuestionItem,
+ type QuestionsListFilter,
+} from "@/lib/api";
+import { Button } from "@/shared/components/ui/button";
+import { notify } from "@/shared/lib/notify";
+
+interface ImportExportButtonsProps {
+ filter: QuestionsListFilter;
+ onImported: () => void;
+}
+
+/**
+ * 导入/导出按钮组。
+ *
+ * - 导入按钮:触发隐藏 file input,选择文件后调用 mutation
+ * - 导出按钮:触发查询 refetch,拿到结果后下载 CSV
+ *
+ * @param filter 当前列表筛选条件(用于导出查询)
+ * @param onImported 导入成功后的回调(父组件刷新列表)
+ */
+export function ImportExportButtons({
+ filter,
+ onImported,
+}: ImportExportButtonsProps): React.ReactElement {
+ const t = useTranslations("admin.questions.importExport");
+ const tCommon = useTranslations("common");
+ const importMutation = useImportQuestions();
+ const exportQuery = useExportQuestions(filter);
+ const fileInputRef = useRef(null);
+ const [exporting, setExporting] = useState(false);
+
+ const handleImportClick = (): void => {
+ fileInputRef.current?.click();
+ };
+
+ const handleFileChange = async (
+ e: React.ChangeEvent,
+ ): Promise => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ const format = file.name.toLowerCase().endsWith(".json") ? "json" : "csv";
+ try {
+ const payload = await file.text();
+ const result = await importMutation.run({ payload, format });
+ notify.success(
+ t("importSuccess", {
+ imported: result.imported,
+ skipped: result.skipped,
+ }),
+ );
+ onImported();
+ } catch (err) {
+ notify.error(tCommon("error.loadFailed", { message: String(err) }));
+ } finally {
+ e.target.value = "";
+ }
+ };
+
+ const handleExportClick = async (): Promise => {
+ setExporting(true);
+ try {
+ const data = await exportQuery.refetch();
+ if (!data || data.items.length === 0) {
+ notify.info(t("exportEmpty"));
+ return;
+ }
+ const csv = convertQuestionsToCsv(data.items);
+ const ok = downloadCsv("questions-export.csv", csv);
+ if (ok) {
+ notify.success(t("exportSuccess", { count: data.total }));
+ } else {
+ notify.error(t("exportFailed"));
+ }
+ } catch (err) {
+ notify.error(tCommon("error.loadFailed", { message: String(err) }));
+ } finally {
+ setExporting(false);
+ }
+ };
+
+ const isExporting = exporting || exportQuery.loading;
+
+ return (
+
+
+
+
+
+ );
+}
+
+/**
+ * 将题目列表转为 CSV 字符串(含 BOM 以兼容 Excel 中文)。
+ *
+ * 纯函数,便于单测。对齐 audit-logs/transformations.ts 的 downloadCsv 模式。
+ */
+function convertQuestionsToCsv(items: ExportQuestionItem[]): string {
+ const headers = [
+ "id",
+ "type",
+ "content",
+ "difficulty",
+ "answer",
+ "explanation",
+ "knowledgePointId",
+ "subjectId",
+ "source",
+ "status",
+ "createdAt",
+ ];
+ const escapeCell = (value: string | null | undefined): string => {
+ if (value === null || value === undefined) return "";
+ const s = String(value);
+ if (s.includes(",") || s.includes("\n") || s.includes('"')) {
+ return `"${s.replace(/"/g, '""')}"`;
+ }
+ return s;
+ };
+ const rows = items.map((item) =>
+ [
+ item.id,
+ item.type,
+ item.content,
+ item.difficulty,
+ item.answer,
+ item.explanation ?? "",
+ item.knowledgePointId,
+ item.subjectId,
+ item.source,
+ item.status,
+ item.createdAt,
+ ]
+ .map(escapeCell)
+ .join(","),
+ );
+ return `\uFEFF${headers.join(",")}\n${rows.join("\n")}`;
+}
+
+/**
+ * 触发浏览器下载 CSV 文件。返回是否成功。
+ */
+function downloadCsv(filename: string, csv: string): boolean {
+ if (typeof window === "undefined") return false;
+ try {
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/apps/portal-shell/src/features/admin/questions/question-detail-dialog.tsx b/apps/portal-shell/src/features/admin/questions/question-detail-dialog.tsx
new file mode 100644
index 0000000..92d5e97
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/questions/question-detail-dialog.tsx
@@ -0,0 +1,307 @@
+"use client";
+
+/**
+ * 题目详情对话框(ARCHITECTURE.md §5.4 / §9.4 / §10 P5)
+ *
+ * 数据契约:
+ * - adminQuestion(id):❌ schema 无 → MSW 兜底(@contract-pending)
+ *
+ * 适配 portal-shell:
+ * - 用原生轻量模态(fixed inset-0 + bg-black/50 + 卡片)替代 shadcn Dialog
+ * - 数据通过 useAdminQuestion hook 拉取
+ * - 错误处理走 notify.error()
+ *
+ * 关联:ARCHITECTURE.md §5.3 契约纪律 / §9.4 / §11.3 DoD
+ */
+import { useEffect } from "react";
+import { useTranslations } from "next-intl";
+import {
+ BookOpen,
+ Calendar,
+ FileText,
+ Hash,
+ HelpCircle,
+ Lightbulb,
+ RefreshCw,
+ Target,
+ User,
+} from "lucide-react";
+
+import { useAdminQuestion } from "@/lib/api";
+import { notify } from "@/shared/lib/notify";
+import { Badge } from "@/shared/components/ui/badge";
+import { Button } from "@/shared/components/ui/button";
+import { Separator } from "@/shared/components/ui/separator";
+import {
+ difficultyToColorClass,
+ formatDifficulty,
+ formatQuestionDate,
+ formatQuestionStatus,
+ formatQuestionType,
+ questionStatusToBadgeClass,
+} from "@/features/admin/questions/transformations";
+
+export interface QuestionDetailDialogProps {
+ /** 当前选中的题目 id,为空时关闭对话框 */
+ questionId: string | null;
+ onOpenChange: (open: boolean) => void;
+}
+
+/**
+ * 题目详情对话框。questionId 非空时打开,按 id 拉取详情。
+ */
+export function QuestionDetailDialog({
+ questionId,
+ onOpenChange,
+}: QuestionDetailDialogProps): React.ReactElement {
+ const t = useTranslations("admin.questions.detailDialog");
+ const tCommon = useTranslations("common");
+ const open = questionId !== null && questionId.length > 0;
+
+ const { data, loading, error, refetch } = useAdminQuestion(questionId ?? "", {
+ enabled: open,
+ });
+
+ // 查询失败时通知用户(§11.3 DoD #8:catch 块必须包含 notify.error())
+ useEffect(() => {
+ if (error) {
+ notify.error(tCommon("error.loadFailed", { message: String(error) }));
+ }
+ }, [error, tCommon]);
+
+ // ESC 键关闭
+ useEffect(() => {
+ if (!open) return;
+ const handleKeyDown = (e: KeyboardEvent): void => {
+ if (e.key === "Escape") onOpenChange(false);
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [open, onOpenChange]);
+
+ if (!open) return <>>;
+
+ return (
+ onOpenChange(false)}
+ >
+ e.stopPropagation()}
+ >
+ {/* 头部 */}
+
+
+
+
+ {t("title")}
+
+ {data ? (
+
+ {formatQuestionType(data.type)}
+
+ {formatQuestionStatus(data.status)}
+
+ {data.subjectName ? (
+
+ {data.subjectName}
+
+ ) : null}
+
+ ) : null}
+
+
+
+
+ {/* 内容区 */}
+
+ {loading ? (
+
+ ) : error ? (
+
+
+ {tCommon("error.loadFailed", { message: String(error) })}
+
+
+
+ ) : data ? (
+
+ {/* 元信息 */}
+
+ }
+ label={t("type")}
+ value={formatQuestionType(data.type)}
+ />
+ }
+ label={t("difficulty")}
+ value={
+
+ {formatDifficulty(data.difficulty)}
+
+ }
+ />
+ }
+ label={t("status")}
+ value={
+
+ {formatQuestionStatus(data.status)}
+
+ }
+ />
+ }
+ label={t("subject")}
+ value={data.subjectName || data.subjectId || "--"}
+ />
+ }
+ label={t("textbook")}
+ value={data.textbookTitle || data.textbookId || "--"}
+ />
+ }
+ label={t("knowledgePoint")}
+ value={
+ data.knowledgePointTitle || data.knowledgePointId || "--"
+ }
+ />
+
+
+
+
+ {/* 题目内容 */}
+
+ {t("content")}
+
+
+ {data.content || t("noContent")}
+
+
+
+
+ {/* 正确答案 */}
+ {data.answer ? (
+
+
+
+ {t("answer")}
+
+
+
+ ) : null}
+
+ {/* 解析 */}
+ {data.explanation ? (
+
+
+
+ {t("explanation")}
+
+
+
+ {data.explanation}
+
+
+
+ ) : null}
+
+ {/* 来源 */}
+ {data.source ? (
+
+ {t("source")}
+ {data.source}
+
+ ) : null}
+
+ {/* 元信息 */}
+
+
+
+ {t("createdBy")}:{data.createdBy || "--"}
+
+
+
+ {t("createdAt")}:{formatQuestionDate(data.createdAt)}
+
+
+
+ {t("updatedAt")}:{formatQuestionDate(data.updatedAt)}
+
+
+
+ ) : (
+
+ {t("noData")}
+
+ )}
+
+
+ {/* 底部 */}
+
+
+
+
+
+ );
+}
+
+/** 信息项(图标 + 标签 + 值)。 */
+function InfoItem({
+ icon,
+ label,
+ value,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ value: React.ReactNode;
+}): React.ReactElement {
+ return (
+
+ {icon}
+
+ {label}
+ {value || "--"}
+
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/questions/questions-list-client.tsx b/apps/portal-shell/src/features/admin/questions/questions-list-client.tsx
index 98f127d..1a37335 100644
--- a/apps/portal-shell/src/features/admin/questions/questions-list-client.tsx
+++ b/apps/portal-shell/src/features/admin/questions/questions-list-client.tsx
@@ -5,6 +5,7 @@
*
* 数据契约:
* - 列表查询 adminQuestions(filter) ❌ schema 无此字段 → MSW 兜底(@contract-pending)
+ * - 创建/导入/导出/批量删除:均 ❌ schema 未就绪 → MSW 兜底(@contract-pending)
* - 契约工单:docs/architecture/issues/contracts/core-edu_contract.md
*
* URL 状态:?type=&difficulty=&subjectId=&q=
@@ -14,9 +15,8 @@
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
import { HelpCircle } from "lucide-react";
-import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
-import { useMemo, useTransition } from "react";
+import { useMemo, useState, useTransition } from "react";
import { useTranslations } from "next-intl";
import { useAdminQuestions, type AdminQuestionListItem } from "@/lib/api";
@@ -37,6 +37,15 @@ import {
questionTypeToBadgeClass,
truncateContent,
} from "@/features/admin/questions/transformations";
+import { CreateQuestionDialog } from "@/features/admin/questions/create-question-dialog";
+import { ImportExportButtons } from "@/features/admin/questions/import-export-buttons";
+import { BatchOperations } from "@/features/admin/questions/batch-operations";
+import { QuestionDetailDialog } from "@/features/admin/questions/question-detail-dialog";
+
+/** 每页条数 */
+const PAGE_SIZE = 10;
+/** 分页组件最多展示的页码按钮数(奇数,便于左右对称) */
+const MAX_PAGE_BUTTONS = 7;
/**
* 列表客户端主体。需由 server page 包裹在 中
@@ -53,9 +62,16 @@ export function AdminQuestionsListClient(): React.ReactElement {
const difficultyFilter = searchParams.get("difficulty") ?? "";
const subjectId = searchParams.get("subjectId") ?? "";
const q = searchParams.get("q") ?? "";
+ const page = Number(searchParams.get("page") ?? "1") || 1;
+
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
+ const [selectedIds, setSelectedIds] = useState([]);
+ const [selectedQuestionId, setSelectedQuestionId] = useState(
+ null,
+ );
// @contract-pending:MSW 兜底
- const { data, loading, error } = useAdminQuestions({
+ const { data, loading, error, refetch } = useAdminQuestions({
type: typeFilter || null,
difficulty: difficultyFilter || null,
subjectId: subjectId || null,
@@ -70,6 +86,14 @@ export function AdminQuestionsListClient(): React.ReactElement {
return items.filter((item) => item.content.toLowerCase().includes(lower));
}, [data, q]);
+ const total = data?.total ?? filteredItems.length;
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
+ const safePage = Math.min(Math.max(1, page), totalPages);
+ const pagedItems = useMemo(() => {
+ const start = (safePage - 1) * PAGE_SIZE;
+ return filteredItems.slice(start, start + PAGE_SIZE);
+ }, [filteredItems, safePage]);
+
const updateQuery = (key: string, value: string): void => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
@@ -77,6 +101,10 @@ export function AdminQuestionsListClient(): React.ReactElement {
} else {
params.delete(key);
}
+ // 切换筛选时重置页码
+ if (key !== "page") {
+ params.delete("page");
+ }
startTransition(() => {
router.push(`/shell/admin/questions?${params.toString()}`);
});
@@ -103,89 +131,194 @@ export function AdminQuestionsListClient(): React.ReactElement {
/>
);
+ const handleCreateClick = (): void => {
+ setCreateDialogOpen(true);
+ };
+
+ const handleCreated = (): void => {
+ void refetch();
+ };
+
+ const handleImported = (): void => {
+ void refetch();
+ };
+
+ const handleClearSelection = (): void => {
+ setSelectedIds([]);
+ };
+
+ const handleBatchDeleted = (): void => {
+ setSelectedIds([]);
+ void refetch();
+ };
+
+ const handleSelectItem = (id: string, checked: boolean): void => {
+ setSelectedIds((prev) =>
+ checked ? [...prev, id] : prev.filter((x) => x !== id),
+ );
+ };
+
+ const handleSelectAll = (checked: boolean): void => {
+ setSelectedIds(checked ? pagedItems.map((item) => item.id) : []);
+ };
+
+ const exportFilter = {
+ type: typeFilter || undefined,
+ difficulty: difficultyFilter || undefined,
+ subjectId: subjectId || undefined,
+ q: q || undefined,
+ };
+
return (
- }
- actions={
-
- }
- filters={
- <>
- updateQuery("q", v)}
+ <>
+ setCreateDialogOpen(false)}
+ onCreated={handleCreated}
+ />
+ {
+ if (!open) setSelectedQuestionId(null);
+ }}
+ />
+ }
+ actions={
+
+
+
+
+ }
+ filters={
+ <>
+ updateQuery("q", v)}
+ />
+
+
+ updateQuery("subjectId", e.target.value)}
+ placeholder={t("filterSubject")}
+ className="h-9 w-40 rounded-md border border-input bg-background px-3 text-sm"
+ aria-label={t("filterSubject")}
+ />
+ >
+ }
+ loading={loading}
+ loadingNode={}
+ empty={filteredItems.length === 0 && !loading}
+ emptyNode={emptyNode}
+ errorNode={errorNode}
+ pagination={
+ updateQuery("page", String(p))}
/>
-
-
- updateQuery("subjectId", e.target.value)}
- placeholder={t("filterSubject")}
- className="h-9 w-40 rounded-md border border-input bg-background px-3 text-sm"
- aria-label={t("filterSubject")}
+ }
+ >
+
+ {selectedIds.length > 0 ? (
+
+ ) : null}
+
- >
- }
- loading={loading}
- loadingNode={ }
- empty={filteredItems.length === 0 && !loading}
- emptyNode={emptyNode}
- errorNode={errorNode}
- pagination={
-
-
- {t("total", { count: data?.total ?? filteredItems.length })}
-
- }
- >
-
-
+
+ >
);
}
/**
* 题目列表表格(纯展示组件,对齐 §8.2 排版规范)。
+ *
+ * - 第一列为 checkbox(支持单选 / 全选)
+ * - 选中态由父组件维护(selectedIds),本组件仅展示与回调
+ * - 当题目数超过 100 时启用虚拟滚动优化(CSS content-visibility: auto,
+ * 浏览器自动跳过视口外行的渲染,降低首次绘制与滚动开销,无需引入新依赖)
*/
function AdminQuestionsTable({
items,
+ selectedIds,
+ onSelectItem,
+ onSelectAll,
+ onViewDetail,
}: {
items: AdminQuestionListItem[];
+ selectedIds: string[];
+ onSelectItem: (id: string, checked: boolean) => void;
+ onSelectAll: (checked: boolean) => void;
+ onViewDetail: (id: string) => void;
}): React.ReactElement {
const t = useTranslations("admin.questions.list");
+ const allChecked =
+ items.length > 0 && items.every((item) => selectedIds.includes(item.id));
+ const someChecked = items.some((item) => selectedIds.includes(item.id));
+ // 当题目数超过 100 时启用虚拟滚动优化(CSS content-visibility)
+ const enableVirtualScroll = items.length > 100;
+ const rowStyle = enableVirtualScroll
+ ? { contentVisibility: "auto" as const, containIntrinsicSize: "0 80px" }
+ : undefined;
+
return (
@@ -275,3 +422,111 @@ function QuestionStatusBadge({
);
}
+
+/**
+ * 分页组件(页码列表 + 跳转按钮 + total/totalPages 显示)。
+ * 依赖 URL ?page=N 状态,由父组件控制路由跳转。
+ * 页码按钮策略:当 totalPages ≤ MAX_PAGE_BUTTONS 时全量展示;
+ * 超过时展示首尾页 + 当前页附近的页码(含省略号占位)。
+ */
+function Pagination({
+ page,
+ pageSize,
+ total,
+ onJump,
+}: {
+ page: number;
+ pageSize: number;
+ total: number;
+ onJump: (page: number) => void;
+}): React.ReactElement {
+ const t = useTranslations("admin.questions.list");
+ const tCommon = useTranslations("common");
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+ const canPrev = page > 1;
+ const canNext = page < totalPages;
+ const pages = buildPageList(page, totalPages, MAX_PAGE_BUTTONS);
+
+ return (
+
+
+ {t("total", { count: total })}
+
+ {page} / {totalPages}
+
+
+
+
+ {pages.map((p, idx) =>
+ p === "..." ? (
+
+ …
+
+ ) : (
+
+ ),
+ )}
+
+
+
+ );
+}
+
+/**
+ * 构造页码列表:当总页数不超过 maxButtons 时全部展示;
+ * 否则展示首尾页与当前页附近页码,省略位置用 "..." 占位。
+ */
+function buildPageList(
+ current: number,
+ total: number,
+ maxButtons: number,
+): Array {
+ if (total <= maxButtons) {
+ return Array.from({ length: total }, (_, i) => i + 1);
+ }
+ const half = Math.floor(maxButtons / 2);
+ const start = Math.max(2, current - half + 1);
+ const end = Math.min(total - 1, start + maxButtons - 4);
+ const adjustedStart =
+ end - start < maxButtons - 4 ? Math.max(2, end - (maxButtons - 5)) : start;
+ const result: Array = [1];
+ if (adjustedStart > 2) {
+ result.push("...");
+ }
+ for (let p = adjustedStart; p <= end; p += 1) {
+ result.push(p);
+ }
+ if (end < total - 1) {
+ result.push("...");
+ }
+ result.push(total);
+ return result;
+}
diff --git a/apps/portal-shell/src/features/admin/roles/_write-role-form-dialog.ps1 b/apps/portal-shell/src/features/admin/roles/_write-role-form-dialog.ps1
new file mode 100644
index 0000000..e69de29
diff --git a/apps/portal-shell/src/features/admin/roles/role-detail-client.tsx b/apps/portal-shell/src/features/admin/roles/role-detail-client.tsx
index 81eb929..998c8ab 100644
--- a/apps/portal-shell/src/features/admin/roles/role-detail-client.tsx
+++ b/apps/portal-shell/src/features/admin/roles/role-detail-client.tsx
@@ -19,6 +19,7 @@ import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useRole, type RoleDetail } from "@/lib/api";
+import { RolePermissionMatrix } from "./role-permission-matrix";
import {
DetailPageShell,
DetailPageSkeleton,
@@ -103,6 +104,15 @@ function RoleDetailBody({ role }: { role: RoleDetail }): React.ReactElement {
)}
+
+
+
+
>
);
}
diff --git a/apps/portal-shell/src/features/admin/roles/role-form-dialog.tsx b/apps/portal-shell/src/features/admin/roles/role-form-dialog.tsx
new file mode 100644
index 0000000..62b1ec4
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/roles/role-form-dialog.tsx
@@ -0,0 +1,213 @@
+"use client";
+
+/**
+ * Role create/edit dialog - lightweight modal (ARCHITECTURE.md 9.4 / 10 P5)
+ *
+ * Features:
+ * - Create mode: new role, refresh list on submit
+ * - Edit mode: edit existing role name and description
+ * - Zod pattern validation for role name (^[a-z0-9_]+$)
+ * - Value field (optional human-readable label)
+ *
+ * Data contract:
+ * - mutation createRole / updateRole: schema pending -> MSW fallback (@contract-pending)
+ *
+ * Related: ARCHITECTURE.md 5.4 / 9.4 / 10 P5 / 11.3
+ */
+import { Shield } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState, type FormEvent } from "react";
+import { useTranslations } from "next-intl";
+import { z } from "zod";
+
+import { useCreateRole, useUpdateRole, type Role } from "@/lib/api";
+import { Button } from "@/shared/components/ui/button";
+import { Input } from "@/shared/components/ui/input";
+import { Label } from "@/shared/components/ui/label";
+import { Textarea } from "@/shared/components/ui/textarea";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/shared/components/ui/dialog";
+import { notify } from "@/shared/lib/notify";
+
+/** 角色名称 zod 校验:小写字母、数字、下划线,2-50 字符。 */
+const roleNameSchema = z
+ .string()
+ .min(2)
+ .max(50)
+ .regex(/^[a-z0-9_]+$/);
+
+export interface RoleFormDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ editRole?: Role | null;
+}
+
+export function RoleFormDialog({
+ open,
+ onOpenChange,
+ editRole,
+}: RoleFormDialogProps): React.ReactElement | null {
+ const t = useTranslations("admin.roles.createDialog");
+ const tCommon = useTranslations("common");
+ const router = useRouter();
+ const isEdit = Boolean(editRole);
+ const isLocked = editRole?.isLocked ?? false;
+
+ const { run: createRole, loading: creating } = useCreateRole();
+ const { run: updateRole, loading: updating } = useUpdateRole();
+
+ const [name, setName] = useState("");
+ const [value, setValue] = useState("");
+ const [description, setDescription] = useState("");
+ const [nameError, setNameError] = useState(null);
+
+ useEffect(() => {
+ if (open) {
+ setName(editRole?.name ?? "");
+ setValue(editRole?.value ?? "");
+ setDescription(editRole?.description ?? "");
+ setNameError(null);
+ }
+ }, [open, editRole]);
+
+ const isWorking = creating || updating;
+
+ const handleClose = (): void => {
+ onOpenChange(false);
+ };
+
+ const handleSubmit = async (e: FormEvent): Promise => {
+ e.preventDefault();
+ const trimmedName = name.trim();
+ if (!trimmedName) {
+ setNameError(t("errorNameRequired"));
+ return;
+ }
+ const nameValidation = roleNameSchema.safeParse(trimmedName);
+ if (!nameValidation.success) {
+ setNameError(t("errorNamePattern"));
+ return;
+ }
+ setNameError(null);
+
+ const trimmedValue = value.trim();
+ const input = {
+ name: trimmedName,
+ value: trimmedValue || undefined,
+ description: description.trim() || undefined,
+ permissionIds: editRole?.permissions?.map((p) => p.id) ?? [],
+ };
+
+ try {
+ if (isEdit && editRole) {
+ await updateRole(editRole.id, input);
+ notify.success(t("successUpdated"));
+ } else {
+ await createRole(input);
+ notify.success(t("successCreated"));
+ }
+ onOpenChange(false);
+ router.refresh();
+ } catch (err) {
+ notify.error(tCommon("error.operationFailed", { message: String(err) }));
+ }
+ };
+
+ if (!open) {
+ return null;
+ }
+ return (
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/roles/role-permission-matrix.tsx b/apps/portal-shell/src/features/admin/roles/role-permission-matrix.tsx
new file mode 100644
index 0000000..de2aee3
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/roles/role-permission-matrix.tsx
@@ -0,0 +1,348 @@
+"use client";
+
+/**
+ * Role permission matrix - CRUD actions per permission point (ARCHITECTURE.md 9.4 / 10 P5)
+ *
+ * Features:
+ * - Table with rows = permission points (grouped by module)
+ * - Columns = CRUD actions (read, create, update, delete)
+ * - Cells = checkboxes to toggle actions
+ * - Save button to persist changes
+ * - Locked roles are read-only
+ * - Search filter (by permission label / module)
+ * - Collapse/expand per module group
+ * - User-impact Alert when role has associated users
+ *
+ * Data contract:
+ * - query rolePermissions / mutation updateRolePermissionActions: schema pending -> MSW fallback (@contract-pending)
+ *
+ * Related: ARCHITECTURE.md 5.4 / 9.4 / 10 P5 / 11.3
+ */
+import {
+ AlertTriangle,
+ ChevronDown,
+ ChevronRight,
+ Lock,
+ Save,
+ Search,
+} from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
+import { useTranslations } from "next-intl";
+
+import {
+ useRolePermissions,
+ useUpdateRolePermissionActions,
+ type PermissionActions,
+ type RolePermissionMatrixItem,
+} from "@/lib/api";
+import { Alert, AlertDescription } from "@/shared/components/ui/alert";
+import { Badge } from "@/shared/components/ui/badge";
+import { Button } from "@/shared/components/ui/button";
+import { Input } from "@/shared/components/ui/input";
+import { notify } from "@/shared/lib/notify";
+
+export interface RolePermissionMatrixProps {
+ roleId: string;
+ roleName: string;
+ isLocked: boolean;
+ /** 关联用户数(用于展示用户影响 Alert,@contract-pending MSW 兜底) */
+ userCount?: number;
+}
+
+const ACTION_KEYS: Array<{ key: keyof PermissionActions; labelKey: string }> = [
+ { key: "read", labelKey: "actionRead" },
+ { key: "create", labelKey: "actionCreate" },
+ { key: "update", labelKey: "actionUpdate" },
+ { key: "delete", labelKey: "actionDelete" },
+];
+
+export function RolePermissionMatrix({
+ roleId,
+ roleName,
+ isLocked,
+ userCount,
+}: RolePermissionMatrixProps): React.ReactElement {
+ const t = useTranslations("admin.roles.matrix");
+ const tCommon = useTranslations("common");
+
+ const { data, loading, error } = useRolePermissions(roleId);
+ const { run: updateActions, loading: saving } =
+ useUpdateRolePermissionActions();
+
+ const [permissions, setPermissions] = useState(
+ [],
+ );
+ const [search, setSearch] = useState("");
+ const [collapsed, setCollapsed] = useState>(new Set());
+
+ useEffect(() => {
+ setPermissions(data ?? []);
+ }, [data]);
+
+ // 搜索过滤:按权限点 label 或 module 命中(大小写不敏感)
+ const filteredPermissions = useMemo(() => {
+ const q = search.trim().toLowerCase();
+ if (!q) return permissions;
+ return permissions.filter((p) => {
+ if (p.label.toLowerCase().includes(q)) return true;
+ if (p.module.toLowerCase().includes(q)) return true;
+ return false;
+ });
+ }, [permissions, search]);
+
+ const grouped = useMemo(
+ () => groupByModule(filteredPermissions),
+ [filteredPermissions],
+ );
+
+ const hasChanges = useMemo(
+ () => JSON.stringify(permissions) !== JSON.stringify(data ?? []),
+ [permissions, data],
+ );
+
+ const handleToggle = (
+ permissionId: string,
+ action: keyof PermissionActions,
+ ): void => {
+ setPermissions((prev) =>
+ prev.map((p) => {
+ if (p.permissionId !== permissionId) return p;
+ return {
+ ...p,
+ actions: {
+ ...p.actions,
+ [action]: !p.actions[action],
+ },
+ };
+ }),
+ );
+ };
+
+ const handleToggleCollapse = (module: string): void => {
+ setCollapsed((prev) => {
+ const next = new Set(prev);
+ if (next.has(module)) {
+ next.delete(module);
+ } else {
+ next.add(module);
+ }
+ return next;
+ });
+ };
+
+ const handleSave = async (): Promise => {
+ try {
+ await updateActions(
+ roleId,
+ permissions.map((p) => ({
+ permissionId: p.permissionId,
+ actions: p.actions,
+ })),
+ );
+ notify.success(t("successSaved"));
+ } catch (err) {
+ notify.error(tCommon("error.operationFailed", { message: String(err) }));
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ {tCommon("error.loadFailed", { message: String(error) })}
+
+
+ );
+ }
+
+ if (permissions.length === 0) {
+ return (
+
+ );
+ }
+
+ const showUserImpactAlert = !isLocked && (userCount ?? 0) > 0;
+
+ return (
+
+
+
+
+ {t("title", { roleName })}
+
+ {isLocked ? (
+
+
+ {t("locked")}
+
+ ) : null}
+
+ {t("permissionCount", { count: permissions.length })}
+
+
+ {!isLocked ? (
+
+ ) : null}
+
+
+ {showUserImpactAlert ? (
+
+
+
+ {t("userImpactNotice", { count: userCount ?? 0 })}
+
+
+ ) : null}
+
+
+
+ setSearch(e.target.value)}
+ placeholder={t("searchPlaceholder")}
+ className="pl-9"
+ aria-label={t("searchPlaceholder")}
+ />
+
+
+
+
+
+
+ |
+ {t("colPermission")}
+ |
+ {ACTION_KEYS.map((action) => (
+
+ {t(action.labelKey)}
+ |
+ ))}
+
+
+
+ {grouped.size === 0 ? (
+
+ |
+ {tCommon("empty.searchResult")}
+ |
+
+ ) : (
+ Array.from(grouped.entries()).map(([module, perms]) => (
+
+ ))
+ )}
+
+
+
+
+ );
+}
+
+/**
+ * Matrix group - renders a collapsible module header row and permission rows.
+ */
+function MatrixGroup({
+ module,
+ permissions,
+ isLocked,
+ isCollapsed,
+ onToggleCollapse,
+ onToggle,
+}: {
+ module: string;
+ permissions: RolePermissionMatrixItem[];
+ isLocked: boolean;
+ isCollapsed: boolean;
+ onToggleCollapse: (module: string) => void;
+ onToggle: (permissionId: string, action: keyof PermissionActions) => void;
+}): React.ReactElement {
+ const t = useTranslations("admin.roles.matrix");
+ return (
+ <>
+
+ |
+
+ |
+
+ {!isCollapsed
+ ? permissions.map((perm) => (
+
+ | {perm.label} |
+ {ACTION_KEYS.map((action) => (
+
+ onToggle(perm.permissionId, action.key)}
+ disabled={isLocked}
+ className="size-4 rounded border-input accent-primary"
+ aria-label={`${perm.label} ${t(action.labelKey)}`}
+ />
+ |
+ ))}
+
+ ))
+ : null}
+ >
+ );
+}
+
+/**
+ * Group permissions by module.
+ */
+function groupByModule(
+ permissions: RolePermissionMatrixItem[],
+): Map {
+ const groups = new Map();
+ for (const perm of permissions) {
+ const group = groups.get(perm.module) ?? [];
+ group.push(perm);
+ groups.set(perm.module, group);
+ }
+ return groups;
+}
diff --git a/apps/portal-shell/src/features/admin/roles/roles-list-client.tsx b/apps/portal-shell/src/features/admin/roles/roles-list-client.tsx
index 5245641..4c28162 100644
--- a/apps/portal-shell/src/features/admin/roles/roles-list-client.tsx
+++ b/apps/portal-shell/src/features/admin/roles/roles-list-client.tsx
@@ -13,13 +13,23 @@
*
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
-import { ShieldCheck } from "lucide-react";
+import { Plus, Power, Shield, ShieldCheck, Trash2 } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
-import { useMemo, useTransition } from "react";
+import { useEffect, useMemo, useState, useTransition } from "react";
import { useTranslations } from "next-intl";
-import { useRoles, type Role } from "@/lib/api";
+import {
+ useDeleteRole,
+ useRoles,
+ useToggleRoleEnabled,
+ type Role,
+} from "@/lib/api";
+import { notify } from "@/shared/lib/notify";
+import { Badge } from "@/shared/components/ui/badge";
+import { Button } from "@/shared/components/ui/button";
+import { RoleFormDialog } from "./role-form-dialog";
+import { ConfirmDeleteDialog } from "@/shared/components/ui/confirm-delete-dialog";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { FilterSearchInput } from "@/shared/components/ui/filter-bar";
import {
@@ -29,12 +39,15 @@ import {
import {
countRolePermissions,
formatPermissionCount,
+ formatRoleDate,
formatRoleDescription,
isRoleLocked,
- lockedToBadgeClass,
matchRoleSearch,
} from "@/features/admin/roles/transformations";
+/** 新建角色 URL 参数标记 */
+const NEW_ROLE_PARAM = "new";
+
/**
* 列表客户端主体。需由 server page 包裹在 中
* (useSearchParams 要求 Suspense 边界,Next.js 15 强制)。
@@ -45,6 +58,83 @@ export function RolesListClient(): React.ReactElement {
const router = useRouter();
const searchParams = useSearchParams();
const [, startTransition] = useTransition();
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [editRole, setEditRole] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [pendingId, setPendingId] = useState(null);
+
+ const deleteRole = useDeleteRole();
+ const toggleRoleEnabled = useToggleRoleEnabled();
+
+ const handleEdit = (role: Role): void => {
+ setEditRole(role);
+ setDialogOpen(true);
+ };
+
+ const handleDelete = async (): Promise => {
+ if (!deleteTarget) return;
+ setPendingId(deleteTarget.id);
+ try {
+ await deleteRole.run(deleteTarget.id);
+ notify.success(t("list.deleted"));
+ setDeleteTarget(null);
+ router.refresh();
+ } catch (e) {
+ notify.error(String(e));
+ } finally {
+ setPendingId(null);
+ }
+ };
+
+ const handleToggleEnabled = async (role: Role): Promise => {
+ if (isRoleLocked(role)) {
+ notify.warning(t("list.lockedRoleToggleWarn"));
+ return;
+ }
+ const nextEnabled = !(role.isEnabled ?? true);
+ setPendingId(role.id);
+ try {
+ await toggleRoleEnabled.run(role.id, nextEnabled);
+ notify.success(
+ nextEnabled ? t("list.enabledSuccess") : t("list.disabledSuccess"),
+ );
+ router.refresh();
+ } catch (e) {
+ notify.error(String(e));
+ } finally {
+ setPendingId(null);
+ }
+ };
+
+ // ?new=1 时自动打开创建对话框
+ useEffect(() => {
+ if (searchParams.get(NEW_ROLE_PARAM) === "1" && !dialogOpen) {
+ setEditRole(null);
+ setDialogOpen(true);
+ }
+ // 仅在 new 参数首次进入时触发
+ }, [searchParams]);
+
+ const handleDialogOpenChange = (open: boolean): void => {
+ setDialogOpen(open);
+ if (!open) {
+ // 关闭对话框时清理 URL 上的 new 参数
+ const params = new URLSearchParams(searchParams.toString());
+ if (params.has(NEW_ROLE_PARAM)) {
+ params.delete(NEW_ROLE_PARAM);
+ startTransition(() => {
+ const qs = params.toString();
+ router.push(qs ? `/shell/admin/roles?${qs}` : "/shell/admin/roles");
+ });
+ }
+ }
+ };
+
+ const triggerCreateViaUrl = (): void => {
+ startTransition(() => {
+ router.push("/shell/admin/roles?new=1");
+ });
+ };
const search = searchParams.get("search") ?? "";
@@ -86,55 +176,111 @@ export function RolesListClient(): React.ReactElement {
description={t("list.emptyDescription")}
action={{
label: t("list.emptyAction"),
- // 新建功能未开放,指向当前页占位(避免死链)
- href: "/shell/admin/roles",
+ // 通过 URL 参数触发对话框打开,保证状态可被分享/刷新
+ onClick: triggerCreateViaUrl,
}}
/>
);
return (
- }
- filters={
- updateQuery("search", v)}
+ <>
+ }
+ actions={
+ <>
+
+ {t("list.totalBadge", { count: data?.length ?? 0 })}
+
+
+ >
+ }
+ filters={
+ updateQuery("search", v)}
+ />
+ }
+ loading={loading}
+ loadingNode={}
+ empty={filteredItems.length === 0 && !loading}
+ emptyNode={emptyNode}
+ errorNode={errorNode}
+ >
+ setDeleteTarget(r)}
+ onToggleEnabled={handleToggleEnabled}
+ pendingId={pendingId}
/>
- }
- loading={loading}
- loadingNode={}
- empty={filteredItems.length === 0 && !loading}
- emptyNode={emptyNode}
- errorNode={errorNode}
- >
-
-
+
+
+ !v && setDeleteTarget(null)}
+ title={t("list.deleteConfirmTitle")}
+ description={t("list.deleteConfirmDescription", {
+ name: deleteTarget?.name ?? "",
+ count: deleteTarget?.userCount ?? 0,
+ })}
+ confirmText={t("list.confirmDelete")}
+ cancelText={tCommon("button.cancel")}
+ onConfirm={handleDelete}
+ isWorking={pendingId === deleteTarget?.id}
+ />
+ >
);
}
/**
* 角色列表表格(纯展示组件,对齐 §8.2 排版规范)。
*/
-function RolesTable({ items }: { items: Role[] }): React.ReactElement {
+function RolesTable({
+ items,
+ onEdit,
+ onDelete,
+ onToggleEnabled,
+ pendingId,
+}: {
+ items: Role[];
+ onEdit: (role: Role) => void;
+ onDelete: (role: Role) => void;
+ onToggleEnabled: (role: Role) => void;
+ pendingId: string | null;
+}): React.ReactElement {
const t = useTranslations("admin.roles");
return (
+ {t("list.tableCaption")}
| {t("list.colName")} |
+ {t("list.colValue")} |
{t("list.colDescription")}
|
+ {t("list.colType")} |
+ {t("list.colStatus")} |
- {t("list.colIsLocked")}
+ {t("list.colUserCount")}
|
{t("list.colPermissions")}
|
+
+ {t("list.colUpdatedAt")}
+ |
{t("list.colActions")}
|
@@ -144,18 +290,32 @@ function RolesTable({ items }: { items: Role[] }): React.ReactElement {
{items.map((r) => {
const locked = isRoleLocked(r);
const permCount = countRolePermissions(r);
+ const enabled = r.isEnabled ?? true;
+ const userCount = r.userCount ?? 0;
return (
| {r.name} |
+
+ {r.value ?? "--"}
+ |
{formatRoleDescription(r.description)}
|
-
+
+ |
+
+
+ |
+
+ {t("list.userCountValue", { count: userCount })}
|
{formatPermissionCount(permCount)}
|
+
+ {formatRoleDate(r.updatedAt)}
+ |
{t("list.viewDetail")}
- onEdit(r)}
className="inline-flex h-8 items-center rounded-md border border-input bg-background px-2 text-xs transition-colors hover:bg-accent"
>
- {t("list.editPermissions")}
-
+ {t("list.editRole")}
+
+
+
|
@@ -182,16 +373,39 @@ function RolesTable({ items }: { items: Role[] }): React.ReactElement {
}
/**
- * 系统锁定徽章。
+ * 类型徽章:系统角色显示 ShieldCheck 实心徽章,自定义角色显示 Shield 轮廓徽章。
*/
-function LockedBadge({ locked }: { locked: boolean }): React.ReactElement {
+function TypeBadge({ locked }: { locked: boolean }): React.ReactElement {
const t = useTranslations("admin.roles");
- const cls = lockedToBadgeClass(locked);
+ if (locked) {
+ return (
+
+
+ {t("list.typeSystem")}
+
+ );
+ }
+ return (
+
+
+ {t("list.typeCustom")}
+
+ );
+}
+
+/**
+ * 启用/停用状态徽章。
+ */
+function EnabledBadge({ enabled }: { enabled: boolean }): React.ReactElement {
+ const t = useTranslations("admin.roles");
+ const cls = enabled
+ ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
+ : "bg-muted text-muted-foreground";
return (
- {locked ? t("list.lockedRole") : "--"}
+ {enabled ? t("list.enabled") : t("list.disabled")}
);
}
diff --git a/apps/portal-shell/src/features/admin/roles/transformations.ts b/apps/portal-shell/src/features/admin/roles/transformations.ts
index 5ce688e..0f03606 100644
--- a/apps/portal-shell/src/features/admin/roles/transformations.ts
+++ b/apps/portal-shell/src/features/admin/roles/transformations.ts
@@ -95,6 +95,23 @@ export function formatRoleDescription(
return description;
}
+/**
+ * 格式化角色更新时间 ISO 字符串为本地化展示(zh-CN,含年月日时分)。
+ * 输入无效时返回占位符。
+ */
+export function formatRoleDate(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",
+ });
+}
+
/**
* 模糊匹配角色搜索关键字(按 name / description 命中,大小写不敏感)。
* 关键字为空时返回 true。
diff --git a/apps/portal-shell/src/features/admin/school/academic-year-client.tsx b/apps/portal-shell/src/features/admin/school/academic-year-client.tsx
index fe9107b..70e5f2f 100644
--- a/apps/portal-shell/src/features/admin/school/academic-year-client.tsx
+++ b/apps/portal-shell/src/features/admin/school/academic-year-client.tsx
@@ -13,7 +13,7 @@
*
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
-import { Calendar, Pencil, Plus, Trash2 } from "lucide-react";
+import { Calendar, CheckCircle2, Pencil, Plus, Trash2 } from "lucide-react";
import { useSearchParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useState, useTransition } from "react";
import { useTranslations } from "next-intl";
@@ -29,7 +29,23 @@ import {
type SchoolListItem,
} from "@/lib/api";
import { notify } from "@/shared/lib/notify";
+import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/shared/components/ui/card";
+import { Checkbox } from "@/shared/components/ui/checkbox";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/shared/components/ui/dialog";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { FilterSearchInput } from "@/shared/components/ui/filter-bar";
import { Input } from "@/shared/components/ui/input";
@@ -37,6 +53,14 @@ import {
ListPageShell,
ListPageSkeleton,
} from "@/shared/components/page-templates";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/shared/components/ui/table";
import {
activeToBadgeClass,
formatSchoolDay,
@@ -207,15 +231,21 @@ export function AcademicYearClient(): React.ReactElement {
}
>
- {
- setEditTarget(y);
- setFormOpen(true);
- }}
- onDelete={(y) => setDeleteTarget(y)}
- />
+
+
+ {
+ setEditTarget(y);
+ setFormOpen(true);
+ }}
+ onDelete={(y) => setDeleteTarget(y)}
+ />
+
{formOpen ? (
;
+}): React.ReactElement {
+ const t = useTranslations("admin.school.academicYear");
+ const activeYear = items.find((y) => y.isActive) ?? null;
+
+ if (!activeYear) {
+ return (
+
+
+
+
+ {t("activeYearCardTitle")}
+
+ {t("activeYearCardDescription")}
+
+
+
+
+
+ {t("activeYearCardEmpty")}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {t("activeYearCardTitle")}
+
+ {t("activeYearCardDescription")}
+
+
+
+
+
+
+ {t("colName")}
+
+ {truncateText(activeYear.name)}
+
+
+
+
+ {t("colSchool")}
+
+
+ {schoolNameMap.get(activeYear.schoolId) ?? activeYear.schoolId}
+
+
+
+
+
+ {t("colStartDate")}
+
+
+ {formatSchoolDay(activeYear.startDate)}
+
+
+
+
+ {t("colEndDate")}
+
+
+ {formatSchoolDay(activeYear.endDate)}
+
+
+
+
+
+ {activeYear.isActive ? t("active") : t("inactive")}
+
+
+
+
+
+ );
+}
+
+/**
+ * 学年列表表格卡片(lg:col-span-2),与侧栏激活学年卡片并排展示。
+ */
+function AcademicYearTableCard({
items,
schoolNameMap,
onEdit,
@@ -259,65 +394,85 @@ function AcademicYearTable({
}): React.ReactElement {
const t = useTranslations("admin.school.academicYear");
return (
-
-
-
-
- | {t("colName")} |
- {t("colSchool")} |
- {t("colStartDate")} |
- {t("colEndDate")} |
- {t("colIsActive")} |
- {t("colActions")} |
-
-
-
- {items.map((y) => (
-
- | {truncateText(y.name)} |
-
- {schoolNameMap.get(y.schoolId) ?? y.schoolId}
- |
-
- {formatSchoolDay(y.startDate)}
- |
-
- {formatSchoolDay(y.endDate)}
- |
-
-
- {y.isActive ? t("active") : t("inactive")}
-
- |
-
-
-
-
-
- |
-
- ))}
-
-
-
+
+
+ {t("title")}
+
+ {items.length}
+
+
+
+ {items.length === 0 ? (
+
+ {t("emptyTitle")}
+
+ ) : (
+
+
+
+
+ {t("colName")}
+ {t("colSchool")}
+ {t("colStartDate")}
+ {t("colEndDate")}
+ {t("colIsActive")}
+
+ {t("colActions")}
+
+
+
+
+ {items.map((y) => (
+
+
+ {truncateText(y.name)}
+
+
+ {schoolNameMap.get(y.schoolId) ?? y.schoolId}
+
+
+ {formatSchoolDay(y.startDate)}
+
+
+ {formatSchoolDay(y.endDate)}
+
+
+
+ {y.isActive ? t("active") : t("inactive")}
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
);
}
@@ -373,17 +528,13 @@ function AcademicYearFormDialog({
};
return (
-
+
+
);
}
diff --git a/apps/portal-shell/src/features/admin/school/admin-classes-client.tsx b/apps/portal-shell/src/features/admin/school/admin-classes-client.tsx
index a708825..ff793de 100644
--- a/apps/portal-shell/src/features/admin/school/admin-classes-client.tsx
+++ b/apps/portal-shell/src/features/admin/school/admin-classes-client.tsx
@@ -6,6 +6,7 @@
* 数据契约:
* - 列表查询 adminClasses():❌ schema 无 → MSW 兜底(@contract-pending)
* - 班级 CRUD 契约未就绪,本页为只读列表(@contract-pending)
+ * - 课表 CRUD / 邀请码管理:❌ schema 无 → MSW 兜底(@contract-pending)
*
* URL 状态:?schoolId=&gradeId=&q=
*
@@ -13,24 +14,49 @@
*
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
-import { School } from "lucide-react";
+import { Calendar, Pencil, Plus, School, Ticket, Trash2 } from "lucide-react";
import { useSearchParams, useRouter } from "next/navigation";
-import { useMemo, useTransition } from "react";
+import { useEffect, useMemo, useState, useTransition } from "react";
import { useTranslations } from "next-intl";
import {
useAdminClasses,
+ useCreateAdminClass,
+ useDeleteAdminClass,
+ useUpdateAdminClass,
useGrades,
useSchools,
+ useTeacherOptions,
+ type AdminClassInput,
type AdminClassListItem,
+ type SchoolListItem,
} from "@/lib/api";
+import { notify } from "@/shared/lib/notify";
+import { Button } from "@/shared/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/shared/components/ui/dialog";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { FilterSearchInput } from "@/shared/components/ui/filter-bar";
+import { Input } from "@/shared/components/ui/input";
import {
ListPageShell,
ListPageSkeleton,
} from "@/shared/components/page-templates";
-import { truncateText } from "@/features/admin/school/transformations";
+import {
+ formatSchoolDate,
+ truncateText,
+} from "@/features/admin/school/transformations";
+import {
+ DeleteConfirmDialog,
+ FormField,
+} from "@/features/admin/school/schools-client";
+import { ScheduleManagerDialog } from "@/features/admin/school/class-schedule-dialog";
+import { ClassInvitationManagerDialog } from "@/features/admin/school/class-invitation-manager";
/**
* 班级列表客户端主体。需由 server page 包裹在 中
@@ -48,9 +74,32 @@ export function AdminClassesClient(): React.ReactElement {
const gradeId = searchParams.get("gradeId") ?? "";
// @contract-pending:MSW 兜底
- const { data, loading, error } = useAdminClasses();
+ const { data, loading, error, refetch } = useAdminClasses();
const { data: schools } = useSchools();
const { data: grades } = useGrades();
+ const { data: teacherOptions } = useTeacherOptions();
+ const createMutation = useCreateAdminClass();
+ const updateMutation = useUpdateAdminClass();
+ const deleteMutation = useDeleteAdminClass();
+
+ // 课表管理 / 邀请码管理 / 新建编辑 / 删除对话框状态
+ const [scheduleTarget, setScheduleTarget] =
+ useState(null);
+ const [invitationTarget, setInvitationTarget] =
+ useState(null);
+ const [formOpen, setFormOpen] = useState(false);
+ const [editTarget, setEditTarget] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(
+ null,
+ );
+
+ const teacherNameMap = useMemo
@@ -281,9 +286,15 @@ function DepartmentsTable({
{schoolNameMap.get(d.schoolId) ?? d.schoolId}
{d.headName} |
+
+ {truncateText(d.description ?? "--", 30)}
+ |
{d.memberCount}
|
+
+ {formatSchoolDate(d.updatedAt)}
+ |
|
- notify.info(t("list.mswNotice"))}
>
{t("list.viewDetail")}
-
+
|
))}
diff --git a/apps/portal-shell/src/features/admin/teachers/teachers-list-client.tsx b/apps/portal-shell/src/features/admin/teachers/teachers-list-client.tsx
index dc5e669..4589ef6 100644
--- a/apps/portal-shell/src/features/admin/teachers/teachers-list-client.tsx
+++ b/apps/portal-shell/src/features/admin/teachers/teachers-list-client.tsx
@@ -14,19 +14,20 @@
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
import { School } from "lucide-react";
-import Link from "next/link";
import { useSearchParams, useRouter } from "next/navigation";
import { useMemo, useTransition } from "react";
import { useTranslations } from "next-intl";
import { useAdminTeachers, useDepartments } from "@/lib/api/admin-p5";
import type { AdminTeacher } from "@/lib/api/admin-p5";
+import { Button } from "@/shared/components/ui/button";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { FilterSearchInput } from "@/shared/components/ui/filter-bar";
import {
ListPageShell,
ListPageSkeleton,
} from "@/shared/components/page-templates";
+import { notify } from "@/shared/lib/notify";
import {
formatTeacherStatus,
matchTeacherSearch,
@@ -242,12 +243,13 @@ function TeachersTable({
{tc.classCount} |
- notify.info(t("list.mswNotice"))}
>
{t("list.viewDetail")}
-
+
|
))}
diff --git a/apps/portal-shell/src/features/admin/users/user-role-assign-dialog.tsx b/apps/portal-shell/src/features/admin/users/user-role-assign-dialog.tsx
new file mode 100644
index 0000000..2b090b6
--- /dev/null
+++ b/apps/portal-shell/src/features/admin/users/user-role-assign-dialog.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+/**
+ * 用户多角色分配对话框(ARCHITECTURE.md §9.4 B5 / §11.3)
+ *
+ * 数据契约:
+ * - mutation assignUserRoles(userId, roleNames) ❌ schema 无 → MSW 兜底(@contract-pending)
+ * - 替换语义:传入完整角色名数组,覆盖原有角色
+ *
+ * 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
+ * 对齐 CICD:src/modules/rbac/components/user-role-assign-dialog.tsx
+ */
+import { UserCog } from "lucide-react";
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+
+import { useAssignUserRoles } from "@/lib/api";
+import { Button } from "@/shared/components/ui/button";
+import { notify } from "@/shared/lib/notify";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/shared/components/ui/dialog";
+import { Alert, AlertDescription } from "@/shared/components/ui/alert";
+
+/** 可分配角色条目(与 roles 列表对齐) */
+export interface AssignableRole {
+ id: string;
+ name: string;
+ description: string;
+ isSystem: boolean;
+ isEnabled: boolean;
+}
+
+export interface UserRoleAssignDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ userId: string;
+ userName: string;
+ userEmail: string;
+ /** 所有可用角色(仅 isEnabled=true 的可分配) */
+ allRoles: AssignableRole[];
+ /** 当前用户已分配的角色名列表 */
+ currentRoleNames: string[];
+}
+
+/**
+ * 多角色分配对话框。展示所有启用角色的复选框列表,
+ * 保存时按替换语义覆盖原有角色。
+ */
+export function UserRoleAssignDialog({
+ open,
+ onOpenChange,
+ userId,
+ userName,
+ userEmail,
+ allRoles,
+ currentRoleNames,
+}: UserRoleAssignDialogProps): React.ReactElement | null {
+ const t = useTranslations("admin.users.assignDialog");
+ const tCommon = useTranslations("common");
+ const router = useRouter();
+ const assignRoles = useAssignUserRoles();
+
+ const [selected, setSelected] = useState>(
+ new Set(currentRoleNames),
+ );
+
+ // 打开时同步当前角色
+ useEffect(() => {
+ if (open) {
+ setSelected(new Set(currentRoleNames));
+ }
+ }, [open, currentRoleNames]);
+
+ const handleToggle = (roleName: string, checked: boolean): void => {
+ setSelected((prev) => {
+ const next = new Set(prev);
+ if (checked) {
+ next.add(roleName);
+ } else {
+ next.delete(roleName);
+ }
+ return next;
+ });
+ };
+
+ const handleSave = async (): Promise => {
+ try {
+ await assignRoles.run(userId, Array.from(selected));
+ notify.success(t("success"));
+ onOpenChange(false);
+ router.refresh();
+ } catch (err) {
+ notify.error(tCommon("error.operationFailed", { message: String(err) }));
+ }
+ };
+
+ // 仅启用的角色可分配;禁用但已分配的展示在底部
+ const enabledRoles = allRoles.filter((r) => r.isEnabled);
+ const disabledAssignedRoles = allRoles.filter(
+ (r) => !r.isEnabled && selected.has(r.name),
+ );
+
+ if (!open) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/users/users-import-client.tsx b/apps/portal-shell/src/features/admin/users/users-import-client.tsx
index 8f5c6b5..79ddc59 100644
--- a/apps/portal-shell/src/features/admin/users/users-import-client.tsx
+++ b/apps/portal-shell/src/features/admin/users/users-import-client.tsx
@@ -11,7 +11,14 @@
*
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
-import { CheckCircle2, Download, Upload, XCircle } from "lucide-react";
+import {
+ CheckCircle2,
+ Download,
+ Loader2,
+ RotateCcw,
+ Upload,
+ XCircle,
+} from "lucide-react";
import { useState } from "react";
import { useTranslations } from "next-intl";
@@ -50,6 +57,9 @@ const TEMPLATE_HEADERS = [
"inviteCode",
] as const;
+/** 预览最大行数 */
+const PREVIEW_MAX_ROWS = 50;
+
/**
* 生成并下载 CSV 模板(纯客户端,无契约依赖)。
*/
@@ -66,6 +76,28 @@ function downloadTemplate(): void {
URL.revokeObjectURL(url);
}
+/**
+ * 解析文件文本为预览行(仅前 PREVIEW_MAX_ROWS 行)。
+ * 支持 CSV 简易分割;Excel 二进制文件回退为按行文本展示。
+ */
+function parsePreviewRows(file: File): Promise {
+ return new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const text = String(reader.result ?? "");
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "");
+ const rows = lines.slice(0, PREVIEW_MAX_ROWS).map((line) =>
+ // 简易 CSV 分割,不处理引号嵌套(预览用途足够)
+ line.split(",").map((cell) => cell.trim()),
+ );
+ resolve(rows);
+ };
+ reader.onerror = () => resolve([]);
+ // Excel 文件读取为文本会有乱码,但仅用于展示前几行结构
+ reader.readAsText(file);
+ });
+}
+
/**
* 导入客户端主体。
*/
@@ -74,6 +106,8 @@ export function UsersImportClient(): React.ReactElement {
const { run, loading } = useImportUsers();
const [selectedFile, setSelectedFile] = useState(null);
const [result, setResult] = useState(null);
+ const [previewRows, setPreviewRows] = useState([]);
+ const [importing, setImporting] = useState(false);
const handleDownload = (): void => {
try {
@@ -89,19 +123,43 @@ export function UsersImportClient(): React.ReactElement {
const file = e.target.files?.[0];
if (!file) return;
setSelectedFile(file);
+ setResult(null);
try {
- const r = await run(file);
- setResult(r);
- notify.success(t("imported"));
- } catch (e) {
- notify.error(String(e));
+ const rows = await parsePreviewRows(file);
+ setPreviewRows(rows);
+ } catch {
+ setPreviewRows([]);
} finally {
// 允许重复选择同一文件触发 onChange
e.target.value = "";
}
};
+ const handleReselect = (): void => {
+ setSelectedFile(null);
+ setResult(null);
+ setPreviewRows([]);
+ };
+
+ const handleConfirmImport = async (): Promise => {
+ if (!selectedFile) return;
+ setImporting(true);
+ try {
+ const r = await run(selectedFile);
+ setResult(r);
+ setPreviewRows([]);
+ notify.success(t("imported"));
+ } catch (e) {
+ notify.error(String(e));
+ } finally {
+ setImporting(false);
+ }
+ };
+
const steps = [t("step1"), t("step2"), t("step3"), t("step4")];
+ const showPreview =
+ selectedFile && previewRows.length > 0 && !result && !importing;
+ const showImporting = importing || (loading && !!selectedFile && !result);
return (
@@ -187,25 +245,81 @@ export function UsersImportClient(): React.ReactElement {
{t("uploadButton")}
{t("uploadHint")}
{selectedFile ? (
-
- {selectedFile.name}({selectedFile.size} bytes)
-
+
+
+ {selectedFile.name}({selectedFile.size} bytes)
+
+
+
+ {t("reselect")}
+
+
) : null}
- {loading ? (
- ...
+ {showImporting ? (
+
+
+ {t("importStatus")}
+
) : null}
+ {/* 文件预览(前 50 行) */}
+ {showPreview ? (
+
+
+
+ {t("filesPreview", { count: previewRows.length })}
+
+
+
+
+
+
+ {previewRows.map((row, idx) => (
+
+ {row.map((cell, j) => (
+ |
+ {cell}
+ |
+ ))}
+
+ ))}
+
+
+
+
+
+
+ {t("confirmImport")}
+
+
+
+
+ ) : null}
+
{/* 步骤 4:查看结果 */}
{result ? (
diff --git a/apps/portal-shell/src/features/admin/users/users-list-client.tsx b/apps/portal-shell/src/features/admin/users/users-list-client.tsx
index ff010b4..7d24d85 100644
--- a/apps/portal-shell/src/features/admin/users/users-list-client.tsx
+++ b/apps/portal-shell/src/features/admin/users/users-list-client.tsx
@@ -5,7 +5,7 @@
*
* 数据契约:
* - 列表查询 users(role, limit, offset) ❌ schema 无此字段 → MSW 兜底(@contract-pending)
- * - 变更 updateUserStatus / updateUserRole ❌ schema 无 → MSW 兜底(@contract-pending)
+ * - 变更 updateUserStatus / updateUserRole / deleteUser / assignUserRoles ❌ schema 无 → MSW 兜底(@contract-pending)
* - 契约工单:docs/architecture/issues/contracts/iam_contract.md#users
*
* URL 状态:?search=&role=&page=
@@ -13,25 +13,49 @@
* 三态规范(§11.3 DoD):loading(骨架)/ error(局部降级)/ empty(EmptyState + 行动按钮)
*
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
+ * 对齐 CICD:src/modules/users/components/admin-users-view.tsx
*/
-import { Users } from "lucide-react";
+import {
+ Users,
+ Upload,
+ MoreHorizontal,
+ Trash2,
+ Pencil,
+ UserCog,
+ RotateCcw,
+} from "lucide-react";
+import Link from "next/link";
import { useSearchParams, useRouter } from "next/navigation";
import { useMemo, useState, useTransition } from "react";
import { useTranslations } from "next-intl";
import {
+ useDeleteUser,
+ useRoles,
useUpdateUserRole,
useUpdateUserStatus,
useUsers,
type User,
} from "@/lib/api";
import { notify } from "@/shared/lib/notify";
+import { Button } from "@/shared/components/ui/button";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { FilterSearchInput } from "@/shared/components/ui/filter-bar";
import {
ListPageShell,
ListPageSkeleton,
} from "@/shared/components/page-templates";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/shared/components/ui/dropdown-menu";
+import { ConfirmDeleteDialog } from "@/shared/components/ui/confirm-delete-dialog";
+import {
+ UserRoleAssignDialog,
+ type AssignableRole,
+} from "@/features/admin/users/user-role-assign-dialog";
import {
USER_ROLE_OPTIONS,
formatUserDate,
@@ -71,15 +95,35 @@ export function UsersListClient(): React.ReactElement {
offset,
});
+ // 拉取角色列表供多角色分配对话框使用
+ const { data: rolesData } = useRoles();
+
const updateUserStatus = useUpdateUserStatus();
const updateUserRole = useUpdateUserRole();
+ const deleteUser = useDeleteUser();
const [pendingId, setPendingId] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [assignTarget, setAssignTarget] = useState(null);
const filteredItems = useMemo(() => {
const items = data?.items ?? [];
return items.filter((u) => matchUserSearch(u, search));
}, [data, search]);
+ // 将 roles 数据映射为 AssignableRole(兼容旧 Role 结构)
+ const assignableRoles: AssignableRole[] = useMemo(() => {
+ if (!rolesData) {
+ return [];
+ }
+ return rolesData.map((r) => ({
+ id: r.id,
+ name: r.name,
+ description: r.description ?? "",
+ isSystem: r.isLocked ?? false,
+ isEnabled: true,
+ }));
+ }, [rolesData]);
+
const total = data?.total ?? filteredItems.length;
const hasNext = page * PAGE_SIZE < total;
const hasPrev = page > 1;
@@ -111,6 +155,12 @@ export function UsersListClient(): React.ReactElement {
});
};
+ const handleReset = (): void => {
+ startTransition(() => {
+ router.push("/shell/admin/users");
+ });
+ };
+
const handleRoleChange = async (
user: User,
newRole: string,
@@ -140,6 +190,21 @@ export function UsersListClient(): React.ReactElement {
}
};
+ const handleDelete = async (): Promise => {
+ if (!deleteTarget) return;
+ setPendingId(deleteTarget.id);
+ try {
+ await deleteUser.run(deleteTarget.id);
+ notify.success(t("list.deleted"));
+ setDeleteTarget(null);
+ router.refresh();
+ } catch (e) {
+ notify.error(String(e));
+ } finally {
+ setPendingId(null);
+ }
+ };
+
const errorNode = error ? (
@@ -168,6 +233,14 @@ export function UsersListClient(): React.ReactElement {
title={t("list.title")}
description={t("list.description")}
icon={}
+ actions={
+
+
+
+ {t("list.importButton")}
+
+
+ }
filters={
<>
))}
+ {(search || role) && (
+
+
+ {t("list.reset")}
+
+ )}
>
}
loading={loading}
@@ -197,7 +281,7 @@ export function UsersListClient(): React.ReactElement {
errorNode={errorNode}
pagination={
- {t("list.total", { count: filteredItems.length })}
+ {t("list.total", { count: total })}
setAssignTarget(u)}
+ onDelete={(u) => setDeleteTarget(u)}
/>
+
+ !v && setDeleteTarget(null)}
+ title={t("list.deleteConfirmTitle")}
+ description={t("list.deleteConfirmDescription", {
+ name: deleteTarget?.name ?? "",
+ email: deleteTarget?.email ?? "",
+ })}
+ confirmText={t("list.confirmDelete")}
+ cancelText={tCommon("button.cancel")}
+ onConfirm={handleDelete}
+ isWorking={pendingId === deleteTarget?.id}
+ />
+
+ {assignTarget && assignableRoles.length > 0 ? (
+ !v && setAssignTarget(null)}
+ userId={assignTarget.id}
+ userName={assignTarget.name}
+ userEmail={assignTarget.email}
+ allRoles={assignableRoles}
+ currentRoleNames={
+ assignTarget.roles && assignTarget.roles.length > 0
+ ? assignTarget.roles
+ : [assignTarget.role]
+ }
+ />
+ ) : null}
);
}
/**
- * 用户列表表格(纯展示组件,对齐 §8.2 排版规范)。
+ * 用户列表表格(含下拉菜单操作)。
*/
function UsersTable({
items,
pendingId,
onRoleChange,
onToggleStatus,
+ onAssignRoles,
+ onDelete,
}: {
items: User[];
pendingId: string | null;
onRoleChange: (user: User, newRole: string) => void;
onToggleStatus: (user: User) => void;
+ onAssignRoles: (user: User) => void;
+ onDelete: (user: User) => void;
}): React.ReactElement {
const t = useTranslations("admin.users");
+ const router = useRouter();
return (
@@ -253,60 +374,112 @@ function UsersTable({
| {t("list.colName")} |
{t("list.colEmail")} |
{t("list.colRole")} |
+ {t("list.colPhone")} |
{t("list.colStatus")} |
{t("list.colCreatedAt")}
|
+
+ {t("list.colUpdatedAt")}
+ |
{t("list.colActions")}
|
- {items.map((u) => (
-
- | {truncateUserName(u.name)} |
-
- {u.email}
- |
-
-
- |
-
-
- |
-
- {formatUserDate(u.createdAt)}
- |
-
-
-
+ |
+
+ {u.phone ?? "-"}
+ |
+
+
+ |
+
+ {formatUserDate(u.createdAt)}
+ |
+
+ {formatUserDate(u.updatedAt)}
+ |
+
+
+
+
+
+
+
+
+
+ router.push(`/shell/admin/users/${u.id}`)
+ }
+ >
+
+ {t("list.editUser")}
+
+ onToggleStatus(u)}
+ disabled={pendingId === u.id}
+ >
+
+ {isUserActive(u)
+ ? t("list.deactivate")
+ : t("list.activate")}
+
+ onAssignRoles(u)}
+ disabled={pendingId === u.id}
+ >
+
+ {t("list.assignRoles")}
+
+ onRoleChange(u, "teacher")}
+ disabled={pendingId === u.id}
+ >
+
+ {t("list.quickSetTeacher")}
+
+ onDelete(u)}
+ disabled={pendingId === u.id}
+ className="text-destructive"
+ >
+
+ {t("list.delete")}
+
+
+
+ |
+
+ );
+ })}
@@ -342,3 +515,25 @@ function StatusBadge({ status }: { status: string }): React.ReactElement {
);
}
+
+/**
+ * 用户类型徽章(internal/external)。
+ */
+function UserTypeBadge({ userType }: { userType: string }): React.ReactElement {
+ const t = useTranslations("admin.users");
+ const isInternal = userType === "internal";
+ const label = isInternal
+ ? t("list.userTypeInternal")
+ : t("list.userTypeExternal");
+ const cls = isInternal
+ ? "bg-primary/10 text-primary"
+ : "bg-muted text-muted-foreground";
+ return (
+
+ {label}
+
+ );
+}
diff --git a/apps/portal-shell/src/features/admin/viewports/viewports-client.tsx b/apps/portal-shell/src/features/admin/viewports/viewports-client.tsx
index d3f86bb..6ea25db 100644
--- a/apps/portal-shell/src/features/admin/viewports/viewports-client.tsx
+++ b/apps/portal-shell/src/features/admin/viewports/viewports-client.tsx
@@ -245,6 +245,7 @@ function ViewportEditDialog({
onSave: (input: ViewportInput) => Promise;
}): React.ReactElement {
const t = useTranslations("admin.viewports.list");
+ const tCommon = useTranslations("common");
const [name, setName] = useState("");
const [route, setRoute] = useState("");
@@ -332,7 +333,7 @@ function ViewportEditDialog({
- {tCommon0("button.cancel")}
+ {tCommon("button.cancel")}
{t("saveButton")}
@@ -344,17 +345,6 @@ function ViewportEditDialog({
);
}
-/**
- * 模拟 useTranslations("common") 在编辑对话框内的访问。
- * 注:因该组件嵌套较深,使用辅助函数避免在子组件内重复声明。
- */
-function tCommon0(key: string): string {
- // 简化处理:直接返回 key 的最后一段作为兜底文案。
- // 真实场景应通过 useTranslations("common") 获取。
- void key;
- return "取消";
-}
-
/**
* 表单字段容器(label + children)。
*/
diff --git a/apps/portal-shell/src/lib/api/admin-p5.ts b/apps/portal-shell/src/lib/api/admin-p5.ts
index 438a8ba..5c3aec1 100644
--- a/apps/portal-shell/src/lib/api/admin-p5.ts
+++ b/apps/portal-shell/src/lib/api/admin-p5.ts
@@ -22,14 +22,17 @@ import { useWidgetQuery } from "@/lib/useWidgetQuery";
import { useWidgetMutation } from "@/lib/useWidgetMutation";
import { ApiError } from "./errors";
import type { PaginatedResult, Pagination, UseQueryResult } from "./types";
-import type { Role, SchoolInput } from "./admin";
+import type { AuditLogFilter, Role, SchoolInput } from "./admin";
import {
IMPORT_USERS_DOC,
GET_ROLE_DOC,
CREATE_ROLE_DOC,
UPDATE_ROLE_DOC,
DELETE_ROLE_DOC,
+ TOGGLE_ROLE_ENABLED_DOC,
GET_PERMISSION_ROLE_COUNTS_DOC,
+ GET_ROLE_PERMISSIONS_DOC,
+ UPDATE_ROLE_PERMISSION_ACTIONS_DOC,
GET_AUDIT_OVERVIEW_STATS_DOC,
GET_AUDIT_TREND_DOC,
GET_DATA_CHANGE_ACTION_STATS_DOC,
@@ -38,8 +41,12 @@ import {
GET_DATA_CHANGE_TABLE_OPTIONS_DOC,
GET_DATA_CHANGE_STATS_DOC,
GET_AUDIT_MODULE_OPTIONS_DOC,
+ EXPORT_AUDIT_LOGS_DOC,
+ EXPORT_LOGIN_LOGS_DOC,
+ EXPORT_DATA_CHANGES_DOC,
GET_SCHOOLS_DOC,
CREATE_SCHOOL_DOC,
+ ADMIN_UPDATE_SCHOOL_DOC,
DELETE_SCHOOL_DOC,
GET_DEPARTMENTS_DOC,
CREATE_DEPARTMENT_DOC,
@@ -55,6 +62,9 @@ import {
UPDATE_GRADE_DOC,
DELETE_GRADE_DOC,
GET_ADMIN_CLASSES_DOC,
+ CREATE_ADMIN_CLASS_DOC,
+ UPDATE_ADMIN_CLASS_DOC,
+ DELETE_ADMIN_CLASS_DOC,
GET_TEACHER_OPTIONS_DOC,
GET_STAFF_OPTIONS_DOC,
GET_ADMIN_ANNOUNCEMENTS_DOC,
@@ -64,12 +74,16 @@ import {
DELETE_ANNOUNCEMENT_DOC,
ARCHIVE_ANNOUNCEMENT_DOC,
PIN_ANNOUNCEMENT_DOC,
+ PUBLISH_ANNOUNCEMENT_DOC,
GET_FILE_ATTACHMENTS_DOC,
GET_FILE_STATS_DOC,
+ UPLOAD_FILE_DOC,
+ BATCH_DELETE_FILES_DOC,
GET_AI_PROVIDERS_DOC,
CREATE_AI_PROVIDER_DOC,
UPDATE_AI_PROVIDER_DOC,
DELETE_AI_PROVIDER_DOC,
+ TEST_AI_PROVIDER_DOC,
GET_AI_USAGE_DASHBOARD_DOC,
GET_SYSTEM_SETTINGS_DOC,
UPDATE_SYSTEM_SETTINGS_DOC,
@@ -85,9 +99,11 @@ import {
GET_ADMIN_ELECTIVES_DOC,
GET_ADMIN_ELECTIVE_DOC,
GET_ADMIN_QUESTIONS_DOC,
+ GET_ADMIN_QUESTION_DOC,
GET_ADMIN_LESSON_PLANS_DOC,
GET_ADMIN_LESSON_PLAN_DOC,
GET_ADMIN_ERROR_BOOK_STATS_DOC,
+ EXPORT_ERROR_BOOK_CSV_DOC,
GET_ADMIN_SCHEDULE_CHANGES_DOC,
GET_ADMIN_SCHEDULE_ENTRIES_DOC,
ADMIN_GET_SCHEDULING_RULES_DOC,
@@ -98,7 +114,28 @@ import {
GET_ADMIN_ATTENDANCE_STATS_DOC,
GET_ADMIN_ATTENDANCE_RECORDS_DOC,
GET_ATTENDANCE_GRADE_CORRELATION_DOC,
+ GET_AUDIT_RETENTION_CONFIG_DOC,
+ SAVE_AUDIT_RETENTION_CONFIG_DOC,
+ PURGE_EXPIRED_AUDIT_LOGS_DOC,
+ CREATE_COURSE_PLAN_ITEM_DOC,
+ UPDATE_COURSE_PLAN_ITEM_DOC,
+ DELETE_COURSE_PLAN_ITEM_DOC,
+ TOGGLE_COURSE_PLAN_ITEM_COMPLETED_DOC,
+ REORDER_COURSE_PLAN_ITEMS_DOC,
+ DELETE_COURSE_PLAN_DOC,
+ BULK_TOGGLE_COURSE_PLAN_ITEMS_DOC,
+ DELETE_ELECTIVE_DOC,
+ OPEN_ELECTIVE_SELECTION_DOC,
+ CLOSE_ELECTIVE_SELECTION_DOC,
+ RUN_ELECTIVE_LOTTERY_DOC,
+ GET_ELECTIVE_OVERVIEW_STATS_DOC,
} from "./operations/admin.graphql";
+import {
+ CREATE_ELECTIVE_DOC,
+ UPDATE_ELECTIVE_DOC,
+} from "./operations/elective.graphql";
+
+import { SOFT_DELETE_LESSON_PLAN_DOC } from "./operations/lesson-plans.graphql";
// ============================================================
// Types: Users import
@@ -122,6 +159,41 @@ export interface RoleInput {
name: string;
description?: string;
permissionIds: string[];
+ /**
+ * 角色值(如 `role:teacher`,@contract-pending:schema 未就绪,MSW 兜底)。
+ * 缺省视为未知,UI 用 `-` 占位。
+ */
+ value?: string;
+}
+
+// ============================================================
+// Types: RBAC role permission matrix (CRUD actions per permission point)
+// ============================================================
+export interface PermissionActions {
+ read: boolean;
+ create: boolean;
+ update: boolean;
+ delete: boolean;
+}
+
+/**
+ * 权限矩阵中的一个权限点条目(按模块分组,每行一个权限点,列为 CRUD 动作)。
+ *
+ * 命名说明:admin.ts 已定义 RolePermission(id/name/resource/action,
+ * 用于 Role.permissions 列表)。本类型为矩阵视图的独立结构,故显式命名为
+ * RolePermissionMatrixItem 以避免 barrel 导出冲突。
+ */
+export interface RolePermissionMatrixItem {
+ permissionId: string;
+ label: string;
+ module: string;
+ actions: PermissionActions;
+}
+
+// Input for UpdateRolePermissionActions mutation (one entry per permission point)
+export interface PermissionActionUpdate {
+ permissionId: string;
+ actions: PermissionActions;
}
// Role/RolePermission/Permission 类型由 ./admin.ts 提供(barrel 统一导出)
@@ -142,6 +214,12 @@ export interface AuditOverviewStats {
totalToday: number;
totalErrors: number;
totalUsers: number;
+ /** @contract-pending 今日审计事件数(与 totalToday 含义区分,强调事件维度) */
+ auditEventsToday: number;
+ /** @contract-pending 今日失败登录次数(用于失败登录卡片高亮) */
+ failedLoginsToday: number;
+ /** @contract-pending 今日数据变更次数 */
+ dataChangesToday: number;
}
export interface AuditTrendPoint {
@@ -163,6 +241,8 @@ export interface LoginLog {
ip: string;
userAgent: string;
timestamp: string;
+ /** @contract-pending 失败原因(仅 status=failure/error 时有值),MSW 兜底 */
+ errorMessage?: string | null;
}
export interface LoginLogFilter {
@@ -182,6 +262,10 @@ export interface DataChangeLog {
userName: string;
changes: string;
timestamp: string;
+ /** @contract-pending 变更前快照(JSON 字符串或对象),MSW 兜底,用于行内展开对比 */
+ oldValue?: string | null;
+ /** @contract-pending 变更后快照(JSON 字符串或对象),MSW 兜底,用于行内展开对比 */
+ newValue?: string | null;
}
export interface DataChangeLogFilter {
@@ -198,6 +282,17 @@ export interface DataChangeStat {
lastChangeAt: string;
}
+/**
+ * 导出结果(CSV 字符串由 MSW 用 transformations 纯函数构造)。
+ * @contract-pending schema 未就绪,MSW 兜底返回 csv 字符串。
+ */
+export interface ExportResult {
+ success: boolean;
+ count: number;
+ filename: string;
+ csv: string;
+}
+
// ============================================================
// Types: School CRUD
// ============================================================
@@ -209,6 +304,10 @@ export interface SchoolListItem {
email: string;
currentAcademicYear: string;
currentTerm: string;
+ /** 学校编码(@contract-pending,MSW 兜底) */
+ code?: string;
+ /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */
+ updatedAt?: string;
}
export interface Department {
@@ -218,12 +317,17 @@ export interface Department {
headId: string;
headName: string;
memberCount: number;
+ /** 部门描述(@contract-pending,MSW 兜底) */
+ description?: string;
+ /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */
+ updatedAt?: string;
}
export interface DepartmentInput {
name: string;
schoolId: string;
headId?: string;
+ description?: string;
}
export interface AcademicYear {
@@ -252,6 +356,14 @@ export interface AdminGradeListItem {
headStaffName: string;
classCount: number;
studentCount: number;
+ /** 年级序号(@contract-pending,MSW 兜底) */
+ order?: number;
+ /** 教学主任 ID(@contract-pending,MSW 兜底) */
+ teachingHead?: string;
+ /** 教学主任姓名(@contract-pending,MSW 兜底) */
+ teachingHeadName?: string;
+ /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */
+ updatedAt?: string;
}
export interface GradeInput {
@@ -279,6 +391,29 @@ export interface AdminClassListItem {
headTeacherName: string;
studentCount: number;
subjectCount: number;
+ /** 班号(@contract-pending,MSW 兜底) */
+ homeroomLabel?: string;
+ /** 教室(@contract-pending,MSW 兜底) */
+ room?: string;
+ /** 班主任 ID(@contract-pending,MSW 兜底) */
+ homeroom?: string;
+ /** 班主任姓名(@contract-pending,MSW 兜底) */
+ homeroomName?: string;
+ /** 任课教师列表(@contract-pending,MSW 兜底) */
+ subjectTeachers?: string;
+ /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */
+ updatedAt?: string;
+}
+
+/** 班级 CRUD 输入类型(@contract-pending,MSW 兜底) */
+export interface AdminClassInput {
+ name: string;
+ gradeId: string;
+ schoolId: string;
+ headTeacherId?: string;
+ homeroomLabel?: string;
+ room?: string;
+ homeroom?: string;
}
export interface OptionItem {
@@ -300,6 +435,7 @@ export interface AnnouncementListItem {
createdAt: string;
updatedAt: string;
authorName: string;
+ readCount: number;
}
export interface AnnouncementDetail extends AnnouncementListItem {
@@ -340,6 +476,23 @@ export interface FileStats {
byType: Record;
}
+export interface UploadFileInput {
+ filename: string;
+ mimeType: string;
+ size: number;
+}
+
+export interface UploadFileResult {
+ id: string;
+ url: string;
+ filename: string;
+}
+
+export interface BatchDeleteFilesResult {
+ success: boolean;
+ deletedCount: number;
+}
+
// ============================================================
// Types: AI settings
// ============================================================
@@ -350,6 +503,8 @@ export interface AiProvider {
scope: string;
ownerId: string;
isActive: boolean;
+ isDefault: boolean;
+ visibility: string;
model: string;
apiBase: string;
config: Record;
@@ -363,6 +518,8 @@ export interface AiProviderInput {
model: string;
apiBase?: string;
isActive?: boolean;
+ isDefault?: boolean;
+ visibility?: string;
config?: Record;
}
@@ -485,11 +642,79 @@ export interface AdminCoursePlanListItem {
academicYearName: string;
status: string;
createdAt: string;
+ /** 学期(@contract-pending,MSW 兜底,对齐 CICD 列表"学期"列) */
+ semester?: string;
+ /** 总课时(@contract-pending,MSW 兜底,用于进度条 totalHours) */
+ totalHours?: number;
+ /** 已完成课时(@contract-pending,MSW 兜底,用于进度条 completedHours) */
+ completedHours?: number;
+ /** 更新时间 ISO 字符串(@contract-pending,MSW 兜底) */
+ updatedAt?: string;
+}
+
+/** 管理端课程计划周计划项(@contract-pending,MSW 兜底) */
+export interface AdminCoursePlanItem {
+ id: string;
+ planId: string;
+ week: number;
+ topic: string;
+ content: string | null;
+ hours: number;
+ textbookChapter: string | null;
+ notes: string | null;
+ isCompleted: boolean;
+ completedAt: string | null;
+ createdAt: string;
+ updatedAt: string;
}
export interface AdminCoursePlan extends AdminCoursePlanListItem {
content: string;
+ items: AdminCoursePlanItem[];
updatedAt: string;
+ /** 教学大纲(@contract-pending,MSW 兜底,对齐 CICD 详情"教学大纲"区) */
+ syllabus?: string;
+ /** 教学目标(@contract-pending,MSW 兜底,对齐 CICD 详情"教学目标"区) */
+ objectives?: string;
+ /** 周课时(@contract-pending,MSW 兜底) */
+ weeklyHours?: number;
+ /** 计划开始日期(@contract-pending,MSW 兜底) */
+ startDate?: string;
+ /** 计划结束日期(@contract-pending,MSW 兜底) */
+ endDate?: string;
+ /** 教材链接(@contract-pending,MSW 兜底) */
+ textbooksHref?: string;
+ /** 作业链接(@contract-pending,MSW 兜底) */
+ homeworkHref?: string;
+}
+
+/** 创建/更新周计划项的输入类型(@contract-pending) */
+export interface AdminCoursePlanItemInput {
+ planId: string;
+ week: number;
+ topic: string;
+ content?: string | null;
+ hours?: number;
+ textbookChapter?: string | null;
+ notes?: string | null;
+ completedAt?: string | null;
+}
+
+/** 更新周计划项的输入类型(不含 planId,@contract-pending) */
+export interface AdminCoursePlanItemUpdateInput {
+ week?: number;
+ topic?: string;
+ content?: string | null;
+ hours?: number;
+ textbookChapter?: string | null;
+ notes?: string | null;
+ completedAt?: string | null;
+}
+
+/** 重排序周计划项的输入条目(@contract-pending) */
+export interface ReorderCoursePlanItemInput {
+ id: string;
+ week: number;
}
export interface StandardsCoverageCell {
@@ -499,6 +724,13 @@ export interface StandardsCoverageCell {
gradeName: string;
coverageRate: number;
lessonPlanCount: number;
+ /**
+ * 该标准 × 年级下已关联教案的课时数(@contract-pending,MSW 兜底)。
+ * 与 lessonPlanCount 区分:lessonPlanCount 为教案条目数,total 为应覆盖总课时数。
+ */
+ total: number;
+ /** 已关联教案数别名(与 lessonPlanCount 同义,对齐 CICD linked/total 口径) */
+ linked?: number;
}
export interface GlobalLessonPlanStats {
@@ -521,6 +753,30 @@ export interface AdminElectiveListItem {
capacity: number;
selectedCount: number;
status: string;
+ /** 教室(@contract-pending,MSW 兜底,对齐 CICD 列表"教室"列) */
+ classroom?: string;
+ /** 上课时间(@contract-pending,MSW 兜底,对齐 CICD 列表"时间"列) */
+ schedule?: string;
+ /** 学分(@contract-pending,MSW 兜底,对齐 CICD 列表"学分"列) */
+ credit?: number;
+ /** 选课模式(@contract-pending,MSW 兜底:FIRST_COME | LOTTERY) */
+ selectionMode?: string;
+ /** 选课开始时间 ISO(@contract-pending,MSW 兜底) */
+ selectionStartAt?: string;
+ /** 选课结束时间 ISO(@contract-pending,MSW 兜底) */
+ selectionEndAt?: string;
+ /** 退课截止时间 ISO(@contract-pending,MSW 兜底) */
+ dropDeadline?: string;
+ /** 描述(@contract-pending,MSW 兜底,列表展示用 line-clamp) */
+ description?: string;
+ /** 开始日期(@contract-pending,MSW 兜底) */
+ startDate?: string;
+ /** 结束日期(@contract-pending,MSW 兜底) */
+ endDate?: string;
+ /** 创建时间 ISO(@contract-pending,MSW 兜底) */
+ createdAt?: string;
+ /** 更新时间 ISO(@contract-pending,MSW 兜底) */
+ updatedAt?: string;
}
export interface AdminElective extends AdminElectiveListItem {
@@ -529,9 +785,104 @@ export interface AdminElective extends AdminElectiveListItem {
studentId: string;
studentName: string;
selectedAt: string;
+ /** 选课状态(@contract-pending,MSW 兜底:confirmed | pending | cancelled) */
+ status?: string;
+ /** 优先级(@contract-pending,MSW 兜底,对齐 CICD 详情"优先级"列) */
+ priority?: number;
+ /** 选课时间别名(@contract-pending,MSW 兜底,与 selectedAt 同义) */
+ enrolledAt?: string;
}>;
}
+/** 创建课程计划输入(@contract-pending,MSW 兜底,扩展 course-plans.ts 的基础输入) */
+export interface AdminCreateCoursePlanInput {
+ name: string;
+ gradeId: string;
+ classId?: string;
+ subjectId: string;
+ teacherId?: string;
+ academicYearId?: string;
+ semester?: string;
+ description?: string;
+ objectives?: string;
+ syllabus?: string;
+ totalHours?: number;
+ weeklyHours?: number;
+ startDate?: string;
+ endDate?: string;
+ status?: string;
+}
+
+/** 更新课程计划输入(@contract-pending,MSW 兜底) */
+export interface AdminUpdateCoursePlanInput {
+ id: string;
+ name?: string;
+ gradeId?: string;
+ classId?: string;
+ subjectId?: string;
+ teacherId?: string;
+ academicYearId?: string;
+ semester?: string;
+ description?: string;
+ objectives?: string;
+ syllabus?: string;
+ totalHours?: number;
+ weeklyHours?: number;
+ startDate?: string;
+ endDate?: string;
+ status?: string;
+}
+
+/** 创建选修课输入(@contract-pending,MSW 兜底,扩展 elective.ts 的基础输入) */
+export interface AdminCreateElectiveInput {
+ name: string;
+ description?: string;
+ subjectId: string;
+ gradeId: string;
+ teacherId?: string;
+ capacity: number;
+ credit?: number;
+ classroom?: string;
+ schedule?: string;
+ selectionMode?: string;
+ startDate?: string;
+ endDate?: string;
+ selectionStartAt?: string;
+ selectionEndAt?: string;
+ dropDeadline?: string;
+ status?: string;
+}
+
+/** 更新选修课输入(@contract-pending,MSW 兜底) */
+export interface AdminUpdateElectiveInput {
+ id: string;
+ name?: string;
+ description?: string;
+ subjectId?: string;
+ gradeId?: string;
+ teacherId?: string;
+ capacity?: number;
+ credit?: number;
+ classroom?: string;
+ schedule?: string;
+ selectionMode?: string;
+ startDate?: string;
+ endDate?: string;
+ selectionStartAt?: string;
+ selectionEndAt?: string;
+ dropDeadline?: string;
+ status?: string;
+}
+
+/** 选修课总览统计(@contract-pending,MSW 兜底) */
+export interface ElectiveOverviewStats {
+ totalCourses: number;
+ totalCapacity: number;
+ totalEnrolled: number;
+ totalDraft: number;
+ totalOpen: number;
+}
+
export interface AdminQuestionListItem {
id: string;
type: string;
@@ -545,6 +896,32 @@ export interface AdminQuestionListItem {
createdBy: string;
}
+/**
+ * 题目详情(管理域视角,@contract-pending,MSW 兜底)。
+ *
+ * 与 AdminQuestionListItem 的差异:包含 answer / explanation / knowledgePointId /
+ * knowledgePointTitle / updatedAt 等详情字段,供详情对话框展示。
+ */
+export interface AdminQuestionDetail {
+ id: string;
+ type: string;
+ content: string;
+ difficulty: string;
+ subjectId: string;
+ subjectName: string;
+ textbookId: string;
+ textbookTitle: string;
+ knowledgePointId: string;
+ knowledgePointTitle: string;
+ status: string;
+ answer: string;
+ explanation: string | null;
+ source: string | null;
+ createdAt: string;
+ updatedAt: string;
+ createdBy: string;
+}
+
export interface QuestionFilter {
type?: string | null;
difficulty?: string | null;
@@ -586,6 +963,17 @@ export interface AdminErrorBookStats {
questionCount: number;
errorRate: number;
}>;
+ /**
+ * 按班级分组的错题统计(@contract-pending,MSW 兜底)。
+ * 用于"按班级分组"卡片展示,与 bySubject 互补。
+ */
+ byClass: Array<{
+ classId: string;
+ className: string;
+ errorCount: number;
+ questionCount: number;
+ errorRate: number;
+ }>;
topStudents: Array<{
studentId: string;
studentName: string;
@@ -655,6 +1043,8 @@ export interface AdminAttendanceRecord {
studentName: string;
classId: string;
className: string;
+ /** 年级 ID(@contract-pending,MSW 兜底,用于年级筛选) */
+ gradeId: string;
date: string;
status: string;
recordedBy: string;
@@ -665,6 +1055,8 @@ export interface AttendanceFilter {
classId?: string | null;
status?: string | null;
date?: string | null;
+ /** 年级筛选(@contract-pending,MSW 兜底)。与 classId 互不冲突,可同时设置。 */
+ gradeId?: string | null;
}
export interface AttendanceGradeCorrelation {
@@ -784,6 +1176,86 @@ export function useDeleteRole(): {
return { run, loading, error };
}
+/**
+ * 启用/停用角色(@contract-pending MSW 兜底)。
+ * 系统锁定角色(isLocked=true)禁止切换。
+ */
+export function useToggleRoleEnabled(): {
+ run: (
+ id: string,
+ isEnabled: boolean,
+ ) => Promise<{ id: string; isEnabled: boolean }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { toggleRoleEnabled: { id: string; isEnabled: boolean } },
+ { id: string; isEnabled: boolean }
+ >(TOGGLE_ROLE_ENABLED_DOC);
+ const run = async (
+ id: string,
+ isEnabled: boolean,
+ ): Promise<{ id: string; isEnabled: boolean }> => {
+ const data = await rawRun({ id, isEnabled });
+ if (!data?.toggleRoleEnabled) {
+ throw new ApiError("Failed to toggle role enabled", "INTERNAL_ERROR");
+ }
+ return data.toggleRoleEnabled;
+ };
+ return { run, loading, error };
+}
+
+// ============================================================
+// Hooks: RBAC role permission matrix (CRUD actions per permission point)
+// ============================================================
+export function useRolePermissions(
+ roleId: string,
+): UseQueryResult {
+ const result = useWidgetQuery<
+ { rolePermissions: RolePermissionMatrixItem[] },
+ { roleId: string }
+ >(GET_ROLE_PERMISSIONS_DOC, { roleId });
+ return { ...result, data: result.data?.rolePermissions ?? [] };
+}
+
+export function useUpdateRolePermissionActions(): {
+ run: (
+ roleId: string,
+ permissionUpdates: PermissionActionUpdate[],
+ ) => Promise<{ id: string; updatedCount: number }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { updateRolePermissionActions: { id: string; updatedCount: number } },
+ {
+ roleId: string;
+ permissionUpdates: PermissionActionUpdate[];
+ }
+ >(UPDATE_ROLE_PERMISSION_ACTIONS_DOC);
+ const run = async (
+ roleId: string,
+ permissionUpdates: PermissionActionUpdate[],
+ ): Promise<{ id: string; updatedCount: number }> => {
+ const data = await rawRun({ roleId, permissionUpdates });
+ if (!data?.updateRolePermissionActions) {
+ throw new ApiError(
+ "Failed to update role permission actions",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.updateRolePermissionActions;
+ };
+ return { run, loading, error };
+}
export function usePermissionRoleCounts(): UseQueryResult<
PermissionRoleCount[]
> {
@@ -877,6 +1349,78 @@ export function useAuditModuleOptions(): UseQueryResult {
return { ...result, data: result.data?.auditModuleOptions };
}
+// ============================================================
+// Hooks: Audit / Login / DataChange - export (CSV)
+// ============================================================
+export function useExportAuditLogs(): {
+ run: (filter: AuditLogFilter) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { exportAuditLogs: ExportResult },
+ { filter: AuditLogFilter }
+ >(EXPORT_AUDIT_LOGS_DOC);
+ const run = async (filter: AuditLogFilter): Promise => {
+ const data = await rawRun({ filter });
+ if (!data?.exportAuditLogs) {
+ throw new ApiError("Failed to export audit logs", "INTERNAL_ERROR");
+ }
+ return data.exportAuditLogs;
+ };
+ return { run, loading, error };
+}
+
+export function useExportLoginLogs(): {
+ run: (filter: LoginLogFilter) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { exportLoginLogs: ExportResult },
+ { filter: LoginLogFilter }
+ >(EXPORT_LOGIN_LOGS_DOC);
+ const run = async (filter: LoginLogFilter): Promise => {
+ const data = await rawRun({ filter });
+ if (!data?.exportLoginLogs) {
+ throw new ApiError("Failed to export login logs", "INTERNAL_ERROR");
+ }
+ return data.exportLoginLogs;
+ };
+ return { run, loading, error };
+}
+
+export function useExportDataChanges(): {
+ run: (filter: DataChangeLogFilter) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { exportDataChanges: ExportResult },
+ { filter: DataChangeLogFilter }
+ >(EXPORT_DATA_CHANGES_DOC);
+ const run = async (filter: DataChangeLogFilter): Promise => {
+ const data = await rawRun({ filter });
+ if (!data?.exportDataChanges) {
+ throw new ApiError("Failed to export data changes", "INTERNAL_ERROR");
+ }
+ return data.exportDataChanges;
+ };
+ return { run, loading, error };
+}
+
// ============================================================
// Hooks: School CRUD
// ============================================================
@@ -913,6 +1457,35 @@ export function useCreateSchool(): {
return { run, loading, error };
}
+export function useAdminUpdateSchool(): {
+ run: (
+ id: string,
+ input: SchoolInput,
+ ) => Promise<{ id: string; name: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { updateSchool: { id: string; name: string } },
+ { id: string; input: SchoolInput }
+ >(ADMIN_UPDATE_SCHOOL_DOC);
+ const run = async (
+ id: string,
+ input: SchoolInput,
+ ): Promise<{ id: string; name: string }> => {
+ const data = await rawRun({ id, input });
+ if (!data?.updateSchool) {
+ throw new ApiError("Failed to update school", "INTERNAL_ERROR");
+ }
+ return data.updateSchool;
+ };
+ return { run, loading, error };
+}
+
export function useDeleteSchool(): {
run: (id: string) => Promise<{ id: string }>;
loading: boolean;
@@ -1220,6 +1793,94 @@ export function useStaffOptions(): UseQueryResult {
return { ...result, data: result.data?.staffOptions };
}
+/**
+ * 创建班级(@contract-pending,MSW 兜底)。
+ * 用于 admin/school/classes 列表页 ClassFormDialog 新建模式。
+ */
+export function useCreateAdminClass(): {
+ run: (input: AdminClassInput) => Promise<{ id: string; name: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { createAdminClass: { id: string; name: string } },
+ { input: AdminClassInput }
+ >(CREATE_ADMIN_CLASS_DOC);
+ const run = async (
+ input: AdminClassInput,
+ ): Promise<{ id: string; name: string }> => {
+ const data = await rawRun({ input });
+ if (!data?.createAdminClass) {
+ throw new ApiError("Failed to create admin class", "INTERNAL_ERROR");
+ }
+ return data.createAdminClass;
+ };
+ return { run, loading, error };
+}
+
+/**
+ * 更新班级(@contract-pending,MSW 兜底)。
+ * 用于 admin/school/classes 列表页 ClassFormDialog 编辑模式。
+ */
+export function useUpdateAdminClass(): {
+ run: (
+ id: string,
+ input: AdminClassInput,
+ ) => Promise<{ id: string; name: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { updateAdminClass: { id: string; name: string } },
+ { id: string; input: AdminClassInput }
+ >(UPDATE_ADMIN_CLASS_DOC);
+ const run = async (
+ id: string,
+ input: AdminClassInput,
+ ): Promise<{ id: string; name: string }> => {
+ const data = await rawRun({ id, input });
+ if (!data?.updateAdminClass) {
+ throw new ApiError("Failed to update admin class", "INTERNAL_ERROR");
+ }
+ return data.updateAdminClass;
+ };
+ return { run, loading, error };
+}
+
+/**
+ * 删除班级(@contract-pending,MSW 兜底)。
+ * 用于 admin/school/classes 列表页 ClassDeleteDialog。
+ */
+export function useDeleteAdminClass(): {
+ run: (id: string) => Promise<{ id: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<{ deleteAdminClass: { id: string } }, { id: string }>(
+ DELETE_ADMIN_CLASS_DOC,
+ );
+ const run = async (id: string): Promise<{ id: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.deleteAdminClass) {
+ throw new ApiError("Failed to delete admin class", "INTERNAL_ERROR");
+ }
+ return data.deleteAdminClass;
+ };
+ return { run, loading, error };
+}
+
// ============================================================
// Hooks: Announcements
// ============================================================
@@ -1365,6 +2026,35 @@ export function usePinAnnouncement(): {
return { run, loading, error };
}
+export function usePublishAnnouncement(): {
+ run: (
+ id: string,
+ ) => Promise<{ id: string; status: string; publishedAt: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ {
+ publishAnnouncement: { id: string; status: string; publishedAt: string };
+ },
+ { id: string }
+ >(PUBLISH_ANNOUNCEMENT_DOC);
+ const run = async (
+ id: string,
+ ): Promise<{ id: string; status: string; publishedAt: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.publishAnnouncement) {
+ throw new ApiError("Failed to publish announcement", "INTERNAL_ERROR");
+ }
+ return data.publishAnnouncement;
+ };
+ return { run, loading, error };
+}
+
// ============================================================
// Hooks: Files
// ============================================================
@@ -1386,6 +2076,52 @@ export function useFileStats(): UseQueryResult {
return { ...result, data: result.data?.fileStats ?? null };
}
+export function useUploadFile(): {
+ run: (input: UploadFileInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { uploadFile: UploadFileResult },
+ { input: UploadFileInput }
+ >(UPLOAD_FILE_DOC);
+ const run = async (input: UploadFileInput): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.uploadFile) {
+ throw new ApiError("Failed to upload file", "INTERNAL_ERROR");
+ }
+ return data.uploadFile;
+ };
+ return { run, loading, error };
+}
+
+export function useBatchDeleteFiles(): {
+ run: (fileIds: string[]) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { batchDeleteFiles: BatchDeleteFilesResult },
+ { fileIds: string[] }
+ >(BATCH_DELETE_FILES_DOC);
+ const run = async (fileIds: string[]): Promise => {
+ const data = await rawRun({ fileIds });
+ if (!data?.batchDeleteFiles) {
+ throw new ApiError("Failed to batch delete files", "INTERNAL_ERROR");
+ }
+ return data.batchDeleteFiles;
+ };
+ return { run, loading, error };
+}
+
// ============================================================
// Hooks: AI settings
// ============================================================
@@ -1475,6 +2211,33 @@ export function useDeleteAiProvider(): {
return { run, loading, error };
}
+export function useTestAiProvider(): {
+ run: (
+ id: string,
+ ) => Promise<{ ok: boolean; latencyMs: number; message: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { testAiProvider: { ok: boolean; latencyMs: number; message: string } },
+ { id: string }
+ >(TEST_AI_PROVIDER_DOC);
+ const run = async (
+ id: string,
+ ): Promise<{ ok: boolean; latencyMs: number; message: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.testAiProvider) {
+ throw new ApiError("Failed to test AI provider", "INTERNAL_ERROR");
+ }
+ return data.testAiProvider;
+ };
+ return { run, loading, error };
+}
+
export function useAiUsageDashboard(
range: string,
): UseQueryResult {
@@ -1674,6 +2437,24 @@ export function useAdminQuestions(
return { ...result, data: result.data?.adminQuestions };
}
+/**
+ * 题库单条详情 hook(@contract-pending,MSW 兜底)。
+ *
+ * 用于详情对话框按 id 拉取题目明细(answer/explanation/knowledgePointTitle 等)。
+ * id 为空时不发请求(对话框关闭时跳过)。
+ */
+export function useAdminQuestion(
+ id: string,
+ options?: { enabled?: boolean },
+): UseQueryResult {
+ const enabled = options?.enabled ?? true;
+ const result = useWidgetQuery<
+ { adminQuestion: AdminQuestionDetail | null },
+ { id: string }
+ >(GET_ADMIN_QUESTION_DOC, { id }, { enabled });
+ return { ...result, data: result.data?.adminQuestion ?? null };
+}
+
// Lesson plans
export function useAdminLessonPlans(): UseQueryResult<
PaginatedResult
@@ -1695,6 +2476,45 @@ export function useAdminLessonPlan(
return { ...result, data: result.data?.adminLessonPlan ?? null };
}
+/** 软删除教案 mutation 响应(@contract-pending) */
+interface SoftDeleteLessonPlanResponse {
+ softDeleteLessonPlan: { success: boolean; deletedAt: string } | null;
+}
+
+/**
+ * 软删除教案 mutation(@contract-pending,MSW 兜底)。
+ *
+ * 对齐 CICD softDeleteLessonPlan:将 status 置为 archived,返回 { success, deletedAt }。
+ * 用于管理端 admin/lesson-plans 列表/详情页删除按钮。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useSoftDeleteLessonPlan(): {
+ run: (planId: string) => Promise<{ success: boolean; deletedAt: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ SOFT_DELETE_LESSON_PLAN_DOC,
+ );
+
+ const run = async (
+ planId: string,
+ ): Promise<{ success: boolean; deletedAt: string }> => {
+ const data = await rawRun({ planId });
+ if (!data?.softDeleteLessonPlan) {
+ throw new ApiError("Failed to soft delete lesson plan", "INTERNAL_ERROR");
+ }
+ return data.softDeleteLessonPlan;
+ };
+
+ return { run, loading, error };
+}
+
// Error book
export function useAdminErrorBookStats(): UseQueryResult {
const result = useWidgetQuery<
@@ -1704,6 +2524,58 @@ export function useAdminErrorBookStats(): UseQueryResult Promise<{
+ success: boolean;
+ count: number;
+ filename: string;
+ csv: string;
+ }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ {
+ exportErrorBookCsv: {
+ success: boolean;
+ count: number;
+ filename: string;
+ csv: string;
+ };
+ },
+ { filter: { subjectId?: string | null; classId?: string | null } }
+ >(EXPORT_ERROR_BOOK_CSV_DOC);
+ const run = async (filter: {
+ subjectId?: string | null;
+ classId?: string | null;
+ }): Promise<{
+ success: boolean;
+ count: number;
+ filename: string;
+ csv: string;
+ }> => {
+ const data = await rawRun({ filter });
+ if (!data?.exportErrorBookCsv) {
+ throw new ApiError("Failed to export error book CSV", "INTERNAL_ERROR");
+ }
+ return data.exportErrorBookCsv;
+ };
+ return { run, loading, error };
+}
+
// Scheduling
export function useAdminScheduleChanges(): UseQueryResult<
PaginatedResult
@@ -1881,3 +2753,629 @@ export function useAttendanceGradeCorrelation(): UseQueryResult<
>(GET_ATTENDANCE_GRADE_CORRELATION_DOC, {});
return { ...result, data: result.data?.attendanceGradeCorrelation };
}
+
+// ============================================================
+// Types & Hooks: Audit retention config + purge(@contract-pending,MSW 兜底)
+// ============================================================
+export interface AuditRetentionConfig {
+ retentionDays: number;
+ loginLogRetentionDays: number;
+ autoCleanupEnabled: boolean;
+}
+
+export interface PurgeResult {
+ auditLogsDeleted: number;
+ loginLogsDeleted: number;
+ dataChangeLogsDeleted: number;
+}
+
+export interface AuditRetentionConfigInput {
+ retentionDays: number;
+ loginLogRetentionDays: number;
+ autoCleanupEnabled: boolean;
+}
+
+export function useAuditRetentionConfig(): UseQueryResult {
+ const result = useWidgetQuery<
+ { auditRetentionConfig: AuditRetentionConfig | null },
+ Record
+ >(GET_AUDIT_RETENTION_CONFIG_DOC, {});
+ return { ...result, data: result.data?.auditRetentionConfig ?? null };
+}
+
+export function useSaveAuditRetentionConfig(): {
+ run: (input: AuditRetentionConfigInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { saveAuditRetentionConfig: AuditRetentionConfig },
+ { input: AuditRetentionConfigInput }
+ >(SAVE_AUDIT_RETENTION_CONFIG_DOC);
+ const run = async (
+ input: AuditRetentionConfigInput,
+ ): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.saveAuditRetentionConfig) {
+ throw new ApiError(
+ "Failed to save audit retention config",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.saveAuditRetentionConfig;
+ };
+ return { run, loading, error };
+}
+
+export function usePurgeExpiredAuditLogs(): {
+ run: (
+ retentionDays: number,
+ loginLogRetentionDays?: number,
+ ) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { purgeExpiredAuditLogs: PurgeResult },
+ { retentionDays: number; loginLogRetentionDays?: number | null }
+ >(PURGE_EXPIRED_AUDIT_LOGS_DOC);
+ const run = async (
+ retentionDays: number,
+ loginLogRetentionDays?: number,
+ ): Promise => {
+ const data = await rawRun(
+ loginLogRetentionDays !== undefined
+ ? { retentionDays, loginLogRetentionDays }
+ : { retentionDays, loginLogRetentionDays: null },
+ );
+ if (!data?.purgeExpiredAuditLogs) {
+ throw new ApiError(
+ "Failed to purge expired audit logs",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.purgeExpiredAuditLogs;
+ };
+ return { run, loading, error };
+}
+// ============================================================
+// Hooks: Course plan items CRUD(@contract-pending,MSW 兜底)
+// ============================================================
+export function useCreateCoursePlanItem(): {
+ run: (input: AdminCoursePlanItemInput) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { createCoursePlanItem: AdminCoursePlanItem },
+ { input: AdminCoursePlanItemInput }
+ >(CREATE_COURSE_PLAN_ITEM_DOC);
+ const run = async (
+ input: AdminCoursePlanItemInput,
+ ): Promise => {
+ const data = await rawRun({ input });
+ if (!data?.createCoursePlanItem) {
+ throw new ApiError("Failed to create course plan item", "INTERNAL_ERROR");
+ }
+ return data.createCoursePlanItem;
+ };
+ return { run, loading, error };
+}
+
+export function useUpdateCoursePlanItem(): {
+ run: (
+ id: string,
+ input: AdminCoursePlanItemUpdateInput,
+ ) => Promise;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { updateCoursePlanItem: AdminCoursePlanItem },
+ { id: string; input: AdminCoursePlanItemUpdateInput }
+ >(UPDATE_COURSE_PLAN_ITEM_DOC);
+ const run = async (
+ id: string,
+ input: AdminCoursePlanItemUpdateInput,
+ ): Promise => {
+ const data = await rawRun({ id, input });
+ if (!data?.updateCoursePlanItem) {
+ throw new ApiError("Failed to update course plan item", "INTERNAL_ERROR");
+ }
+ return data.updateCoursePlanItem;
+ };
+ return { run, loading, error };
+}
+export function useDeleteCoursePlanItem(): {
+ run: (id: string) => Promise<{ id: string; success: boolean }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ { deleteCoursePlanItem: { id: string; success: boolean } },
+ { id: string }
+ >(DELETE_COURSE_PLAN_ITEM_DOC);
+ const run = async (id: string): Promise<{ id: string; success: boolean }> => {
+ const data = await rawRun({ id });
+ if (!data?.deleteCoursePlanItem) {
+ throw new ApiError("Failed to delete course plan item", "INTERNAL_ERROR");
+ }
+ return data.deleteCoursePlanItem;
+ };
+ return { run, loading, error };
+}
+
+export function useToggleCoursePlanItemCompleted(): {
+ run: (
+ id: string,
+ isCompleted: boolean,
+ ) => Promise<{
+ id: string;
+ isCompleted: boolean;
+ completedAt: string | null;
+ updatedAt: string;
+ }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ {
+ toggleCoursePlanItemCompleted: {
+ id: string;
+ isCompleted: boolean;
+ completedAt: string | null;
+ updatedAt: string;
+ };
+ },
+ { id: string; isCompleted: boolean }
+ >(TOGGLE_COURSE_PLAN_ITEM_COMPLETED_DOC);
+ const run = async (
+ id: string,
+ isCompleted: boolean,
+ ): Promise<{
+ id: string;
+ isCompleted: boolean;
+ completedAt: string | null;
+ updatedAt: string;
+ }> => {
+ const data = await rawRun({ id, isCompleted });
+ if (!data?.toggleCoursePlanItemCompleted) {
+ throw new ApiError(
+ "Failed to toggle course plan item completed",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.toggleCoursePlanItemCompleted;
+ };
+ return { run, loading, error };
+}
+
+export function useReorderCoursePlanItems(): {
+ run: (
+ planId: string,
+ items: ReorderCoursePlanItemInput[],
+ ) => Promise>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ {
+ reorderCoursePlanItems: Array<{
+ id: string;
+ week: number;
+ updatedAt: string;
+ }>;
+ },
+ { planId: string; items: ReorderCoursePlanItemInput[] }
+ >(REORDER_COURSE_PLAN_ITEMS_DOC);
+ const run = async (
+ planId: string,
+ items: ReorderCoursePlanItemInput[],
+ ): Promise> => {
+ const data = await rawRun({ planId, items });
+ if (!data?.reorderCoursePlanItems) {
+ throw new ApiError(
+ "Failed to reorder course plan items",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.reorderCoursePlanItems;
+ };
+ return { run, loading, error };
+}
+
+// ============================================================
+// Hooks: 课程计划 CRUD + 批量操作(@contract-pending,MSW 兜底)
+// ============================================================
+
+/** 删除课程计划 mutation 响应(@contract-pending) */
+interface DeleteCoursePlanResponse {
+ deleteCoursePlan: { id: string; success: boolean } | null;
+}
+
+/**
+ * 删除课程计划(@contract-pending,MSW 兜底)。
+ * 用于详情页/列表页删除按钮,调用后建议 refetch 列表。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useDeleteCoursePlan(): {
+ run: (id: string) => Promise<{ id: string; success: boolean }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ DELETE_COURSE_PLAN_DOC,
+ );
+ const run = async (id: string): Promise<{ id: string; success: boolean }> => {
+ const data = await rawRun({ id });
+ if (!data?.deleteCoursePlan) {
+ throw new ApiError("Failed to delete course plan", "INTERNAL_ERROR");
+ }
+ return data.deleteCoursePlan;
+ };
+ return { run, loading, error };
+}
+
+/** 批量切换周计划项完成状态 mutation 响应(@contract-pending) */
+interface BulkToggleCoursePlanItemsResponse {
+ bulkToggleCoursePlanItems: Array<{
+ id: string;
+ isCompleted: boolean;
+ completedAt: string | null;
+ updatedAt: string;
+ }>;
+}
+
+/**
+ * 批量切换周计划项完成状态(@contract-pending,MSW 兜底)。
+ * 用于详情页批量"标记完成/取消完成"操作。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useBulkToggleCoursePlanItems(): {
+ run: (
+ planId: string,
+ itemIds: string[],
+ isCompleted: boolean,
+ ) => Promise<
+ Array<{
+ id: string;
+ isCompleted: boolean;
+ completedAt: string | null;
+ updatedAt: string;
+ }>
+ >;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ BulkToggleCoursePlanItemsResponse,
+ { planId: string; itemIds: string[]; isCompleted: boolean }
+ >(BULK_TOGGLE_COURSE_PLAN_ITEMS_DOC);
+ const run = async (
+ planId: string,
+ itemIds: string[],
+ isCompleted: boolean,
+ ): Promise<
+ Array<{
+ id: string;
+ isCompleted: boolean;
+ completedAt: string | null;
+ updatedAt: string;
+ }>
+ > => {
+ const data = await rawRun({ planId, itemIds, isCompleted });
+ if (!data?.bulkToggleCoursePlanItems) {
+ throw new ApiError(
+ "Failed to bulk toggle course plan items",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.bulkToggleCoursePlanItems;
+ };
+ return { run, loading, error };
+}
+
+// ============================================================
+// Hooks: 选修课 CRUD + 业务动作(@contract-pending,MSW 兜底)
+// ============================================================
+
+/** 创建选修课 mutation 响应(@contract-pending) */
+interface AdminCreateElectiveResponse {
+ createElective: { id: string } | null;
+}
+
+/**
+ * 创建选修课(@contract-pending,MSW 兜底,admin scope)。
+ * 与 elective.ts 的 useCreateElective 区别:本 hook 走 admin scope,
+ * 支持扩展字段(classroom / schedule / credit / selectionMode 等)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useAdminCreateElective(): {
+ run: (input: AdminCreateElectiveInput) => Promise<{ id: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ AdminCreateElectiveResponse,
+ { input: AdminCreateElectiveInput }
+ >(CREATE_ELECTIVE_DOC);
+ const run = async (
+ input: AdminCreateElectiveInput,
+ ): Promise<{ id: string }> => {
+ const data = await rawRun({ input });
+ if (!data?.createElective) {
+ throw new ApiError("Failed to create elective", "INTERNAL_ERROR");
+ }
+ return data.createElective;
+ };
+ return { run, loading, error };
+}
+
+/** 更新选修课 mutation 响应(@contract-pending) */
+interface AdminUpdateElectiveResponse {
+ updateElective: { id: string } | null;
+}
+
+/**
+ * 更新选修课(@contract-pending,MSW 兜底,admin scope)。
+ * 与 elective.ts 的 useUpdateElective 区别:本 hook 走 admin scope,
+ * 支持扩展字段。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useAdminUpdateElective(): {
+ run: (input: AdminUpdateElectiveInput) => Promise<{ id: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation<
+ AdminUpdateElectiveResponse,
+ { input: AdminUpdateElectiveInput }
+ >(UPDATE_ELECTIVE_DOC);
+ const run = async (
+ input: AdminUpdateElectiveInput,
+ ): Promise<{ id: string }> => {
+ const data = await rawRun({ input });
+ if (!data?.updateElective) {
+ throw new ApiError("Failed to update elective", "INTERNAL_ERROR");
+ }
+ return data.updateElective;
+ };
+ return { run, loading, error };
+}
+
+/** 删除选修课 mutation 响应(@contract-pending) */
+interface DeleteElectiveResponse {
+ deleteElective: { id: string; success: boolean } | null;
+}
+
+/**
+ * 删除选修课(@contract-pending,MSW 兜底)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useDeleteElective(): {
+ run: (id: string) => Promise<{ id: string; success: boolean }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ DELETE_ELECTIVE_DOC,
+ );
+ const run = async (id: string): Promise<{ id: string; success: boolean }> => {
+ const data = await rawRun({ id });
+ if (!data?.deleteElective) {
+ throw new ApiError("Failed to delete elective", "INTERNAL_ERROR");
+ }
+ return data.deleteElective;
+ };
+ return { run, loading, error };
+}
+
+/** 开放选课 mutation 响应(@contract-pending) */
+interface OpenElectiveSelectionResponse {
+ openElectiveSelection: {
+ id: string;
+ status: string;
+ updatedAt: string;
+ } | null;
+}
+
+/**
+ * 开放选课(@contract-pending,MSW 兜底)。
+ * 将选修课状态从 DRAFT 切换为 OPEN。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useOpenElectiveSelection(): {
+ run: (
+ id: string,
+ ) => Promise<{ id: string; status: string; updatedAt: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ OPEN_ELECTIVE_SELECTION_DOC,
+ );
+ const run = async (
+ id: string,
+ ): Promise<{ id: string; status: string; updatedAt: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.openElectiveSelection) {
+ throw new ApiError("Failed to open elective selection", "INTERNAL_ERROR");
+ }
+ return data.openElectiveSelection;
+ };
+ return { run, loading, error };
+}
+
+/** 关闭选课 mutation 响应(@contract-pending) */
+interface CloseElectiveSelectionResponse {
+ closeElectiveSelection: {
+ id: string;
+ status: string;
+ updatedAt: string;
+ } | null;
+}
+
+/**
+ * 关闭选课(@contract-pending,MSW 兜底)。
+ * 将选修课状态从 OPEN 切换为 CLOSED。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useCloseElectiveSelection(): {
+ run: (
+ id: string,
+ ) => Promise<{ id: string; status: string; updatedAt: string }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ CLOSE_ELECTIVE_SELECTION_DOC,
+ );
+ const run = async (
+ id: string,
+ ): Promise<{ id: string; status: string; updatedAt: string }> => {
+ const data = await rawRun({ id });
+ if (!data?.closeElectiveSelection) {
+ throw new ApiError(
+ "Failed to close elective selection",
+ "INTERNAL_ERROR",
+ );
+ }
+ return data.closeElectiveSelection;
+ };
+ return { run, loading, error };
+}
+
+/** 运行选课抽签 mutation 响应(@contract-pending) */
+interface RunElectiveLotteryResponse {
+ runElectiveLottery: {
+ id: string;
+ status: string;
+ selectedCount: number;
+ updatedAt: string;
+ } | null;
+}
+
+/**
+ * 运行选课抽签(@contract-pending,MSW 兜底)。
+ * 对 LOTTERY 模式的选修课执行抽签,返回最终中选人数。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useRunElectiveLottery(): {
+ run: (id: string) => Promise<{
+ id: string;
+ status: string;
+ selectedCount: number;
+ updatedAt: string;
+ }>;
+ loading: boolean;
+ error: unknown;
+} {
+ const {
+ run: rawRun,
+ loading,
+ error,
+ } = useWidgetMutation(
+ RUN_ELECTIVE_LOTTERY_DOC,
+ );
+ const run = async (
+ id: string,
+ ): Promise<{
+ id: string;
+ status: string;
+ selectedCount: number;
+ updatedAt: string;
+ }> => {
+ const data = await rawRun({ id });
+ if (!data?.runElectiveLottery) {
+ throw new ApiError("Failed to run elective lottery", "INTERNAL_ERROR");
+ }
+ return data.runElectiveLottery;
+ };
+ return { run, loading, error };
+}
+
+/** 选修课总览统计查询响应(@contract-pending) */
+interface ElectiveOverviewStatsResponse {
+ electiveOverviewStats: ElectiveOverviewStats | null;
+}
+
+/**
+ * 查询选修课总览统计(@contract-pending,MSW 兜底)。
+ * 用于列表页 StatsGrid 顶部统计卡片(总数 / 容量 / 已选 / 草稿 / 报名中)。
+ *
+ * 关联:ARCHITECTURE.md §5.4 / §9.4 / §11.4 契约工单
+ */
+export function useGetElectiveOverviewStats(): UseQueryResult {
+ const result = useWidgetQuery<
+ ElectiveOverviewStatsResponse,
+ Record
+ >(GET_ELECTIVE_OVERVIEW_STATS_DOC, {});
+ return {
+ ...result,
+ data: result.data?.electiveOverviewStats ?? null,
+ };
+}
diff --git a/apps/portal-shell/src/lib/api/admin.ts b/apps/portal-shell/src/lib/api/admin.ts
index 056bb69..3f545f1 100644
--- a/apps/portal-shell/src/lib/api/admin.ts
+++ b/apps/portal-shell/src/lib/api/admin.ts
@@ -105,6 +105,12 @@ export interface AuditLog {
ip: string;
timestamp: string;
details: string;
+ /**
+ * 审计日志状态(success/failure/error/pending)。
+ * @contract-pending 当前 schema 未提供此字段,MSW 兜底时为 undefined,
+ * 列表 StatusBadge 显示 "--" 占位;契约补齐后切换为真实值。
+ */
+ status?: string | null;
}
export interface AuditLogFilter {
diff --git a/apps/portal-shell/src/lib/api/operations/admin.graphql.ts b/apps/portal-shell/src/lib/api/operations/admin.graphql.ts
index 48b0cd6..35f3d8c 100644
--- a/apps/portal-shell/src/lib/api/operations/admin.graphql.ts
+++ b/apps/portal-shell/src/lib/api/operations/admin.graphql.ts
@@ -476,6 +476,17 @@ export const DELETE_SCHOOL_DOC = gql`
}
`;
+// P5 多校 CRUD:按 ID 更新学校(区别于 school-settings 的单校 UpdateSchool($input!))
+// operationName 用 AdminUpdateSchool 避免与 school-settings 的 UpdateSchool 重名导致 codegen 重复声明
+export const ADMIN_UPDATE_SCHOOL_DOC = gql`
+ mutation AdminUpdateSchool($id: ID!, $input: SchoolInput!) {
+ updateSchool(id: $id, input: $input) {
+ id
+ name
+ }
+ }
+`;
+
// ── School: departments ──
export const GET_DEPARTMENTS_DOC = gql`
query GetDepartments {
@@ -585,7 +596,7 @@ export const GET_GRADE_OVERVIEW_STATS_DOC = gql`
`;
export const ADMIN_CREATE_GRADE_DOC = gql`
- mutation CreateGrade($input: GradeInput!) {
+ mutation AdminCreateGrade($input: GradeInput!) {
createGrade(input: $input) {
id
name
@@ -1186,7 +1197,7 @@ export const GET_ADMIN_SCHEDULE_ENTRIES_DOC = gql`
`;
export const ADMIN_GET_SCHEDULING_RULES_DOC = gql`
- query GetSchedulingRules {
+ query AdminGetSchedulingRules {
schedulingRules {
id
name
diff --git a/apps/portal-shell/src/messages/en.json b/apps/portal-shell/src/messages/en.json
index 6d39198..e75d522 100644
--- a/apps/portal-shell/src/messages/en.json
+++ b/apps/portal-shell/src/messages/en.json
@@ -6,10 +6,13 @@
"cancel": "Cancel",
"edit": "Edit",
"export": "Export",
+ "import": "Import",
"search": "Search",
"delete": "Delete",
"retry": "Retry",
- "back": "Back"
+ "back": "Back",
+ "prev": "Previous",
+ "next": "Next"
},
"label": {
"search": "Search",
@@ -30,6 +33,7 @@
},
"error": {
"loadFailed": "Failed to load: {message}",
+ "operationFailed": "Operation failed: {message}",
"pageError": "Something went wrong"
},
"nav": {
@@ -45,7 +49,8 @@
},
"empty": {
"title": "No data",
- "description": "Adjust filters and retry, or create your first record"
+ "description": "Adjust filters and retry, or create your first record",
+ "searchResult": "No matching permission points"
},
"navLabel": {
"dashboard": "Dashboard",
@@ -120,11 +125,16 @@
"subtitle": "{email} · Roles: {roles} · Data scope: {dataScope}",
"subtitleNoRoles": "None",
"error": {
- "loadFailed": "Dashboard failed to load: {message}"
+ "loadFailed": "Dashboard failed to load: {message}",
+ "loadFailedGeneric": "Load Failed",
+ "loadFailedDesc": "Data could not be loaded. Please retry.",
+ "retry": "Retry"
},
"empty": {
"title": "No data",
- "description": "Dashboard data is not ready yet"
+ "description": "Dashboard data is not ready yet",
+ "noNotifications": "No Notifications",
+ "noNotificationsDesc": "New notifications will appear here"
},
"stats": {
"classes": "Total Classes",
@@ -145,6 +155,11 @@
"title": "Teacher Dashboard",
"description": "Today's teaching overview",
"loadFailed": "Dashboard data failed to load. Please try again later.",
+ "greeting": {
+ "morning": "Good morning",
+ "afternoon": "Good afternoon",
+ "evening": "Good evening"
+ },
"stats": {
"totalClasses": "Total Classes",
"totalStudents": "Total Students",
@@ -164,6 +179,74 @@
"currentValue": "Current",
"threshold": "Threshold"
}
+ },
+ "teacherCards": {
+ "quickActions": {
+ "createAssignment": "Create Assignment",
+ "grade": "Grade",
+ "myClasses": "My Classes"
+ },
+ "todo": {
+ "title": "To-Do",
+ "empty": "No to-dos today"
+ },
+ "classes": {
+ "title": "My Classes",
+ "viewAll": "View All",
+ "emptyTitle": "No Classes Yet",
+ "emptyDescription": "Please contact admin to assign classes",
+ "createClass": "Manage Classes",
+ "homeroom": "Homeroom",
+ "room": "Room"
+ },
+ "homework": {
+ "title": "Homework",
+ "createNewAssignment": "Create new assignment",
+ "emptyTitle": "No Assignments",
+ "emptyDescription": "Click + to create your first assignment",
+ "create": "Create Assignment",
+ "noDueDate": "No due date",
+ "viewAllAssignments": "View all assignments"
+ },
+ "schedule": {
+ "title": "Today's Schedule",
+ "emptyTitle": "No Classes Today",
+ "emptyDescription": "Enjoy a free day",
+ "viewSchedule": "View Schedule",
+ "live": "LIVE",
+ "scrollForMore": "Scroll for more",
+ "noMoreClasses": "No more classes today"
+ },
+ "gradeTrends": {
+ "title": "Class Performance",
+ "description": "Average score across last {count} assignments",
+ "emptyTitle": "No Data Yet",
+ "emptyDescription": "Trends will appear after grading",
+ "averageScorePercent": "Average Score (%)",
+ "submittedCount": "Submitted {submitted}/{total}"
+ },
+ "recentSubmissions": {
+ "title": "Recent Submissions",
+ "viewAll": "View All",
+ "emptyTitle": "No New Submissions",
+ "emptyDescription": "All graded",
+ "colStudent": "Student",
+ "colAssignment": "Assignment",
+ "colSubmitted": "Submitted",
+ "colAction": "Action",
+ "late": "Late",
+ "grade": "Grade"
+ }
+ },
+ "timeRange": {
+ "label": "Time Range",
+ "today": "Today",
+ "week": "This Week",
+ "month": "This Month"
+ },
+ "sections": {
+ "notifications": "Notifications",
+ "viewAllNotifications": "View All"
}
},
"classes": {
@@ -430,7 +513,106 @@
"error": {
"title": "Exams page error",
"unknown": "Unknown error",
- "retry": "Retry"
+ "retry": "Retry",
+ "createdSuccess": "Question created successfully",
+ "updatedSuccess": "Question updated successfully",
+ "unexpected": "An unexpected error occurred"
+ },
+ "actions": {
+ "view": "View",
+ "build": "Build",
+ "analytics": "Analytics",
+ "delete": "Delete",
+ "confirmDelete": "Confirm Delete",
+ "cancel": "Cancel",
+ "deleteSuccess": "Deleted successfully",
+ "deleteFailed": "Delete failed: {message}",
+ "readOnly": "Read only"
+ },
+ "table": {
+ "selectAll": "Select all",
+ "selectRow": "Select row",
+ "selectedCount": "{selected}/{total} rows selected",
+ "noResults": "No results"
+ },
+ "viewer": {
+ "section": "Section",
+ "group": "Group",
+ "unknown": "Unknown type",
+ "scoreLabel": "Score: {score}",
+ "noQuestions": "No questions"
+ },
+ "paper": {
+ "section": "Section",
+ "group": "Group",
+ "scoreWithUnit": "({score} pts)",
+ "subject": "Subject",
+ "grade": "Grade",
+ "time": "Duration",
+ "minutes": "min",
+ "total": "Total",
+ "pts": "pts",
+ "class": "Class",
+ "name": "Name",
+ "no": "No.",
+ "empty": "No questions"
+ },
+ "editor": {
+ "contentPlaceholder": "Start typing exam content...",
+ "loading": "Loading editor..."
+ },
+ "richEditor": {
+ "bold": "Bold",
+ "italic": "Italic",
+ "strike": "Strikethrough",
+ "dotted": "Dotted",
+ "bulletList": "Bullet List",
+ "orderedList": "Ordered List",
+ "quote": "Quote",
+ "undo": "Undo",
+ "redo": "Redo",
+ "markBlank": "Mark Blank"
+ },
+ "selectionToolbar": {
+ "ariaLabel": "Selection toolbar",
+ "blankShortAnswer": "Short Answer",
+ "composite": "Composite",
+ "defaultGroupTitle": "Group",
+ "defaultSectionTitle": "Section",
+ "groupLabel": "Group",
+ "image": "Image",
+ "sectionLabel": "Section",
+ "singleChoice": "Single Choice"
+ },
+ "richForm": {
+ "titlePlaceholder": "Enter exam title",
+ "classIdPlaceholder": "Class ID",
+ "subjectIdPlaceholder": "Subject ID",
+ "difficulty": "Difficulty",
+ "difficultyLevel1": "1 star",
+ "difficultyLevel2": "2 stars",
+ "difficultyLevel3": "3 stars",
+ "difficultyLevel4": "4 stars",
+ "difficultyLevel5": "5 stars",
+ "totalScore": "Total Score",
+ "durationMin": "Duration (min)",
+ "examDate": "Exam Date",
+ "back": "Back",
+ "save": "Save",
+ "saving": "Saving...",
+ "emptyContent": "Editor content is empty",
+ "titleRequired": "Please enter exam title",
+ "missingExamId": "Missing exam ID",
+ "classSubjectRequired": "Please enter class and subject",
+ "examDateRequired": "Please select exam date",
+ "saveSuccess": "Saved successfully",
+ "saveFailed": "Save failed: {message}",
+ "createSuccess": "Created successfully",
+ "createFailed": "Create failed: {message}",
+ "editorArea": "Editor",
+ "previewArea": "Preview",
+ "previewSummary": "{count} questions · {total} pts",
+ "emptyPreview": "Type in the editor on the left to see the preview here"
}
},
"homework": {
@@ -638,10 +820,184 @@
"suggestedScore": "Suggested",
"confidence": "Confidence"
},
+ "scanUploader": {
+ "scanTitle": "Upload Scans",
+ "dragDropHint": "Drag and drop images here to upload",
+ "fileTypesHint": "Supports JPG / PNG / WebP / PDF",
+ "uploading": "Uploading...",
+ "uploadFailed": "Upload failed",
+ "uploadSuccess": "Uploaded {count} scan images",
+ "selectImageFiles": "Please select image or PDF files",
+ "pageLabel": "Page {page}",
+ "moveUp": "Move Up",
+ "moveDown": "Move Down",
+ "deleteScan": "Delete Scan",
+ "noScans": "No scans"
+ },
+ "scanViewer": {
+ "noImages": "No scan images",
+ "noImagesHint": "Please upload student answer scans first",
+ "zoomIn": "Zoom In",
+ "zoomOut": "Zoom Out",
+ "rotate": "Rotate",
+ "fullscreen": "Fullscreen",
+ "pageIndicator": "Page {current}/{total}",
+ "answerImageAlt": "Answer image page {page}",
+ "thumbnailAlt": "Thumbnail page {page}"
+ },
+ "result": {
+ "scoreRate": "Score Rate",
+ "fullyGraded": "All questions have been graded",
+ "partiallyGraded": "Some questions are still pending grading",
+ "correctCount": "Correct",
+ "incorrectCount": "Incorrect",
+ "partialCount": "Partial",
+ "pendingCount": "Pending",
+ "wrongAnswersTitle": "Wrong Answers Preview",
+ "wrongAnswersDesc": "Below are incorrect or partially correct answers",
+ "backToList": "Back to List",
+ "viewErrorBook": "View Error Book",
+ "yourAnswer": "Your Answer",
+ "correctAnswer": "Correct Answer",
+ "teacherFeedback": "Teacher Feedback",
+ "correctAnswerTrue": "True",
+ "correctAnswerFalse": "False"
+ },
"error": {
"title": "Homework page error",
"unknown": "Unknown error",
"retry": "Retry"
+ },
+ "review": {
+ "gradedReport": "Graded Report",
+ "submissionDetails": "Submission Details",
+ "questionsUnit": "Questions",
+ "backToList": "Back to List",
+ "assignmentInfo": "Assignment Info",
+ "status": "Status",
+ "description": "Description",
+ "noDescription": "No description",
+ "totalScore": "Total Score",
+ "questionBreakdown": "Question Breakdown",
+ "responseSummary": "Response Summary"
+ },
+ "grade": {
+ "correct": "Correct",
+ "partial": "Partial",
+ "incorrect": "Incorrect"
+ },
+ "take": {
+ "back": "Back",
+ "questions": "Questions",
+ "notStarted": "Not Started",
+ "timedExam": "Timed Exam: {minutes} min",
+ "starting": "Starting...",
+ "startAssignment": "Start Assignment",
+ "timeRemaining": "Time Remaining",
+ "submitting": "Submitting...",
+ "submitAssignment": "Submit Assignment",
+ "saveFailed": "Save failed",
+ "startSuccess": "Assignment started",
+ "startFailed": "Failed to start",
+ "saved": "Saved",
+ "submitSuccess": "Submitted",
+ "submitFailed": "Failed to submit",
+ "timeUpAutoSubmit": "Time is up. Auto-submitted.",
+ "readyToStart": "Ready to Start",
+ "readyDescription": "Click the button below to start.",
+ "startNow": "Start Now",
+ "confirmSubmit": "Confirm Submit",
+ "unansweredWarning": "{count} question(s) unanswered",
+ "confirmSubmitDescription": "Are you sure you want to submit? You cannot modify after submission.",
+ "cancel": "Cancel",
+ "confirmSubmitAction": "Confirm Submit",
+ "assignmentInfo": "Assignment Info",
+ "status": "Status",
+ "dueDate": "Due Date",
+ "overdue": "Overdue",
+ "lessThanOneHour": "Less than 1 hour",
+ "hoursLeft": "{hours} hours left",
+ "attempts": "Attempts",
+ "attemptsUsed": "{used}/{max} used",
+ "attemptsRemaining": "{remaining} remaining",
+ "description": "Description",
+ "noDescription": "No description",
+ "progress": "Progress",
+ "jumpToQuestion": "Jump to question {index}",
+ "answered": "Answered",
+ "unanswered": "Unanswered",
+ "submitAll": "Submit All",
+ "makeSureAnswered": "Make sure all questions are answered",
+ "saveAnswer": "Save Answer",
+ "scanTitle": "Scan Upload",
+ "scanDescription": "Upload handwritten homework scans"
+ }
+ },
+ "examHomework": {
+ "homework": {
+ "analytics": {
+ "examContent": "Exam Content",
+ "questionPreview": "Question Preview",
+ "errorAnalysis": "Error Analysis",
+ "errorRateOverview": "Error Rate Overview",
+ "errorRateAriaLabel": "Error rate {rate}%",
+ "question": "Question",
+ "errors": "Errors",
+ "errorRateLabel": "Error Rate",
+ "wrongAnswersWithCount": "Wrong Answers ({count})",
+ "wrongAnswers": "Wrong Answers",
+ "noWrongAnswers": "No wrong answers recorded.",
+ "studentAnswer": "Student Answer",
+ "studentCount": "{count} student(s)",
+ "notAnswered": "Not answered",
+ "selectQuestionHint": "Select a question from the left",
+ "selectQuestionHintDesc": "to view error analysis",
+ "noGradedSubmissions": "No graded submissions yet."
+ },
+ "take": {
+ "true": "True",
+ "false": "False"
+ },
+ "excellent": {
+ "title": "Excellent Submissions",
+ "description": "Top submissions scoring {minPercentage}% or above in this assignment.",
+ "empty": "No excellent submissions yet.",
+ "emptyHint": "They will appear here after grading is complete.",
+ "studentAnon": "Student",
+ "rank": "Rank {rank}",
+ "lateTag": "Late",
+ "submittedAt": "Submitted on {date}",
+ "scoreValue": "{score} / {max}",
+ "percentage": "{value}%"
+ },
+ "form": {
+ "createTitle": "Create Assignment",
+ "quickMode": "Quick Assignment",
+ "quickModeDescription": "Enter title and description directly, no questions needed",
+ "examMode": "Exam-based Assignment",
+ "examModeDescription": "Derive assignment from an existing exam",
+ "class": "Class",
+ "selectClass": "Select a class",
+ "sourceExam": "Source Exam",
+ "selectExam": "Select an exam",
+ "assignmentTitle": "Assignment Title",
+ "titlePlaceholderQuick": "e.g. Recite Lesson 3",
+ "titlePlaceholderExam": "Defaults to exam title",
+ "description": "Description (optional)",
+ "descriptionPlaceholderQuick": "Enter assignment requirements, question content, or instructions...",
+ "availableAt": "Available At (optional)",
+ "dueAt": "Due At (optional)",
+ "allowLate": "Allow late submissions",
+ "lateDueAt": "Late Due At (optional)",
+ "maxAttempts": "Max Attempts",
+ "submit": "Create Assignment",
+ "submitting": "Creating...",
+ "creating": "Creating assignment...",
+ "selectExamRequired": "Please select an exam",
+ "titleRequired": "Please enter a title",
+ "selectClassRequired": "Please select a class",
+ "createFailed": "Failed to create"
+ }
}
},
"grades": {
@@ -733,7 +1089,18 @@
"colMinScore": "Min",
"colPassRate": "Pass Rate",
"colPassCount": "Pass Count",
- "colFailCount": "Fail Count"
+ "colFailCount": "Fail Count",
+ "noData": "No data",
+ "average": "Average",
+ "median": "Median",
+ "max": "Max",
+ "min": "Min",
+ "stdDev": "Std Dev",
+ "stdDevHint": "Reflects score dispersion",
+ "passRateHint": "Pass rate (>=60)",
+ "excellentRate": "Excellent Rate",
+ "excellentRateHint": "Excellent rate (>=85)",
+ "count": "Count"
},
"reportCard": {
"title": "Report Card",
@@ -762,7 +1129,53 @@
"error": {
"title": "Grades page error",
"unknown": "Unknown error",
- "retry": "Retry"
+ "retry": "Retry",
+ "createdSuccess": "Question created successfully",
+ "updatedSuccess": "Question updated successfully",
+ "unexpected": "An unexpected error occurred"
+ },
+ "classReport": {
+ "studentCountInfo": "Expected {studentCount} students · {recordCount} records entered",
+ "noDataTitle": "No class grade data",
+ "noDataDescription": "Please enter grades before viewing the class report",
+ "classRanking": "Class Ranking",
+ "caption": "Class grade ranking table",
+ "rankColumn": "Rank",
+ "recordsColumn": "Records"
+ },
+ "growthArchive": {
+ "title": "Student Growth Archive",
+ "description": "Covering {years} academic years · {records} records · {subjects} subjects",
+ "descriptionEmpty": "No growth archive data",
+ "emptyTitle": "No growth data",
+ "emptyDescription": "At least two semesters of grade data are needed to show growth trends",
+ "deltaUp": "Up {delta} points from last period",
+ "deltaDown": "Down {delta} points from last period",
+ "deltaStable": "Stable compared to last period",
+ "overallAverage": "Overall average {score}",
+ "averageScore": "Average Score",
+ "ariaLabelNonEmpty": "Growth trend chart with {count} data points",
+ "ariaLabelEmpty": "Growth trend chart is empty",
+ "statsAverage": "Average {score}",
+ "statsPassRate": "Pass rate {rate}%",
+ "statsRecords": "{count} records"
+ },
+ "knowledgePointMastery": {
+ "title": "Knowledge Point Mastery",
+ "description": "{count} knowledge points · average mastery {avg}%",
+ "descriptionEmpty": "No knowledge point mastery data",
+ "emptyTitle": "No mastery data",
+ "emptyDescription": "Practice or exam data is needed to calculate mastery",
+ "viewDetail": "View Details",
+ "ariaLabel": "Knowledge point mastery bar chart with {count} points",
+ "averageMastery": "Average Mastery",
+ "tooltipMastery": "Mastery: {value}%",
+ "tooltipStudents": "Mastered {mastered} / {total} students",
+ "weakPointsAriaLabel": "Weak knowledge points list",
+ "weakPointsTitle": "Weak Knowledge Points"
+ },
+ "summary": {
+ "averageScore": "Average Score"
}
},
"analytics": {
@@ -1001,7 +1414,261 @@
"error": {
"title": "AI module page error",
"unknown": "AI module encountered an unknown error",
- "retry": "Retry"
+ "retry": "Retry",
+ "invalidInput": "Invalid input data",
+ "chatFailed": "AI request failed",
+ "suggestionFailed": "AI suggestion failed",
+ "gradingFailed": "AI grading failed",
+ "contentFailed": "Content generation failed",
+ "variantFailed": "Question variant generation failed",
+ "analysisFailed": "Weakness analysis failed",
+ "statsFailed": "AI usage stats query failed",
+ "boundaryTitle": "AI Feature Error",
+ "boundaryDescription": "An error occurred while processing AI request. Please try again.",
+ "unauthorized": "You do not have permission to use AI features",
+ "providerNotConfigured": "AI provider not configured. Please contact administrator."
+ },
+ "chat": {
+ "title": "AI Assistant",
+ "placeholder": "Ask anything...",
+ "inputLabel": "Message input",
+ "send": "Send",
+ "thinking": "AI is thinking...",
+ "streaming": "AI is typing...",
+ "stopGeneration": "Stop generating",
+ "maxReached": "Maximum messages reached",
+ "clear": "Clear conversation",
+ "clearConfirm": "Clear all messages?",
+ "copy": "Copy",
+ "copied": "Copied!",
+ "suggestedPrompts": {
+ "title": "Try asking...",
+ "teacher": [
+ "Help me grade this question",
+ "Generate a classroom activity",
+ "Create a quiz question"
+ ],
+ "student": [
+ "Explain this concept",
+ "Give me a practice question",
+ "Help me study"
+ ],
+ "parent": [
+ "How is my child doing?",
+ "What should I focus on at home?"
+ ],
+ "admin": [
+ "Show AI usage stats",
+ "Which teachers use AI most?"
+ ],
+ "context": {
+ "teacherGrading": [
+ "What are common mistakes in this type of question?",
+ "How should I give constructive feedback?"
+ ],
+ "teacherLesson": [
+ "Suggest a hook for this lesson",
+ "What are some differentiation strategies?"
+ ],
+ "teacherExam": [
+ "Generate a question on this topic",
+ "Analyze the difficulty distribution"
+ ],
+ "studentHomework": [
+ "Give me a hint, not the answer",
+ "Help me understand this concept"
+ ]
+ }
+ },
+ "contextMessage": {
+ "teacherGrading": "Current page: Homework grading view",
+ "teacherLesson": "Current page: Lesson plan editor",
+ "teacherExam": "Current page: Exam builder",
+ "studentErrorBook": "Current page: Error book (student view)",
+ "studentHomework": "Current page: Student homework view",
+ "parent": "Current page: Parent dashboard",
+ "admin": "Current page: Admin dashboard"
+ }
+ },
+ "provider": {
+ "label": "AI Provider",
+ "placeholder": "Select provider",
+ "loading": "Loading providers...",
+ "default": "Default",
+ "description": "Select the AI configuration for this operation.",
+ "manage": "Manage",
+ "manageTitle": "AI Provider Settings",
+ "manageDescription": "Create a new provider or update existing configuration."
+ },
+ "suggestion": {
+ "title": "AI Suggestions",
+ "generate": "Generate Suggestions",
+ "regenerate": "Regenerate",
+ "loading": "AI is thinking...",
+ "empty": "No suggestions available",
+ "error": "Failed to generate suggestions",
+ "loaded": "Suggestions loaded",
+ "selected": "Suggestion selected",
+ "select": "Select",
+ "difficulty": "Difficulty",
+ "practiceNow": "Practice Now",
+ "addAll": "Add All"
+ },
+ "grading": {
+ "title": "AI Grading Suggestion",
+ "description": "AI-powered scoring and feedback for subjective questions",
+ "suggestedScore": "Suggested Score",
+ "confidence": "Confidence",
+ "feedback": "Feedback",
+ "reasoning": "Reasoning",
+ "applyScore": "Apply Score",
+ "applyFeedback": "Apply Feedback",
+ "loading": "AI is grading...",
+ "error": "AI grading failed",
+ "notAvailable": "AI grading not available for this question type",
+ "batchTitle": "Batch AI Grading",
+ "batchDescription": "Generate AI suggestions for all subjective questions at once",
+ "batchGenerate": "Generate All Suggestions",
+ "batchProgress": "Processing {done}/{total}",
+ "currentScore": "Current Score",
+ "scoreDifference": "Difference"
+ },
+ "errorBook": {
+ "similarQuestions": "Similar Questions",
+ "weaknessAnalysis": "Weakness Analysis",
+ "studyPlan": "Study Plan",
+ "recommendedResources": "Recommended Resources",
+ "weakAreas": "Weak Areas",
+ "severity": {
+ "high": "High",
+ "medium": "Medium",
+ "low": "Low"
+ }
+ },
+ "lessonPrep": {
+ "generateContent": "Generate Content",
+ "description": "AI-powered teaching content generation",
+ "generateActivity": "Suggest Activity",
+ "generateAssessment": "Generate Assessment",
+ "generateQuestion": "Generate Discussion Question",
+ "loading": "Generating...",
+ "error": "Content generation failed",
+ "additionalContext": "Additional context",
+ "additionalContextPlaceholder": "Add any specific requirements or context...",
+ "insertContent": "Insert Content",
+ "editBeforeInsert": "Edit before insert",
+ "history": "Generation History",
+ "clearHistory": "Clear history"
+ },
+ "exam": {
+ "generate": "Generate",
+ "generating": "Generating...",
+ "preview": "Preview",
+ "queue": "Add to Queue",
+ "queueRunning": "Running",
+ "queueQueued": "Queued",
+ "backgroundTasks": "Background Tasks",
+ "taskStatus": {
+ "queued": "Queued",
+ "running": "Running",
+ "success": "Completed",
+ "failed": "Failed"
+ },
+ "openPreview": "Open Preview",
+ "sourceText": "Source Exam Text",
+ "sourceTextPlaceholder": "Paste the full exam text to parse into questions.",
+ "sourceTextDesc": "AI will extract questions and structure from this text.",
+ "generationTitle": "AI Generation",
+ "generationDesc": "Paste the exam text and generate a structured preview.",
+ "variantType": {
+ "label": "Variant type",
+ "same_knowledge_point": "Same knowledge point, different context",
+ "different_difficulty": "Different difficulty level",
+ "different_format": "Different question format"
+ },
+ "targetDifficulty": "Target difficulty",
+ "addVariant": "Add Variant"
+ },
+ "parent": {
+ "summary": "AI Learning Summary",
+ "summaryDescription": "AI-generated overview of your child's learning progress",
+ "generateSummary": "Generate Summary",
+ "weaknessHint": "Areas to focus on",
+ "suggestion": "Family tutoring suggestion",
+ "loading": "Generating summary...",
+ "error": "Failed to generate summary"
+ },
+ "admin": {
+ "usageDashboard": "AI Usage Dashboard",
+ "dashboardDescription": "Monitor AI usage across the school",
+ "totalCalls": "Total AI Calls",
+ "activeUsers": "Active Users",
+ "costEstimate": "Estimated Cost",
+ "topUsers": "Top Users",
+ "byCapability": "By Capability",
+ "byRole": "By Role",
+ "recentActivity": "Recent Activity",
+ "noData": "No AI usage data available",
+ "callsToday": "Calls today",
+ "callsThisWeek": "Calls this week",
+ "errorRate": "Error rate",
+ "avgDuration": "Avg duration",
+ "settings": {
+ "title": "AI Settings",
+ "description": "Manage AI providers, API keys, and usage statistics.",
+ "descriptionAdmin": "Manage AI providers, API keys, and usage statistics (Admin view).",
+ "descriptionUser": "Manage your AI providers and API keys."
+ }
+ },
+ "studyPath": {
+ "title": "Your Learning Path",
+ "description": "AI-personalized learning recommendations",
+ "nextSteps": "Recommended Next Steps",
+ "mastered": "Mastered",
+ "inProgress": "In Progress",
+ "needsWork": "Needs Work",
+ "generate": "Generate Learning Path",
+ "loading": "Generating learning path...",
+ "error": "Failed to generate learning path",
+ "startLearning": "Start Learning"
+ },
+ "widget": {
+ "title": "AI Assistant",
+ "open": "Open AI Assistant",
+ "close": "Close",
+ "contextAware": "Context-aware",
+ "dragHint": "Drag to move · Long press edge to hide",
+ "hidden": "Hidden",
+ "show": "Show",
+ "hide": "Hide",
+ "resetPosition": "Reset position",
+ "welcome": "Hi, I'm your AI Assistant",
+ "welcomeDesc": "How can I help you today?",
+ "newChat": "New chat",
+ "history": "History",
+ "online": "Online",
+ "tokens": "{count} chars"
+ },
+ "safety": {
+ "blocked": "Your message was blocked by the safety filter. Please keep the conversation educational.",
+ "dailyLimit": "Daily AI usage limit reached. Please try again tomorrow.",
+ "studentMode": "AI is in student mode. It will guide you to find the answer.",
+ "contentFiltered": "Inappropriate content was filtered from the AI response."
+ },
+ "capability": {
+ "chat": "AI Chat",
+ "examGenerate": "AI Exam Generation",
+ "gradingAssist": "AI Grading Assist",
+ "lessonContent": "AI Lesson Content",
+ "questionVariant": "AI Question Variant",
+ "similarQuestion": "AI Similar Questions",
+ "weaknessAnalysis": "AI Weakness Analysis",
+ "childSummary": "AI Child Summary",
+ "studyPath": "AI Study Path",
+ "explainError": "AI Error Explanation"
+ },
+ "chart": {
+ "parseError": "Chart data format error, cannot render"
}
},
"attendance": {
@@ -1030,7 +1697,12 @@
"colStatus": "Status",
"colRemark": "Remark",
"colRecordedBy": "Recorded by",
- "colUpdatedAt": "Updated"
+ "colUpdatedAt": "Updated",
+ "columns": {
+ "student": "Student",
+ "reason": "Reason",
+ "status": "Status"
+ }
},
"sheet": {
"title": "Attendance Sheet",
@@ -1049,7 +1721,14 @@
"errorNoEntries": "No entries to save",
"success": "Sheet saved successfully",
"error": "Save failed",
- "loadFailed": "Load failed: {message}"
+ "loadFailed": "Load failed: {message}",
+ "selectClass": "Please select a class",
+ "reasonPlaceholder": "Enter reason",
+ "saved": "Attendance sheet saved",
+ "saving": "Saving...",
+ "noStudents": "No students in this class",
+ "confirmClassSwitch": "Switching class will discard unsaved changes. Confirm?",
+ "confirmClassSwitchAction": "Confirm switch"
},
"report": {
"title": "Attendance Report",
@@ -1070,7 +1749,31 @@
"colAbsent": "Absent",
"colLate": "Late",
"colLeave": "Leave",
- "colAttendanceRate": "Attendance rate"
+ "colAttendanceRate": "Attendance rate",
+ "printing": "Printing...",
+ "relationship": "Relationship",
+ "period": "Period",
+ "parentSignature": "Parent signature",
+ "type": "Report type",
+ "noData": "No report data",
+ "controls": "Report controls",
+ "noDataDescription": "Adjust filters and try again",
+ "parentComment": "Parent comments",
+ "studentDetails": "Student details",
+ "footer": "This report is auto-generated by the system, for reference only.",
+ "generatedAt": "Generated at",
+ "signature": "Signature",
+ "parentName": "Parent name",
+ "summary": "Summary",
+ "startDate": "Start date",
+ "endDate": "End date",
+ "print": "Print",
+ "parentSignatureNotice": "Please review the attendance records above and sign to confirm.",
+ "signDate": "Sign date",
+ "types": {
+ "weekly": "Weekly",
+ "monthly": "Monthly"
+ }
},
"stats": {
"title": "Attendance Stats",
@@ -1095,7 +1798,45 @@
"error": {
"title": "Attendance module error",
"unknown": "Unknown attendance module error",
- "retry": "Retry"
+ "retry": "Retry",
+ "createdSuccess": "Question created successfully",
+ "updatedSuccess": "Question updated successfully",
+ "unexpected": "An unexpected error occurred"
+ },
+ "filters": {
+ "class": "Class",
+ "date": "Date"
+ },
+ "description": {
+ "teacherRecords": "Teachers record student daily attendance"
+ },
+ "actions": {
+ "cancel": "Cancel",
+ "save": "Save",
+ "markAllPresent": "Mark all present"
+ },
+ "errors": {
+ "invalidForm": "Form has invalid fields",
+ "unexpected": "Operation failed, please try again later"
+ },
+ "period": {
+ "label": "Period",
+ "full_day": "Full day",
+ "morning_reading": "Morning reading",
+ "morning": "Morning",
+ "afternoon": "Afternoon",
+ "evening": "Evening"
+ },
+ "rules": {
+ "title": "Attendance Rules",
+ "lateThreshold": "Late threshold (minutes)",
+ "earlyLeaveThreshold": "Early leave threshold (minutes)",
+ "enableAutoMark": "Enable auto mark",
+ "attendanceRateThreshold": "Attendance rate threshold (%)",
+ "attendanceRateThresholdHint": "Alert triggered below this threshold",
+ "consecutiveAbsenceThreshold": "Consecutive absence threshold (count)",
+ "consecutiveAbsenceThresholdHint": "Alert triggered above this count",
+ "saved": "Rules saved"
}
},
"questions": {
@@ -1137,7 +1878,121 @@
"error": {
"title": "Question page error",
"unknown": "Unknown error",
- "retry": "Retry"
+ "retry": "Retry",
+ "createdSuccess": "Question created successfully",
+ "updatedSuccess": "Question updated successfully",
+ "unexpected": "An unexpected error occurred"
+ },
+ "content": {
+ "empty": "No question content",
+ "answer": "Answer",
+ "explanation": "Explanation"
+ },
+ "actions": {
+ "menuLabel": "Actions",
+ "viewDetails": "View Details",
+ "delete": "Delete",
+ "deleteConfirmTitle": "Confirm Delete",
+ "deleteConfirmDesc": "Are you sure you want to delete this question? This action cannot be undone.",
+ "deleteConfirmCancel": "Cancel",
+ "deleting": "Deleting...",
+ "deleteSuccess": "Deleted successfully",
+ "deleteFailed": "Delete failed",
+ "copyId": "Copy ID",
+ "copyIdSuccess": "Copied to clipboard",
+ "copyIdFailed": "Copy failed",
+ "close": "Close",
+ "detailsTitle": "Question Details",
+ "detailsId": "Question ID",
+ "detailsType": "Type",
+ "detailsDifficulty": "Difficulty",
+ "detailsContent": "Content"
+ },
+ "batch": {
+ "selected": "{count} selected",
+ "delete": "Batch Delete",
+ "deleteConfirmTitle": "Confirm Batch Delete",
+ "deleteConfirmDesc": "Are you sure you want to delete the selected {count} question(s)? This action cannot be undone.",
+ "deleting": "Deleting...",
+ "deleteSuccess": "Batch delete successful",
+ "deleteFailed": "Batch delete failed",
+ "cancel": "Cancel",
+ "clear": "Clear selection",
+ "deleteConfirmAction": "Confirm Delete"
+ },
+ "cascade": {
+ "textbook": "Textbook",
+ "textbookAll": "All textbooks",
+ "chapter": "Chapter",
+ "chapterAll": "All chapters",
+ "knowledgePoint": "Knowledge Point",
+ "knowledgePointAll": "All knowledge points",
+ "loading": "Loading..."
+ },
+ "importExport": {
+ "import": "Import",
+ "export": "Export",
+ "importing": "Importing...",
+ "exporting": "Exporting...",
+ "importSuccess": "Import successful",
+ "importFailed": "Import failed",
+ "exportSuccess": "Export successful",
+ "exportFailed": "Export failed",
+ "confirmTitle": "Confirm Import",
+ "confirmDesc": "This will overwrite existing questions. Continue?",
+ "cancel": "Cancel",
+ "invalidFile": "Invalid file format",
+ "readFailed": "Failed to read file",
+ "confirmImport": "Confirm Import"
+ },
+ "table": {
+ "type": "Type",
+ "content": "Content",
+ "difficulty": "Difficulty",
+ "knowledgePoints": "Knowledge Points",
+ "created": "Created At",
+ "noResults": "No results"
+ },
+ "dialog": {
+ "editTitle": "Edit Question",
+ "createTitle": "Create Question",
+ "editDesc": "Edit the question details below.",
+ "createDesc": "Fill in the question details below.",
+ "questionType": "Question Type",
+ "difficulty": "Difficulty",
+ "questionContent": "Question Content",
+ "contentPlaceholder": "Enter question content...",
+ "answer": "Answer",
+ "explanation": "Explanation",
+ "cancel": "Cancel",
+ "updating": "Updating...",
+ "creating": "Creating...",
+ "update": "Update",
+ "create": "Create",
+ "addOption": "Add Option",
+ "markCorrect": "Mark as Correct",
+ "options": "Options",
+ "loading": "Loading...",
+ "knowledgePoints": "Knowledge Points",
+ "knowledgePointsOptional": "Knowledge Points (optional)",
+ "knowledgePointsSelected": "{count} knowledge points selected",
+ "noKnowledgePoints": "No knowledge points",
+ "searchKnowledgePoints": "Search knowledge points...",
+ "optionPlaceholder": "Option {index}"
+ },
+ "type": {
+ "single_choice": "Single Choice",
+ "multiple_choice": "Multiple Choice",
+ "judgment": "True / False",
+ "text": "Short Answer",
+ "composite": "Composite"
+ },
+ "difficulty": {
+ "1": "Easy",
+ "2": "Medium",
+ "3": "Hard",
+ "4": "Very Hard",
+ "5": "Expert"
}
},
"textbooks": {
@@ -1185,12 +2040,151 @@
"colChapterStatus": "Status",
"noChapters": "No chapters",
"noChaptersAction": "Back to textbook list",
- "chaptersMswNotice": "Chapter list query contract pending, currently served by MSW."
+ "chaptersMswNotice": "Chapter list query contract pending, currently served by MSW.",
+ "sectionKnowledgeGraph": "Knowledge Graph"
},
"error": {
"title": "Textbook page error",
"unknown": "Unknown error",
"retry": "Retry"
+ },
+ "graph": {
+ "viewModeStructure": "Structure",
+ "viewModeStudentMastery": "Student Mastery",
+ "viewModeClassMastery": "Class Mastery",
+ "searchPlaceholder": "Search knowledge point name...",
+ "resetView": "Reset View",
+ "refreshing": "Refreshing...",
+ "refresh": "Refresh",
+ "edit": "Edit",
+ "delete": "Delete",
+ "createQuestion": "Create Question",
+ "questionCount": "{count} question(s)",
+ "layout": {
+ "hierarchical": "Hierarchical",
+ "force": "Force-directed"
+ },
+ "error": {
+ "loadFailed": "Failed to load knowledge graph"
+ },
+ "node": {
+ "questions": "Questions",
+ "mastery": "Mastery",
+ "prerequisite": "Prerequisites",
+ "successor": "Successors"
+ },
+ "detail": {
+ "title": "Knowledge Point Detail",
+ "close": "Close",
+ "description": "Description",
+ "noDescription": "No description",
+ "correctRate": "Correct Rate",
+ "masteryNotAssessed": "Not assessed",
+ "totalQuestions": "Total Questions",
+ "viewAllQuestions": "View all questions",
+ "addPrerequisite": "Add prerequisite",
+ "removePrerequisite": "Remove prerequisite",
+ "noPrerequisites": "No prerequisites",
+ "noSuccessors": "No successors",
+ "prerequisiteRemoveFailed": "Failed to remove prerequisite"
+ }
+ },
+ "knowledge": {
+ "title": "Knowledge Points",
+ "create": "Create Knowledge Point",
+ "empty": "No knowledge points",
+ "difficulty": "Difficulty {level}"
+ },
+ "card": {
+ "gradeNA": "N/A",
+ "version": "v{version}",
+ "moreOptions": "More options",
+ "editContent": "Edit Content",
+ "delete": "Delete"
+ },
+ "reader": {
+ "contents": "Contents",
+ "noChapters": "No chapters",
+ "selectChapter": "Select a chapter to read",
+ "chapters": "Chapters",
+ "deleteFailed": "Failed to delete textbook",
+ "emptyKnowledge": "No knowledge points",
+ "emptyKnowledgeDesc": "No knowledge points have been entered for this textbook yet. Please create knowledge points under chapters first.",
+ "loadingKnowledge": "Loading knowledge points..."
+ },
+ "action": {
+ "updateNotSupported": "Update not supported (contract pending).",
+ "kpCreateNotSupported": "Creating knowledge points not supported (contract pending).",
+ "deleteNotSupported": "Delete not supported (contract pending).",
+ "updateFailedGeneric": "Update failed",
+ "deleteFailed": "Delete failed",
+ "errorOccurred": "An error occurred",
+ "prerequisiteAddNotSupported": "Adding prerequisites not supported (dialog pending migration).",
+ "prerequisiteDeleteNotSupported": "Deleting prerequisites not supported (mutation pending)."
+ },
+ "dialog": {
+ "chapter": {
+ "createTitle": "Create Chapter",
+ "titlePlaceholder": "Chapter title...",
+ "cancel": "Cancel",
+ "create": "Create",
+ "cannotDeleteWithSubchapters": "Cannot delete a chapter that has subchapters. Please delete subchapters first."
+ },
+ "knowledge": {
+ "editTitle": "Edit Knowledge Point",
+ "nameLabel": "Name",
+ "cancel": "Cancel",
+ "saving": "Saving...",
+ "save": "Save",
+ "deleteTitle": "Delete Knowledge Point",
+ "deleteDesc": "Are you sure you want to delete this textbook? This action cannot be undone.",
+ "delete": "Delete"
+ },
+ "settings": {
+ "title": "Textbook Settings",
+ "save": "Save",
+ "deleteConfirmTitle": "Confirm Delete",
+ "deleteConfirmDesc": "Are you sure you want to delete this textbook? This action cannot be undone.",
+ "processing": "Processing...",
+ "delete": "Delete"
+ },
+ "textbook": {
+ "titleLabel": "Title",
+ "titlePlaceholder": "Enter textbook title...",
+ "subjectLabel": "Subject",
+ "subjectPlaceholder": "Select subject",
+ "gradeLabel": "Grade",
+ "gradePlaceholder": "Select grade",
+ "versionLabel": "Version",
+ "versionPlaceholder": "Enter version...",
+ "editTitle": "Edit Textbook",
+ "createTitle": "Create Textbook",
+ "save": "Save"
+ }
+ },
+ "subject": {
+ "chinese": "Chinese",
+ "mathematics": "Mathematics",
+ "physics": "Physics",
+ "chemistry": "Chemistry",
+ "biology": "Biology",
+ "english": "English",
+ "history": "History",
+ "geography": "Geography"
+ },
+ "grade": {
+ "grade1": "Grade 1",
+ "grade2": "Grade 2",
+ "grade3": "Grade 3",
+ "grade4": "Grade 4",
+ "grade5": "Grade 5",
+ "grade6": "Grade 6",
+ "grade7": "Grade 7",
+ "grade8": "Grade 8",
+ "grade9": "Grade 9",
+ "grade10": "Grade 10",
+ "grade11": "Grade 11",
+ "grade12": "Grade 12"
}
},
"lessonPlans": {
@@ -1309,7 +2303,184 @@
"error": {
"title": "Lesson plan page error",
"unknown": "An unknown error occurred",
- "retry": "Retry"
+ "retry": "Retry",
+ "createdSuccess": "Question created successfully",
+ "updatedSuccess": "Question updated successfully",
+ "unexpected": "An unexpected error occurred"
+ },
+ "action": {
+ "close": "Close",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "delete": "Delete"
+ },
+ "attachment": {
+ "title": "Attachment Library",
+ "add": "Upload Attachment",
+ "libraryLabel": "Library",
+ "loading": "Loading...",
+ "empty": "No attachments",
+ "delete": "Delete",
+ "uploadFailed": "Upload failed",
+ "uploadSuccess": "Upload success",
+ "type": {
+ "material": "Material",
+ "resource": "Resource",
+ "other": "Other"
+ }
+ },
+ "knowledgePoint": {
+ "title": "Select Knowledge Points",
+ "empty": "No knowledge points"
+ },
+ "consistency": {
+ "title": "Consistency Check",
+ "loading": "Checking...",
+ "allPassed": "All passed",
+ "hasErrors": "Has errors",
+ "hasWarnings": "Has warnings"
+ },
+ "aiDifferentiation": {
+ "title": "AI Differentiated Instruction",
+ "loading": "AI analyzing...",
+ "empty": "No suggestions",
+ "covered": "{count} covered",
+ "missed": "{count} missed",
+ "tabs": {
+ "differentiation": "Differentiation",
+ "curriculum": "Curriculum Alignment",
+ "assessment": "Assessment"
+ },
+ "level": {
+ "basic": "Basic",
+ "intermediate": "Intermediate",
+ "advanced": "Advanced"
+ }
+ },
+ "feedback": {
+ "title": "AI Feedback",
+ "loading": "AI analyzing...",
+ "empty": "No feedback",
+ "apply": "Apply",
+ "category": {
+ "strengths": "Strengths",
+ "improvements": "Improvements",
+ "alignment": "Alignment",
+ "differentiation": "Differentiation"
+ }
+ },
+ "banner": {
+ "anchorMigration": "Legacy anchor data detected, automatically migrated to new format."
+ },
+ "exercise": {
+ "difficulty": "Difficulty",
+ "contentPlaceholder": "Enter question content...",
+ "questionId": "Question #{id}",
+ "inlineQuestion": "Inline Question",
+ "questionType": {
+ "single_choice": "Single Choice",
+ "multiple_choice": "Multiple Choice",
+ "text": "Text",
+ "judgment": "Judgment",
+ "composite": "Composite"
+ }
+ },
+ "detail": {
+ "selectNodeHint": "Select a node on the left to view details",
+ "aiAssist": "AI Assistant",
+ "aiGenerateLayered": "Generate Layered Exercises",
+ "aiFillExpected": "Fill Expected Answers",
+ "aiOptimizeFollowup": "Optimize Follow-up",
+ "aiGenerate": "AI Generate",
+ "aiOptimize": "AI Optimize",
+ "aiDifferentiation": "AI Differentiation",
+ "designIntent": "Design Intent",
+ "qaDialog": "Q&A Dialog",
+ "addTurn": "Add Turn",
+ "turnTeacher": "Teacher",
+ "turnStudent": "Student",
+ "round": "Round {n}",
+ "turnContentPlaceholder": "Enter content...",
+ "expectedAnswer": "Expected answer...",
+ "stageLabel": "Stage",
+ "differentiationLabel": "Differentiation Level",
+ "titleLabel": "Title",
+ "stageNone": "(none)",
+ "typeLabel": "Type",
+ "stage": {
+ "import": "Import",
+ "new_teaching": "New Teaching",
+ "consolidation": "Consolidation",
+ "summary": "Summary"
+ }
+ },
+ "version": {
+ "historyTitle": "Version History",
+ "empty": "No history versions",
+ "auto": "Auto-save",
+ "revert": "Revert",
+ "compare": "Compare",
+ "diffCount": "{count} differences"
+ },
+ "dialog": {
+ "versions": "Version History",
+ "print": "Print Preview",
+ "schedule": "Schedule",
+ "consistency": "Consistency Check",
+ "aiFeedback": "AI Feedback",
+ "aiDifferentiation": "AI Differentiation"
+ },
+ "homework": {
+ "type": {
+ "exercise": "Exercise",
+ "reading": "Reading",
+ "writing": "Writing"
+ }
+ },
+ "blackboard": {
+ "layout": {
+ "structure": "Structure",
+ "mindmap": "Mind Map",
+ "text": "Text"
+ }
+ },
+ "keyPoint": {
+ "type": {
+ "key": "Key Point",
+ "difficult": "Difficult Point"
+ }
+ },
+ "import": {
+ "method": {
+ "question": "Question",
+ "situation": "Situation",
+ "review": "Review",
+ "other": "Other"
+ }
+ },
+ "objective": {
+ "dimension": {
+ "knowledge": "Knowledge & Skills",
+ "process": "Process & Methods",
+ "emotion": "Emotional Attitudes & Values"
+ }
+ },
+ "reflection": {
+ "aspect": {
+ "effectiveness": "Effectiveness",
+ "problems": "Problems",
+ "improvements": "Improvements"
+ }
+ },
+ "newTeaching": {
+ "pointIndex": "Point {index}"
+ },
+ "paper": {
+ "textbookHeader": "Textbook Content",
+ "textbookPlaceholder": "Enter textbook content...",
+ "expandedCount": "Expanded: {count} nodes",
+ "insertNode": "Insert Node",
+ "consistencyCheck": "Consistency Check"
}
},
"coursePlans": {
@@ -1333,7 +2504,113 @@
"colUpdatedAt": "Updated",
"colActions": "Actions",
"viewDetail": "View Detail →",
- "mswNotice": "List query contract pending; ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "List query contract pending; ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "new": "New Plan",
+ "empty": "No course plans",
+ "emptyFiltered": "No matching plans",
+ "noClass": "Unassigned class",
+ "unknownSubject": "Unknown subject",
+ "semester": "Semester {semester}",
+ "created": "Created on {date}"
+ },
+ "progress": {
+ "label": "Progress",
+ "hours": "{completed}/{total} hours · {percent}%",
+ "weekPlansCompleted": "{completed}/{total} week plans completed"
+ },
+ "status": {
+ "planning": "Planning",
+ "active": "Active",
+ "completed": "Completed",
+ "paused": "Paused"
+ },
+ "filter": {
+ "placeholder": "Filter by status",
+ "all": "All statuses"
+ },
+ "calendar": {
+ "title": "Teaching Calendar",
+ "weekShort": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ],
+ "monthTitle": "{month} {year}",
+ "prevMonth": "Previous month",
+ "nextMonth": "Next month",
+ "today": "Today",
+ "week": "Week {week}",
+ "hours": "{hours} hours",
+ "completed": "Completed",
+ "pending": "Pending",
+ "noStartDate": "Course plan has no start date, calendar view unavailable",
+ "noPlans": "No teaching plans this month"
+ },
+ "form": {
+ "new": "New Course Plan",
+ "edit": "Edit Course Plan",
+ "class": "Class",
+ "selectClass": "Select class",
+ "subject": "Subject",
+ "selectSubject": "Select subject",
+ "teacher": "Teacher",
+ "selectTeacher": "Select teacher",
+ "academicYear": "Academic Year",
+ "optional": "Optional",
+ "semester": "Semester",
+ "semester1": "First Semester",
+ "semester2": "Second Semester",
+ "status": "Status",
+ "selectStatus": "Select status",
+ "totalHours": "Total Hours",
+ "weeklyHours": "Weekly Hours",
+ "startDate": "Start Date",
+ "endDate": "End Date",
+ "syllabus": "Syllabus",
+ "syllabusPlaceholder": "Syllabus and scope...",
+ "objectives": "Objectives",
+ "objectivesPlaceholder": "Teaching objectives and expected outcomes...",
+ "cancel": "Cancel",
+ "create": "Create",
+ "save": "Save",
+ "saving": "Saving...",
+ "invalidState": "Invalid form state",
+ "saveFailed": "Save failed"
+ },
+ "templates": {
+ "title": "Create from Template",
+ "createFromTemplate": "Copy from existing plan",
+ "searchPlaceholder": "Search by class, subject, or teacher...",
+ "empty": "No templates available",
+ "cancel": "Cancel",
+ "confirm": "Copy",
+ "cloning": "Cloning...",
+ "cloneSuccess": "New plan created from template",
+ "cloneFailed": "Clone failed"
+ },
+ "export": {
+ "csv": "Export CSV",
+ "filename": "{subject}_{className}_CoursePlan",
+ "content": "Content",
+ "notes": "Notes",
+ "exported": "Exported",
+ "exportFailed": "Export failed"
+ },
+ "toast": {
+ "deleted": "Course plan deleted",
+ "deleteFailed": "Delete failed",
+ "bulkMarked": "Bulk marked {count} items",
+ "bulkFailed": "Bulk operation failed"
+ },
+ "bulk": {
+ "markComplete": "Bulk mark complete"
+ },
+ "loading": {
+ "title": "Loading..."
},
"detail": {
"title": "Course Plan Detail",
@@ -1359,7 +2636,71 @@
"colUnitOrder": "#",
"colUnitTitle": "Unit Title",
"colUnitProgress": "Progress",
- "colUnitStatus": "Status"
+ "colUnitStatus": "Status",
+ "back": "Back",
+ "heading": "Course Plan Details",
+ "edit": "Edit",
+ "delete": "Delete",
+ "deleteTitle": "Delete Course Plan",
+ "deleteDescription": "Are you sure you want to delete this course plan? This action cannot be undone.",
+ "week": "Week",
+ "topic": "Topic",
+ "hours": "Hours",
+ "chapter": "Textbook Chapter",
+ "statusCol": "Status",
+ "weekPlans": "Weekly Plans",
+ "addWeekPlan": "Add Weekly Plan",
+ "emptyWeekPlans": "No weekly plans",
+ "emptyWeekPlansCta": ". Click the button above to add",
+ "reorderSaved": "Reorder saved",
+ "reorderFailed": "Reorder failed",
+ "noClass": "Unassigned class",
+ "unknownSubject": "Unknown subject",
+ "unknownSubjectHeading": "Unknown Subject",
+ "semester": "Semester {semester}",
+ "teacher": "Teacher: {name}",
+ "unassigned": "Unassigned teacher",
+ "created": "Created on {date}",
+ "startDate": "Starts on {date}",
+ "endDate": "Ends on {date}",
+ "syllabus": "Syllabus",
+ "objectives": "Objectives",
+ "completed": "Completed",
+ "pending": "Pending",
+ "selectWeekAria": "Select week {week}",
+ "viewHomeworkAria": "View homework",
+ "viewTextbookAria": "View textbook chapter {chapter}",
+ "dragHandle": "Drag to reorder",
+ "notes": "Notes: {notes}"
+ },
+ "item": {
+ "addTitle": "New Week Plan",
+ "editTitle": "Edit Week Plan",
+ "week": "Week",
+ "hours": "Hours",
+ "topic": "Topic",
+ "topicPlaceholder": "Enter this week's topic",
+ "content": "Content",
+ "contentPlaceholder": "Enter this week's teaching content",
+ "chapter": "Textbook Chapter",
+ "chapterPlaceholder": "e.g., Chapter 3",
+ "completedAt": "Completed Date",
+ "notes": "Notes",
+ "notesPlaceholder": "Optional notes",
+ "cancel": "Cancel",
+ "save": "Save",
+ "saving": "Saving...",
+ "delete": "Delete",
+ "markComplete": "Mark Complete",
+ "markIncomplete": "Mark Incomplete",
+ "invalidState": "Invalid form state",
+ "saveFailed": "Save failed",
+ "deleteFailed": "Delete failed",
+ "updateFailed": "Update failed",
+ "createSuccess": "Week plan created",
+ "updateSuccess": "Week plan updated",
+ "deleteSuccess": "Week plan deleted",
+ "toggleSuccess": "Completion status updated"
},
"error": {
"title": "Course Plans module error",
@@ -1441,11 +2782,458 @@
"error": {
"title": "Diagnostic page error",
"unknown": "An unknown error occurred in the diagnostic module",
- "retry": "Retry"
+ "retry": "Retry",
+ "generateClassFailed": "Failed to generate class diagnostic report",
+ "loadFailed": "Data loading failed"
+ },
+ "classDiagnostic": {
+ "noClassDataTitle": "No Class Diagnostic Data",
+ "heatmapDescription": "Darker colors indicate lower mastery. Click a cell for details.",
+ "heatmapAriaLabel": "Knowledge point mastery heatmap, {count} knowledge points",
+ "heatmapCellAriaLabel": "{name}: {level}%, {label}, {mastered} mastered / {total} total",
+ "masteryLevelExcellent": "Excellent",
+ "masteryLevelGood": "Good",
+ "masteryLevelNeedsImprovement": "Needs Improvement",
+ "masteryLevelWeak": "Weak",
+ "legendLabel": "Legend:",
+ "filterByKpTitle": "Filter Students by Knowledge Point",
+ "filterByKpDescription": "Select a knowledge point to view all students mastery of it",
+ "kpFilterLabel": "Knowledge Point",
+ "kpFilterPlaceholder": "Select a knowledge point",
+ "kpFilterAll": "All Knowledge Points",
+ "filtering": "Filtering...",
+ "avgMasteryColumn": "Average Mastery",
+ "totalQuestionsColumn": "Total Questions",
+ "correctQuestionsColumn": "Correct Answers",
+ "statusColumn": "Status",
+ "needsAttention": "Needs Attention",
+ "mastered": "Mastered",
+ "viewAriaLabel": "View {studentName} diagnostic detail",
+ "viewAction": "View",
+ "noStudentsForKp": "No student data for this knowledge point",
+ "knowledgePointColumn": "Knowledge Point",
+ "masteredColumn": "Mastered Count",
+ "notMasteredColumn": "Not Mastered Count",
+ "noRankingData": "No ranking data",
+ "studentsNeedingAttentionTitle": "Students Needing Attention",
+ "studentsNeedingAttentionDescription": "Students with mastery below threshold",
+ "allStudentsAboveThreshold": "All students mastery above threshold",
+ "weakPointsColumn": "Weak Points Count",
+ "generateDescription": "Select period and generate class diagnostic report",
+ "periodLabel": "Period",
+ "generating": "Generating...",
+ "generateButton": "Generate Report"
+ },
+ "empty": {
+ "noClassData": "No class assigned or no diagnostic data for the class",
+ "noData": "No diagnostic data"
+ },
+ "summary": {
+ "class": "Class",
+ "students": "Students",
+ "avgMastery": "Average Mastery",
+ "needAttention": "Need Attention",
+ "student": "Student",
+ "overallMastery": "Overall Mastery",
+ "strengths": "Strengths",
+ "weaknesses": "Weaknesses"
+ },
+ "chart": {
+ "heatmapTitle": "Knowledge Point Mastery Heatmap",
+ "rankingTitle": "Knowledge Point Mastery Ranking",
+ "radarTitle": "Knowledge Point Mastery Radar",
+ "radarDescriptionNonEmpty": "Student score vs class average",
+ "radarEmptyTitle": "No Mastery Data",
+ "radarAriaLabelEmpty": "Radar chart is empty, no mastery data for student",
+ "radarAriaLabelNonEmpty": "Radar chart, {count} knowledge points{withClassAverage}",
+ "withClassAverage": "(including class average)",
+ "studentSeries": "Student",
+ "classAvgSeries": "Class Average",
+ "noMasteryDataForStudent": "No mastery data for this student"
+ },
+ "strengths": {
+ "title": "Strength Knowledge Points"
+ },
+ "weaknesses": {
+ "title": "Weak Knowledge Points",
+ "practice": "Practice"
+ },
+ "status": {
+ "draft": "Draft",
+ "published": "Published",
+ "archived": "Archived"
+ },
+ "type": {
+ "individual": "Individual",
+ "class": "Class",
+ "grade": "Grade"
+ },
+ "report": {
+ "generateClass": "Generate Class Diagnostic Report",
+ "recommendations": "Recommendations",
+ "history": "Historical Reports"
+ },
+ "reportList": {
+ "actionsColumn": "Actions",
+ "allStatuses": "All Statuses",
+ "allTypes": "All Types",
+ "cancel": "Cancel",
+ "caption": "Diagnostic Reports List",
+ "classReportPlaceholder": "Class Report",
+ "confidenceColumn": "Confidence",
+ "confidenceHigh": "High Confidence",
+ "confidenceInsufficient": "Insufficient Data",
+ "confidenceLow": "Low Confidence",
+ "confidenceMedium": "Medium Confidence",
+ "confidenceHighHint": "Sufficient sample data, high reliability",
+ "confidenceMediumHint": "Average sample data, for reference only",
+ "confidenceLowHint": "Insufficient sample data, recommend gathering more data",
+ "confidenceAriaLabel": "Confidence: {level}",
+ "dateColumn": "Generated At",
+ "deleteAction": "Delete",
+ "deleteConfirmation": "Confirm delete this diagnostic report? This action cannot be undone.",
+ "deleteSuccess": "Diagnostic report deleted",
+ "deleteTitle": "Delete Report",
+ "deleting": "Deleting...",
+ "emptyNoReports": "No Diagnostic Reports",
+ "exportAction": "Export",
+ "exportPending": "Export contract not ready (@contract-pending), will be wired via GraphQL later",
+ "filterReportType": "Filter by Type",
+ "filterStatus": "Filter by Status",
+ "generatedByColumn": "Generated By",
+ "gradeReportPlaceholder": "Grade Report",
+ "noReportsDescription": "Diagnostic reports will appear here after students complete homework or exams",
+ "periodColumn": "Period",
+ "publishAction": "Publish",
+ "publishConfirmation": "Confirm publish this diagnostic report? It will be visible to related users after publishing.",
+ "publishing": "Publishing...",
+ "publishSuccess": "Diagnostic report published",
+ "publishTitle": "Publish Report",
+ "reportType": "Report Type",
+ "scoreColumn": "Score",
+ "status": "Status",
+ "statusColumn": "Status",
+ "studentTargetColumn": "Target",
+ "typeColumn": "Type"
+ },
+ "studentDiagnostic": {
+ "noDataDescription": "No diagnostic data for this student. Please complete related homework or exams first",
+ "strengthsDescription": "Knowledge points with mastery >= 80%",
+ "noStrengths": "No strength knowledge points",
+ "strengthsListAriaLabel": "Strength knowledge points list",
+ "weaknessesDescription": "Knowledge points with mastery < 80%, click practice to reinforce",
+ "noWeaknesses": "No weak knowledge points",
+ "weaknessesListAriaLabel": "Weak knowledge points list",
+ "practiceAriaLabel": "Practice knowledge point {name}",
+ "diagnosticReportTitle": "Diagnostic Report",
+ "reportMeta": "Period: {period} · Score: {score}",
+ "recommendationsListAriaLabel": "Recommendations list",
+ "historyDescription": "Historical diagnostic reports list",
+ "untitledPeriod": "Untitled Period",
+ "historyReportMeta": "Generated at {date} · Score: {score}"
}
},
"errorBook": {
"title": "Error Book",
+ "description": "Automatically collect wrong answers from exams and homework, review scientifically",
+ "stats": {
+ "total": "Total Errors",
+ "new": "New",
+ "learning": "Learning",
+ "mastered": "Mastered",
+ "dueReview": "Due Review",
+ "masteredRate": "Mastery Rate",
+ "totalDesc": "Total collected errors",
+ "newDesc": "Not yet reviewed",
+ "learningDesc": "Being reviewed",
+ "masteredDesc": "Mastery rate {rate}%",
+ "dueReviewDesc": "Due today",
+ "totalErrorQuestions": "Total Error Questions",
+ "totalErrorCount": "Total Error Count",
+ "recent7dErrors": "Recent 7d Errors",
+ "knowledgePointCount": "Knowledge Points"
+ },
+ "status": {
+ "new": "New",
+ "learning": "Learning",
+ "mastered": "Mastered",
+ "archived": "Archived"
+ },
+ "source": {
+ "exam": "Exam",
+ "homework": "Homework",
+ "manual": "Manual"
+ },
+ "review": {
+ "again": "Again",
+ "hard": "Hard",
+ "good": "Good",
+ "easy": "Easy",
+ "againDesc": "Don't know, review tomorrow",
+ "hardDesc": "Barely correct, review in 2 days",
+ "goodDesc": "Correct, review in 4 days",
+ "easyDesc": "Easy, review in 7 days"
+ },
+ "actions": {
+ "add": "Add Manually",
+ "viewDetail": "View Details",
+ "saveNote": "Save Note",
+ "archive": "Archive",
+ "delete": "Delete",
+ "collect": "Collect Errors",
+ "cancel": "Cancel",
+ "adding": "Adding..."
+ },
+ "fields": {
+ "question": "Select Question",
+ "note": "Study Note",
+ "errorTags": "Error Reason Tags",
+ "masteryLevel": "Mastery Level",
+ "reviewCount": "Review Count",
+ "nextReview": "Next Review",
+ "createdAt": "Added At",
+ "student": "Student",
+ "className": "Class"
+ },
+ "masteryLevel": {
+ "0": "Not Started",
+ "1": "Beginner",
+ "2": "Familiar",
+ "3": "Proficient",
+ "4": "Skilled",
+ "5": "Mastered"
+ },
+ "questionType": {
+ "single_choice": "Single Choice",
+ "multiple_choice": "Multiple Choice",
+ "judgment": "True/False",
+ "text": "Short Answer",
+ "composite": "Composite"
+ },
+ "errorTags": {
+ "concept": "Concept Gap",
+ "calculation": "Calculation Error",
+ "careless": "Careless",
+ "misread": "Misread Question",
+ "method": "Wrong Method",
+ "memory": "Memory Error",
+ "time": "Out of Time"
+ },
+ "itemCard": {
+ "questionDeleted": "Question deleted",
+ "questionContent": "Question content",
+ "difficulty": "Difficulty {level}",
+ "mastery": "Mastery: {level}",
+ "reviewTimes": "Reviewed {count} times",
+ "needReview": "Needs review",
+ "nextReview": "Next {date}",
+ "addedAt": "Added on {date}",
+ "masteryOutOf": "Mastery: {level}/5"
+ },
+ "detailDialog": {
+ "question": "Question",
+ "myAnswer": "My Answer",
+ "correctAnswer": "Correct Answer",
+ "aiAnalysis": "AI Analysis",
+ "reviewSelf": "Self Review",
+ "studyNote": "Study Note",
+ "notePlaceholder": "Record your reflections, solutions, common mistakes...",
+ "errorTagsLabel": "Error Reason Tags",
+ "reviewHistory": "Review History",
+ "questionDeleted": "Question deleted",
+ "variantPractice": "Variant Practice",
+ "variantPracticeDesc": "Practice with variants from this error"
+ },
+ "filters": {
+ "searchPlaceholder": "Search notes...",
+ "status": "Status",
+ "source": "Source",
+ "review": "Review",
+ "allStatus": "All Status",
+ "allSource": "All Sources",
+ "allErrors": "All Errors",
+ "dueOnly": "Due Only"
+ },
+ "addDialog": {
+ "title": "Add Error",
+ "description": "Select a question from the question bank to add to your error book. Errors are also auto-collected after completing homework/exams.",
+ "selectQuestion": "Select Question",
+ "selectPlaceholder": "Select from bank...",
+ "noteLabel": "Study Note (optional)",
+ "notePlaceholder": "Record error reasons, solutions...",
+ "errorTagsLabel": "Error Reason Tags",
+ "questionPreview": "Question"
+ },
+ "empty": {
+ "title": "Error book is empty",
+ "description": "Wrong answers from exams and homework will be collected here automatically. You can also add manually."
+ },
+ "teacher": {
+ "title": "Error Analysis",
+ "description": "View class error statistics and weak knowledge points to support precision teaching",
+ "descriptionShort": "View class error statistics and weak knowledge points by subject.",
+ "coverage": "Student Coverage",
+ "totalErrors": "Total Errors",
+ "avgMastery": "Avg Mastery Rate",
+ "weakPoints": "Weak Knowledge Points",
+ "subjectDist": "Subject Distribution",
+ "studentDetail": "Student Error Details",
+ "topWrong": "Top Wrong Questions",
+ "noClass": "No classes assigned",
+ "noClassDesc": "You have not been assigned to any class. Unable to view error analysis data.",
+ "noStudent": "No students in class",
+ "noStudentDesc": "There are no students in the class. Unable to view error analysis data.",
+ "noChapterDataTitle": "No Chapter Error Data",
+ "noChapterDataDesc": "Knowledge points have not been linked to chapters. Unable to display chapter-level statistics.",
+ "noKpDataTitle": "No Knowledge Point Data",
+ "noKpDataDesc": "Errors have not been linked to knowledge points. Unable to display weak point statistics.",
+ "noStudentErrorsTitle": "No Student Errors",
+ "noStudentErrorsDesc": "No student error data for the selected range.",
+ "studentsCount": "{total} students total, {withErrors} with errors"
+ },
+ "parent": {
+ "title": "Child Error Book",
+ "description": "View your child's error statistics and learning progress",
+ "noChild": "No children linked",
+ "noChildDesc": "Your account is not linked to any children yet. Please contact the school administrator.",
+ "unknown": "Unknown",
+ "totalErrors": "Total Errors",
+ "dueReview": "Due Review",
+ "newItems": "New",
+ "mastered": "Mastered",
+ "mastery": "{rate}% Mastery",
+ "weakPoints": "Weak Knowledge Points",
+ "errorsAndMastery": "{count} errors · {rate}% mastery"
+ },
+ "admin": {
+ "title": "School-wide Error Analysis",
+ "description": "School-wide error statistics and weak point analysis for teaching decisions",
+ "description2": "View school-wide error statistics and weak knowledge points by subject.",
+ "noPermissionTitle": "Insufficient Permissions",
+ "noPermissionDescription": "You do not have permission to view school-wide error analysis data.",
+ "noStudentsTitle": "No Student Data",
+ "noStudentsDescription": "No student users in the system. Unable to display error analysis.",
+ "topStudents": "Top 50 Students with Most Errors",
+ "studentsWithErrors": "{count} students with errors",
+ "noChapterDataTitle": "No Chapter Error Data",
+ "noChapterDataDescription": "Knowledge points have not been linked to chapters. Unable to display chapter-level statistics.",
+ "noKnowledgePointDataTitle": "No Knowledge Point Data",
+ "noKnowledgePointDataDescription": "Errors have not been linked to knowledge points. Unable to display weak point statistics.",
+ "noStudentErrorsTitle": "No Student Errors",
+ "noStudentErrorsDescription": "No student error data for the selected subject."
+ },
+ "analyticsStats": {
+ "coverage": "Coverage",
+ "coverageSub": "/ {total}",
+ "totalErrors": "Total Errors",
+ "totalErrorsSub": "{avg} per student",
+ "avgMastery": "Avg Mastery",
+ "avgMasteryGood": "Good",
+ "avgMasteryNeedImprove": "Needs improvement",
+ "dueReview": "Due Review",
+ "dueReviewNeedAttention": "Needs attention",
+ "dueReviewNone": "None due",
+ "knowledgePoints": "Knowledge Points",
+ "knowledgePointsWide": "Wide range",
+ "knowledgePointsFocused": "Focused"
+ },
+ "subjectTabs": {
+ "all": "All Subjects",
+ "dueReview": "Due {count}"
+ },
+ "classFilter": {
+ "all": "All Classes",
+ "errorCount": "{count} errors",
+ "dueReview": "{count} due"
+ },
+ "topWrong": {
+ "title": "Top Wrong Questions",
+ "topTitle": "Top 10 Wrong Questions",
+ "emptyTitle": "No frequent wrong questions",
+ "emptyDesc": "After students complete homework or exams, frequency statistics will appear here.",
+ "errorCount": "{count} students wrong",
+ "masteredCount": "{count} mastered",
+ "masteryRate": "Mastery rate {rate}%"
+ },
+ "weaknessChart": {
+ "title": "Weak Knowledge Points Top {count}",
+ "errorCount": "Errors",
+ "chapterLabel": "Chapter: {title}",
+ "masteredLabel": "Mastered",
+ "masteryRateLabel": "Mastery Rate",
+ "unclassified": "Unclassified"
+ },
+ "chapterChart": {
+ "title": "Chapter Error Distribution",
+ "errorCount": "Errors",
+ "masteredLabel": "Mastered",
+ "masteryRateLabel": "Mastery Rate",
+ "knowledgePointCount": "Knowledge Points",
+ "weakKpsLabel": "Weak points:",
+ "knowledgePointBadge": "{count} knowledge points"
+ },
+ "classErrorBar": {
+ "title": "Class Error Comparison",
+ "errorCount": "Total Errors",
+ "studentCount": "Students",
+ "avgPerStudent": "Avg per student",
+ "avgMastery": "Avg Mastery",
+ "dueReview": "Due Review"
+ },
+ "subjectDistChart": {
+ "title": "Subject Error Distribution",
+ "errorCount": "Errors",
+ "masteredLabel": "Mastered",
+ "masteryRateLabel": "Mastery Rate"
+ },
+ "groupedTable": {
+ "unclassified": "Unclassified",
+ "studentCount": "{count}",
+ "studentsWithErrors": "{count} with errors",
+ "totalErrors": "Total Errors",
+ "avgMastery": "Avg Mastery",
+ "student": "Student",
+ "new": "New",
+ "learning": "Learning",
+ "mastered": "Mastered",
+ "dueReview": "Due",
+ "masteryRate": "Mastery Rate",
+ "unknown": "Unknown"
+ },
+ "classOverview": {
+ "coverage": "Coverage",
+ "coverageDesc": "Students with error records",
+ "totalErrors": "Total Errors",
+ "totalErrorsDesc": "Class cumulative errors",
+ "avgMastery": "Avg Mastery",
+ "avgMasteryDesc": "Mastered error ratio",
+ "weakPoints": "Weak Points",
+ "weakPointsDesc": "Needs focus",
+ "weakPointsTitle": "Weak Knowledge Points Top 10",
+ "subjectDist": "Subject Distribution",
+ "noData": "No data",
+ "errorsAndMastery": "{count} errors · {rate}% mastery",
+ "noStudentData": "No student error data",
+ "noStudentDataDesc": "After students complete homework or exams, error data will be summarized here."
+ },
+ "messages": {
+ "added": "Error added",
+ "noteSaved": "Note saved",
+ "reviewRecorded": "Review recorded",
+ "archived": "Error archived",
+ "deleted": "Error deleted",
+ "collected": "Collected {count} errors",
+ "noNewErrors": "No new errors to collect",
+ "addFailed": "Failed to add error",
+ "saveFailed": "Save failed",
+ "deleteFailed": "Delete failed",
+ "archiveFailed": "Archive failed",
+ "collectFailed": "Failed to collect errors",
+ "notFound": "Error not found or access denied",
+ "selectQuestion": "Please select a question",
+ "recordFailed": "Record failed",
+ "addedShort": "Added"
+ },
"list": {
"title": "Error Book",
"description": "View student error records and knowledge point stats",
@@ -1459,12 +3247,6 @@
"colContent": "Content",
"noContent": "No content"
},
- "stats": {
- "totalErrorQuestions": "Total Error Questions",
- "totalErrorCount": "Total Error Count",
- "recent7dErrors": "Recent 7d Errors",
- "knowledgePointCount": "Knowledge Points"
- },
"error": {
"title": "Error Book page error",
"unknown": "An unknown error occurred in the error book module",
@@ -1473,6 +3255,19 @@
},
"practice": {
"title": "Practice",
+ "tabs": {
+ "assignments": "Assignments",
+ "adaptive": "Adaptive Practice"
+ },
+ "adaptive": {
+ "description": "Choose knowledge points and question types to start a targeted adaptive practice. Your practice history is shown below.",
+ "starterSection": "Start Practice",
+ "historySection": "Practice History",
+ "knowledgePointsLoading": "Loading knowledge points...",
+ "knowledgePointsEmpty": "No knowledge points available; cannot start practice",
+ "historyLoading": "Loading practice history...",
+ "historyError": "Failed to load practice history"
+ },
"list": {
"title": "Practice Analysis",
"description": "View and manage all practice assignments",
@@ -1504,6 +3299,92 @@
"title": "Practice page error",
"unknown": "Practice module encountered an unknown error",
"retry": "Retry"
+ },
+ "starter": {
+ "title": "Start Adaptive Practice",
+ "description": "Select knowledge points and question type to begin a targeted practice",
+ "type": "Practice Type",
+ "knowledgePoints": "Knowledge Points",
+ "difficulty": "Difficulty",
+ "anyDifficulty": "Any difficulty",
+ "questionCount": "Question Count",
+ "start": "Start Practice",
+ "creating": "Creating..."
+ },
+ "types": {
+ "error_variant": "Error Variant",
+ "knowledge_point": "Knowledge Point",
+ "weak_chapter": "Weak Chapter",
+ "ai_recommended": "AI Recommended"
+ },
+ "status": {
+ "in_progress": "In Progress",
+ "completed": "Completed",
+ "abandoned": "Abandoned"
+ },
+ "toasts": {
+ "created": "Practice session created",
+ "createFailed": "Failed to create practice session",
+ "selectKnowledgePoint": "Please select at least one knowledge point",
+ "selectWeakKnowledgePoint": "Please select at least one weak knowledge point",
+ "submitted": "Answer submitted",
+ "submitFailed": "Failed to submit answer",
+ "completed": "Practice completed",
+ "completeFailed": "Failed to complete practice",
+ "abandoned": "Practice abandoned",
+ "abandonFailed": "Failed to abandon practice"
+ },
+ "errors": {
+ "SESSION_NOT_FOUND": "Practice session not found",
+ "SESSION_NOT_IN_PROGRESS": "Practice session is not in progress",
+ "ANSWER_NOT_FOUND": "Answer record not found",
+ "QUESTION_NOT_FOUND": "Question not found",
+ "INSUFFICIENT_QUESTIONS": "Insufficient questions available",
+ "INVALID_INPUT": "Invalid input",
+ "UNAUTHORIZED": "You are not authorized to operate this practice session"
+ },
+ "session": {
+ "progress": "Progress",
+ "previous": "Previous",
+ "next": "Next",
+ "abandon": "Abandon",
+ "abandonConfirm": "Confirm abandon?",
+ "abandonDescription": "Submitted answers will be retained, but you cannot continue answering after abandoning.",
+ "cancel": "Cancel",
+ "confirmAbandon": "Confirm Abandon",
+ "complete": "Complete",
+ "empty": "No questions in this practice",
+ "question": "Question",
+ "difficulty": "Difficulty",
+ "variant": "Variant",
+ "skip": "Skip",
+ "retry": "Retry",
+ "submit": "Submit",
+ "submitting": "Submitting...",
+ "submitFailedDescription": "Submission failed. Please retry or check your network connection.",
+ "true": "True",
+ "false": "False",
+ "textPlaceholder": "Enter your answer...",
+ "correct": "Correct",
+ "incorrect": "Incorrect",
+ "pendingReview": "Pending Review",
+ "skipped": "Skipped",
+ "yourAnswer": "Your Answer"
+ },
+ "result": {
+ "title": "Practice Result",
+ "answered": "Answered",
+ "correct": "Correct",
+ "accuracy": "Accuracy",
+ "review": "Question Review"
+ },
+ "history": {
+ "empty": "No practice history"
+ },
+ "reasons": {
+ "student_initiated": "Student Initiated",
+ "teacher_assigned": "Teacher Assigned",
+ "parent_suggested": "Parent Suggested"
}
},
"elective": {
@@ -1528,7 +3409,11 @@
"colUpdatedAt": "Updated",
"colActions": "Actions",
"edit": "Edit",
- "mswNotice": "List query contract pending; ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "List query contract pending; ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "adminList": "elective courses",
+ "empty": "No elective courses",
+ "emptyDescription": "Click the create button to add the first elective course",
+ "emptyStudent": "No courses available for selection"
},
"create": {
"title": "New Elective",
@@ -1572,6 +3457,118 @@
"title": "Elective module error",
"unknown": "An unknown error occurred in the Elective module",
"retry": "Retry"
+ },
+ "errors": {
+ "unexpected": "An unexpected error occurred, please try again later",
+ "notFound": "Course not found"
+ },
+ "actions": {
+ "create": "Create",
+ "edit": "Edit",
+ "delete": "Delete",
+ "openSelection": "Open Selection",
+ "closeSelection": "Close Selection",
+ "runLottery": "Run Lottery",
+ "select": "Select",
+ "drop": "Drop",
+ "cancel": "Cancel"
+ },
+ "fields": {
+ "credit": "Credit",
+ "teacher": "Teacher",
+ "selectionMode": "Selection Mode",
+ "capacity": "Capacity",
+ "classroom": "Classroom",
+ "schedule": "Schedule",
+ "subject": "Subject",
+ "grade": "Grade",
+ "startDate": "Start Date",
+ "endDate": "End Date",
+ "selectionStart": "Selection Start",
+ "selectionEnd": "Selection End",
+ "description": "Description",
+ "enrolled": "Enrolled",
+ "dropReason": "Drop Reason"
+ },
+ "status": {
+ "draft": "Draft",
+ "open": "Open",
+ "closed": "Closed",
+ "cancelled": "Cancelled"
+ },
+ "selectionMode": {
+ "fcfs": "First Come First Served",
+ "lottery": "Lottery"
+ },
+ "selectionStatus": {
+ "selected": "Selected",
+ "enrolled": "Enrolled",
+ "waitlist": "Waitlist",
+ "dropped": "Dropped",
+ "rejected": "Rejected"
+ },
+ "student": {
+ "capacityFull": "Full",
+ "mySelections": "My Selections",
+ "availableCourses": "Available Courses",
+ "selected": "Selected",
+ "selectSuccess": "Course selected successfully",
+ "dropSuccess": "Course dropped successfully",
+ "confirmDrop": "Confirm drop?",
+ "dropReasonPlaceholder": "Enter drop reason (optional)"
+ },
+ "parent": {
+ "noRecordsTitle": "No selection records",
+ "noRecordsDescription": "This student has not selected any courses"
+ },
+ "description": {
+ "detail": "Elective Course Details",
+ "student": "Elective Selection"
+ },
+ "detail": {
+ "back": "Back",
+ "editCourse": "Edit Course",
+ "studentsTitle": "Enrolled Students",
+ "noStudents": "No students",
+ "noStudentsDescription": "No students have selected this course",
+ "studentName": "Student",
+ "priority": "Priority",
+ "selectedAt": "Selected At",
+ "enrolledAt": "Enrolled At"
+ },
+ "export": {
+ "statusHeader": "Status",
+ "selectedAtHeader": "Selected At"
+ },
+ "form": {
+ "createTitle": "Create Elective",
+ "editTitle": "Edit Elective",
+ "nameLabel": "Name",
+ "subjectLabel": "Subject",
+ "selectSubjectPlaceholder": "Select subject",
+ "gradeLabel": "Grade",
+ "selectGradePlaceholder": "Select grade",
+ "teacherLabel": "Teacher",
+ "selectTeacherPlaceholder": "Select teacher",
+ "capacityLabel": "Capacity",
+ "classroomLabel": "Classroom",
+ "scheduleLabel": "Schedule",
+ "schedulePlaceholder": "e.g. Monday periods 3-4",
+ "creditLabel": "Credit",
+ "startDateLabel": "Start Date",
+ "endDateLabel": "End Date",
+ "selectionStartLabel": "Selection Start Time",
+ "selectionEndLabel": "Selection End Time",
+ "dropDeadlineLabel": "Drop Deadline",
+ "dropDeadlineHint": "Students cannot drop after this time; leave empty for no limit",
+ "descriptionLabel": "Description",
+ "descriptionPlaceholder": "Course summary, target audience, etc.",
+ "cancelButton": "Cancel",
+ "createButton": "Create",
+ "saveButton": "Save",
+ "savingButton": "Saving...",
+ "invalidFormState": "Invalid form state",
+ "saveFailed": "Save failed"
}
},
"leave": {
@@ -1634,6 +3631,7 @@
"emptyTitle": "No schedule changes",
"emptyDescription": "Adjust filters or submit your first request",
"emptyAction": "Clear filters",
+ "createAction": "New Request",
"mswNotice": "Schedule changes list query contract pending, ensure NEXT_PUBLIC_MSW=1 is enabled.",
"colSummary": "Summary",
"colClassName": "Class",
@@ -1643,7 +3641,134 @@
"colReason": "Reason",
"colStatus": "Status",
"colApplicant": "Applicant",
- "colCreatedAt": "Created At"
+ "colCreatedAt": "Created At",
+ "colActions": "Actions"
+ },
+ "form": {
+ "title": "Schedule Change Request",
+ "dialogTitle": "Submit Schedule Change Request",
+ "dialogDescription": "Fill in the form to submit a reschedule, cancel, substitute, or merge request",
+ "classLabel": "Class",
+ "classPlaceholder": "Select a class",
+ "typeLabel": "Change Type",
+ "typeReschedule": "Reschedule",
+ "typeCancel": "Cancel",
+ "typeSubstitute": "Substitute",
+ "typeMerge": "Merge",
+ "originalLessonLabel": "Original Lesson",
+ "originalLessonPlaceholder": "Enter original lesson name",
+ "originalTeacherLabel": "Original Teacher",
+ "originalTeacherPlaceholder": "Select original teacher",
+ "substituteTeacherLabel": "Substitute Teacher",
+ "substituteTeacherPlaceholder": "Select substitute teacher",
+ "originalDateLabel": "Original Date",
+ "newDateLabel": "New Date",
+ "newStartLabel": "New Start Time",
+ "newEndLabel": "New End Time",
+ "reasonLabel": "Reason",
+ "reasonPlaceholder": "Explain the reason for this schedule change...",
+ "cancel": "Cancel",
+ "submit": "Submit Request",
+ "submitting": "Submitting...",
+ "createSuccess": "Schedule change request submitted",
+ "errors": {
+ "classRequired": "Please select a class",
+ "originalLessonRequired": "Please enter original lesson",
+ "reasonRequired": "Reason is required",
+ "submitFailed": "Failed to submit request"
+ }
+ },
+ "review": {
+ "approve": "Approve",
+ "reject": "Reject",
+ "approveTitle": "Approve Schedule Change",
+ "rejectTitle": "Reject Schedule Change",
+ "approveDescription": "Are you sure you want to approve this schedule change request?",
+ "rejectDescription": "Please provide a reason for rejecting this request (optional)",
+ "commentLabel": "Review Comment",
+ "commentPlaceholder": "Enter review comment...",
+ "commentRequired": "Comment is required when rejecting",
+ "cancel": "Cancel",
+ "stubSuccess": "Review processed (stub)",
+ "errors": {
+ "submitFailed": "Failed to submit review"
+ }
+ },
+ "grid": {
+ "title": "Schedule Grid",
+ "emptyClasses": "No classes available. Please create a class first.",
+ "selectClass": "Select a class",
+ "periodColumn": "Period",
+ "periodLabel": "Period {n}",
+ "legend": "Legend"
+ },
+ "conflicts": {
+ "title": "Conflict Detection",
+ "classLabel": "Class",
+ "classPlaceholder": "Select a class",
+ "checkButton": "Check Conflicts",
+ "checking": "Checking...",
+ "resultsTitle": "Results",
+ "conflictCount": "{count} conflict(s)",
+ "noConflicts": "No conflicts detected",
+ "typeTeacherOverlap": "Teacher Overlap",
+ "typeClassroomOverlap": "Classroom Overlap",
+ "typeClassOverlap": "Class Overlap",
+ "typeRuleViolation": "Rule Violation",
+ "errors": {
+ "classRequired": "Please select a class",
+ "checkFailed": "Failed to check conflicts"
+ }
+ },
+ "rules": {
+ "title": "Scheduling Rules",
+ "classLabel": "Class",
+ "classPlaceholder": "Select a class",
+ "maxDailyHours": "Max Daily Hours",
+ "maxContinuousHours": "Max Continuous Hours",
+ "morningStart": "Morning Start",
+ "afternoonEnd": "Afternoon End",
+ "lunchBreakStart": "Lunch Break Start",
+ "lunchBreakEnd": "Lunch Break End",
+ "avoidBackToBack": "Avoid back-to-back sessions",
+ "balancedSubjects": "Balance subjects across the week",
+ "cancel": "Cancel",
+ "save": "Save Rules",
+ "saving": "Saving...",
+ "errors": {
+ "classRequired": "Please select a class",
+ "saveFailed": "Failed to save rules"
+ }
+ },
+ "auto": {
+ "title": "Auto Schedule",
+ "classLabel": "Class",
+ "classPlaceholder": "Select a class",
+ "previewButton": "Preview Schedule",
+ "previewing": "Generating...",
+ "applyButton": "Apply to Class",
+ "applying": "Applying...",
+ "previewSummary": "Generated {scheduled} sessions, {conflicts} conflicts",
+ "applySuccess": "Schedule applied successfully",
+ "errors": {
+ "classRequired": "Please select a class",
+ "previewFailed": "Failed to generate schedule",
+ "applyFailed": "Failed to apply schedule"
+ }
+ },
+ "autoResult": {
+ "title": "Generated Schedule",
+ "sessionCount": "{count} sessions",
+ "conflictCount": "{count} conflicts",
+ "noSessions": "No sessions generated.",
+ "colDay": "Day",
+ "colStart": "Start",
+ "colEnd": "End",
+ "colCourse": "Course",
+ "colLocation": "Location",
+ "conflictsTitle": "Conflicts & Warnings",
+ "readyToApply": "No conflicts detected. The schedule is ready to apply.",
+ "unknownDay": "Day {n}"
},
"error": {
"title": "Schedule changes page error",
@@ -1778,7 +3903,64 @@
"labelScoreValue": "{score} pts",
"emptyWeakPoints": "No weak knowledge point data",
"emptyTrends": "No trend data",
- "loadFailed": "Failed to load dashboard data, please try again later."
+ "loadFailed": "Failed to load dashboard data, please try again later.",
+ "statEnrolledClasses": "Enrolled Classes",
+ "descActiveEnrollments": "Active enrollments",
+ "statGraded": "Graded",
+ "descCompletedAssignments": "Completed assignments",
+ "statDueSoon": "Due Soon",
+ "descNext7Days": "Next 7 days",
+ "statOverdue": "Overdue",
+ "descNeedsAttention": "Needs attention",
+ "descOverallPerformance": "Overall performance",
+ "descNoGradesYet": "No grades yet",
+ "descCurrentPosition": "Current position",
+ "descNoRankingYet": "No ranking yet",
+ "sectionUpcomingAssignments": "Upcoming Assignments",
+ "sectionRecentGrades": "Recent Grades",
+ "sectionTodaySchedule": "Today's Schedule",
+ "emptyNoAssignments": "No upcoming assignments",
+ "emptyNoAssignmentsDesc": "No assignments to complete",
+ "emptyNoGradedWork": "No graded work",
+ "emptyNoGradedWorkDesc": "No graded assignments yet",
+ "emptyNoClassesToday": "No classes today",
+ "emptyNoClassesTodayDesc": "No classes scheduled for today",
+ "colTitle": "Title",
+ "colSubject": "Subject",
+ "colStatus": "Status",
+ "colDue": "Due",
+ "colScore": "Score",
+ "colAction": "Action",
+ "colAssignment": "Assignment",
+ "colWhen": "When",
+ "colClass": "Class",
+ "colTime": "Time",
+ "colLocation": "Location",
+ "actionViewAll": "View All",
+ "actionViewSchedule": "View Schedule",
+ "actionStart": "Start",
+ "actionContinue": "Continue",
+ "actionReview": "Review",
+ "badgeInProgress": "In Progress",
+ "badgeUpNext": "Up Next",
+ "badgeLate": "Late",
+ "badgeGraded": "Graded",
+ "badgeSubmitted": "Submitted",
+ "badgeNotStarted": "Not Started",
+ "labelLatest": "Latest",
+ "labelPoints": "Points",
+ "labelNoGrades": "No grades",
+ "greetingMorning": "Good morning",
+ "greetingNoon": "Good noon",
+ "greetingAfternoon": "Good afternoon",
+ "greetingEvening": "Good evening",
+ "greetingNight": "Late night",
+ "greetingWithName": "{greeting}, {name}",
+ "sectionGradeTrend": "Grade Trend",
+ "legendScore": "My Score",
+ "legendClassAvg": "Class Average",
+ "emptyGradeTrend": "No grade trend data",
+ "labelUrgent": "Urgent"
},
"trend": {
"title": "Learning Trend",
@@ -1809,6 +3991,38 @@
"practiceNow": "Practice Now"
}
},
+ "diagnostic": {
+ "title": "Student Diagnostic Report",
+ "description": "View personal learning diagnostics, knowledge mastery distribution, and historical reports",
+ "statStudentName": "Student Name",
+ "statOverallMastery": "Overall Mastery",
+ "statStrengthCount": "Strength Subjects",
+ "statWeaknessCount": "Weakness Subjects",
+ "sectionMasteryRadar": "Knowledge Mastery Distribution",
+ "sectionStrengths": "Strength Knowledge Points",
+ "sectionWeakness": "Weak Knowledge Points",
+ "sectionLatestReport": "Latest Diagnostic Report",
+ "sectionHistoryReports": "Historical Reports",
+ "emptyDiagnostic": "No diagnostic data",
+ "emptyStrengths": "No strength knowledge points",
+ "emptyWeaknessPoints": "No weak knowledge points",
+ "emptyReports": "No historical reports",
+ "colKnowledgePoint": "Knowledge Point",
+ "colMastery": "Mastery",
+ "colActions": "Actions",
+ "practiceNow": "Practice Now",
+ "badgePublished": "Published",
+ "badgeArchived": "Archived",
+ "badgeDraft": "Draft",
+ "badgeGenerated": "Generated",
+ "fieldPeriod": "Period",
+ "fieldScore": "Score",
+ "fieldSummary": "Summary",
+ "fieldRecommendations": "Recommendations",
+ "confidenceLabel": "Confidence",
+ "confidenceTooltip": "Confidence reflects the reliability of the diagnostic conclusion, range 0-1, higher is more reliable",
+ "mswNotice": "Diagnostic data contract is @contract-pending, currently served by MSW. Will switch to real data after backend contract is ready."
+ },
"grades": {
"list": {
"title": "My Grades",
@@ -1818,6 +4032,11 @@
"allSubjects": "All subjects",
"typeFilter": "Filter by type",
"allTypes": "All types",
+ "semesterFilter": "Filter by semester",
+ "allSemesters": "All semesters",
+ "semester1": "Semester 1",
+ "semester2": "Semester 2",
+ "resetFilters": "Reset",
"total": "{count} records",
"colSubject": "Subject",
"colType": "Type",
@@ -1829,7 +4048,59 @@
"colActions": "Actions",
"viewReportCard": "View report card →",
"emptyTitle": "No grade records",
- "mswNotice": "Grades list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "emptyDescription": "Adjust filters or wait for grades to be entered",
+ "mswNotice": "Grades list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "sectionSummary": "Grade Summary",
+ "fieldTotalRecords": "Total records",
+ "fieldAverageScore": "Average rate",
+ "fieldPassRate": "Pass rate",
+ "fieldExcellentRate": "Excellent rate",
+ "sectionTrend": "Grade Trend",
+ "sectionRankingTrend": "Ranking Trend",
+ "sectionDistribution": "Class Distribution",
+ "sectionGrowthArchive": "Growth Archive",
+ "trendEmptyTitle": "No trend data",
+ "trendEmptyDescription": "At least 1 grade record is required to plot the trend",
+ "trendScorePercent": "Score rate (%)",
+ "trendClassAverage": "Class average",
+ "trendRangeAll": "All",
+ "trendRangeDays7": "Last 7 days",
+ "trendRangeDays30": "Last 30 days",
+ "trendRangeDays90": "Last 90 days",
+ "rankingEmptyTitle": "No ranking data",
+ "rankingEmptyDescription": "Ranking data will appear once backend contract is ready",
+ "distributionEmptyTitle": "No distribution data",
+ "distributionEmptyDescription": "Class distribution will appear once backend contract is ready",
+ "distributionYourPosition": "Your position: score {score}, in {bucket} bucket",
+ "growthEmptyTitle": "No growth archive",
+ "growthEmptyDescription": "Cross-semester growth archive will appear once backend contract is ready",
+ "rankingLegend": "Class rank (lower is better)",
+ "rankingTotalStudents": "{total} students in class",
+ "rankingCurrentRank": "Currently rank {rank}",
+ "distributionXAxis": "Score range",
+ "distributionYAxis": "Count",
+ "distributionBucketCount": "{count} students",
+ "distributionStudentPosition": "You're in {bucket} bucket, rank {rank} in class",
+ "growthArchiveXAxis": "Semester",
+ "growthArchiveYAxis": "Score",
+ "growthArchiveSubjectLabel": "Subject",
+ "subjects": {
+ "chinese": "Chinese",
+ "math": "Math",
+ "english": "English",
+ "physics": "Physics",
+ "chemistry": "Chemistry",
+ "biology": "Biology",
+ "history": "History",
+ "geography": "Geography",
+ "politics": "Politics"
+ },
+ "types": {
+ "exam": "Exam",
+ "homework": "Homework",
+ "quiz": "Quiz",
+ "comprehensive": "Comprehensive"
+ }
},
"reportCard": {
"title": "Report Card",
@@ -1845,20 +4116,65 @@
"colScoreRate": "Rate",
"colLevel": "Level",
"colTeacherComment": "Teacher comment",
- "emptyTitle": "No report card data"
+ "emptyTitle": "No report card data",
+ "emptyDescription": "Switch academic year/semester or wait for grades",
+ "schoolName": "School",
+ "reportCardTitle": "Semester Report Card",
+ "periodLabel": "Year {year} · {semester}",
+ "allSemesters": "All semesters",
+ "studentNameLabel": "Student name",
+ "classNameLabel": "Class",
+ "generatedAtLabel": "Generated at",
+ "gradesSectionTitle": "Subject Grades",
+ "summarySectionTitle": "Summary",
+ "overallAverage": "Overall average",
+ "overallRank": "Overall rank",
+ "passRate": "Pass rate",
+ "excellentRate": "Excellent rate",
+ "commentsTitle": "Teacher comments",
+ "commentsPlaceholder": "(Teacher comments pending)",
+ "signatureClassTeacher": "Class teacher signature",
+ "signatureParent": "Parent signature",
+ "signaturePrincipal": "Principal signature",
+ "footerNote": "This report card was generated on {date}",
+ "academicYearsCount": "{count} academic years available",
+ "periodSelectorTitle": "Academic Year / Semester",
+ "allAcademicYears": "All academic years",
+ "semester1": "Semester 1",
+ "semester2": "Semester 2",
+ "resetFilters": "Reset",
+ "subjectCount": "Subjects",
+ "signaturesTitle": "Signatures",
+ "summaryRowLabel": "Total",
+ "classTeacherLabel": "Class teacher",
+ "studentInfoTitle": "Student information",
+ "colAssessment": "Assessment",
+ "colType": "Type",
+ "colRank": "Rank",
+ "colRemark": "Remark",
+ "typeExam": "Exam",
+ "typeHomework": "Homework",
+ "typeQuiz": "Quiz",
+ "subjectAvgLabel": "Subject avg",
+ "noRecords": "No records",
+ "preparing": "Preparing...",
+ "errorPrint": "Print failed, please retry",
+ "commentsAriaLabel": "Teacher comments area",
+ "rankFormat": "{rank} / {total}",
+ "classTotalStudentsLabel": "Class size"
}
},
"exams": {
"list": {
"title": "My Exams",
- "description": "View upcoming and ended exams",
+ "description": "View all exams by status. Exams in progress can be entered directly.",
"statusFilter": "Filter by status",
"allStatus": "All status",
"statusUpcoming": "Upcoming",
"statusInProgress": "In progress",
"statusEnded": "Ended",
"statusScored": "Scored",
- "total": "{count} records",
+ "total": "{count} exam(s)",
"colTitle": "Exam title",
"colSubject": "Subject",
"colExamDate": "Exam date",
@@ -1871,7 +4187,28 @@
"takeExam": "Take exam →",
"viewResult": "View result →",
"emptyTitle": "No exams",
- "mswNotice": "Student exams list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Student exams list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "statusNotStarted": "Not started",
+ "statusExpired": "Ended",
+ "subjectFilter": "Filter by subject",
+ "allSubjects": "All subjects",
+ "groupInProgress": "In progress",
+ "groupInProgressDesc": "Click to start",
+ "groupUpcoming": "Upcoming",
+ "groupUpcomingDesc": "Waiting to start",
+ "groupSubmitted": "Submitted",
+ "groupSubmittedDesc": "Awaiting grading",
+ "groupScored": "Scored",
+ "groupScoredDesc": "View results",
+ "groupEnded": "Ended",
+ "groupEndedDesc": "Exam ended",
+ "countdownStartsIn": "Starts in {n} day(s)",
+ "countdownHoursLeft": "Starts in {n} hour(s)",
+ "countdownStarted": "Already started",
+ "countdownUrgent": "Starting soon",
+ "colQuestionCount": "Questions",
+ "colTotalScore": "Total score",
+ "emptyDesc": "When teachers publish exams, the schedule and entry will appear here."
},
"result": {
"title": "Exam Result",
@@ -1883,7 +4220,7 @@
"fieldTotalScore": "Total",
"fieldRank": "Class rank",
"fieldClassAvg": "Class avg",
- "fieldDuration": "Duration",
+ "fieldDuration": "Duration (min)",
"colQuestionNo": "No",
"colQuestion": "Question",
"colYourAnswer": "Your answer",
@@ -1894,7 +4231,15 @@
"colMastery": "Mastery",
"addToErrorBook": "Add to error book",
"emptyQuestions": "No question data",
- "notFound": "Exam result not found"
+ "notFound": "Exam result not found",
+ "sectionStats": "Answer stats",
+ "sectionWrongQuestions": "Wrong questions",
+ "statsTotal": "Total",
+ "statsCorrect": "Correct",
+ "statsWrong": "Wrong",
+ "statsUnanswered": "Unanswered",
+ "noWrongQuestions": "No wrong questions in this exam",
+ "colQuestionType": "Type"
},
"take": {
"title": "Take Exam",
@@ -1908,19 +4253,37 @@
"unanswered": "Unanswered",
"marked": "Marked",
"notFound": "Exam not found or ended",
- "submitError": "Submission failed, please retry"
+ "submitError": "Submission failed, please retry",
+ "totalScore": "Total score",
+ "back": "Back to list",
+ "submitting": "Submitting…",
+ "confirmSubmit": "Confirm submit",
+ "confirmSubmitDescription": "You cannot change answers after submission. Please verify all answers.",
+ "unansweredWarning": "{count} question(s) unanswered. Are you sure to submit?",
+ "confirmSubmitAction": "Confirm submission",
+ "autoSaveSaving": "Saving…",
+ "autoSaveIdle": "Not saved",
+ "timeUpAutoSubmit": "Time is up. Auto-submitted.",
+ "questionUnit": "Q",
+ "makeSureAnswered": "Please verify all answers before submitting",
+ "questionType": "Type",
+ "score": "Score",
+ "prevQuestion": "Previous",
+ "nextQuestion": "Next",
+ "jumpToQuestion": "Jump to question {no}"
}
},
"homework": {
"list": {
"title": "My Homework",
- "description": "View homework list grouped by subject",
+ "description": "View homework grouped by subject. Pending homework links to the submit page.",
"statusFilter": "Filter by status",
"allStatus": "All status",
"statusPending": "Pending",
"statusSubmitted": "Submitted",
"statusGraded": "Graded",
- "total": "{count} records",
+ "statusOverdue": "Overdue",
+ "total": "{count} assignment(s)",
"colTitle": "Homework title",
"colSubject": "Subject",
"colDueDate": "Due date",
@@ -1930,7 +4293,38 @@
"submitHomework": "Submit →",
"viewAnalysis": "View analysis →",
"emptyTitle": "No homework",
- "mswNotice": "Student homework list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Student homework list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "subjectFilter": "Filter by subject",
+ "allSubjects": "All subjects",
+ "searchPlaceholder": "Search homework title…",
+ "groupPending": "Pending",
+ "groupPendingDesc": "Click to submit",
+ "groupOverdue": "Overdue",
+ "groupOverdueDesc": "Please submit as soon as possible",
+ "groupSubmitted": "Submitted",
+ "groupSubmittedDesc": "Awaiting grading",
+ "groupGraded": "Graded",
+ "groupGradedDesc": "View score",
+ "bucketActive": "To do",
+ "bucketDone": "Done",
+ "urgent24h": "Due within 24h",
+ "overdueTag": "Overdue",
+ "emptyDesc": "When teachers assign homework, the list and entry will appear here.",
+ "viewCard": "Card view",
+ "viewTable": "Table view",
+ "attempts": "Attempts",
+ "attemptsValue": "{used} / {max}",
+ "latestScore": "Latest score",
+ "actionStart": "Start",
+ "actionContinue": "Continue",
+ "actionView": "View",
+ "actionReview": "Review",
+ "overdueBadge": "Overdue",
+ "groupUnanswered": "Unanswered",
+ "groupAnswered": "Answered",
+ "noResults": "No matching homework found",
+ "statusInProgress": "In progress",
+ "statusNotStarted": "Not started"
},
"submit": {
"title": "Submit Homework",
@@ -1939,7 +4333,39 @@
"submitSuccess": "Homework submitted",
"autoSaveTip": "Answers auto-saved",
"notFound": "Homework not found or closed",
- "submitError": "Submission failed, please retry"
+ "submitError": "Submission failed, please retry",
+ "questionNav": "Question Nav",
+ "answerPlaceholder": "Enter your answer...",
+ "submitPanelTitle": "Submit",
+ "answerProgress": "Answer Progress",
+ "storageRestoreFailed": "Local storage corrupted, fell back to backend data",
+ "storageSaveFailed": "Local storage write failed, does not affect answering",
+ "reviewTitle": "Homework review",
+ "takeMode": "Take mode",
+ "reviewMode": "Read-only review",
+ "description": "Description",
+ "attemptsUsed": "Attempts",
+ "attemptsValue": "{used} / {max}",
+ "back": "Back to list",
+ "submitting": "Submitting…",
+ "confirmSubmit": "Confirm submit",
+ "confirmSubmitDescription": "You cannot change answers after submission. Please verify all answers.",
+ "unansweredWarning": "{count} question(s) unanswered. Are you sure to submit?",
+ "confirmSubmitAction": "Confirm submission",
+ "autoSaveSaving": "Saving…",
+ "autoSaveIdle": "Not saved",
+ "questionType": "Type",
+ "fieldTitle": "Title",
+ "fieldDueDate": "Due date",
+ "fieldSubmittedAt": "Submitted at",
+ "fieldStatus": "Status",
+ "fieldScore": "Score",
+ "fieldTotalScore": "Total score",
+ "viewAnalysis": "View analysis",
+ "teacherComment": "Teacher comment",
+ "noComment": "No comment",
+ "questionStem": "Question",
+ "maxAttempts": "Max attempts"
},
"analysis": {
"title": "Homework Analysis",
@@ -1954,7 +4380,13 @@
"colCorrectAnswer": "Correct answer",
"colScore": "Score",
"colIsCorrect": "Result",
- "notFound": "Homework analysis not found"
+ "notFound": "Homework analysis not found",
+ "sectionStats": "Answer stats",
+ "sectionWrongQuestions": "Wrong questions",
+ "statsTotal": "Total",
+ "statsCorrect": "Correct",
+ "statsWrong": "Wrong",
+ "noWrongQuestions": "No wrong questions in this homework"
}
},
"schedule": {
@@ -1968,23 +4400,51 @@
"colClassroom": "Classroom",
"colTime": "Time",
"emptyTitle": "No schedule data",
- "mswNotice": "Student schedule contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Student schedule contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "weekdays": {
+ "monday": "Mon",
+ "tuesday": "Tue",
+ "wednesday": "Wed",
+ "thursday": "Thu",
+ "friday": "Fri",
+ "saturday": "Sat",
+ "sunday": "Sun"
+ },
+ "emptyDescription": "No schedule data available, please try again later or contact admin",
+ "today": "Today",
+ "noClasses": "No classes today",
+ "noStudentTitle": "No student identity",
+ "noStudentDesc": "Please sign in with a student account or contact admin to set up student identity"
},
"attendance": {
"title": "Attendance",
"description": "View personal attendance summary and details",
"sectionSummary": "Summary",
+ "fieldStudentName": "Student name",
+ "fieldTotalRecords": "Total records",
+ "fieldPresentCount": "Present count",
"fieldAttendanceRate": "Attendance rate",
+ "fieldLateRate": "Late rate",
"fieldLateCount": "Late count",
"fieldEarlyLeaveCount": "Early leave count",
"fieldLeaveCount": "Leave count",
"fieldAbsentCount": "Absent count",
+ "fieldSchoolActivityCount": "School activity count",
"sectionRecords": "Records",
"colDate": "Date",
+ "colClass": "Class",
"colStatus": "Status",
"colRemark": "Remark",
"emptyTitle": "No attendance records",
- "mswNotice": "Student attendance contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Student attendance contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "status": {
+ "present": "Present",
+ "late": "Late",
+ "early_leave": "Early leave",
+ "leave": "Leave",
+ "absent": "Absent",
+ "school_activity": "School activity"
+ }
},
"classes": {
"title": "My Classes",
@@ -2016,7 +4476,32 @@
"colActions": "Actions",
"viewDetail": "View detail →",
"emptyTitle": "No courses",
- "mswNotice": "Student courses list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Student courses list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "viewModeLabel": "View mode",
+ "viewCard": "Card view",
+ "viewTable": "Table view",
+ "joinClass": "Join class",
+ "joinClassTitle": "Join class by invitation code",
+ "joinClassDescription": "Enter the 6-digit invitation code provided by your teacher",
+ "joinClassCodeLabel": "Invitation code",
+ "joinClassCodePlaceholder": "Enter 6-digit invitation code",
+ "joinClassCodeHint": "The invitation code is 6 digits. Ask your teacher if you don't have one.",
+ "joinClassSubmit": "Join",
+ "joinClassSubmitting": "Joining...",
+ "joinClassCancel": "Cancel",
+ "joinClassSuccess": "Joined class successfully",
+ "joinClassFailed": "Failed to join: {message}",
+ "joinClassCodeRequired": "Please enter the invitation code",
+ "joinClassCodeInvalid": "Invitation code must be 6 digits",
+ "statusActive": "Active",
+ "statusInactive": "Concluded",
+ "cardTeacher": "Teacher",
+ "cardSchool": "School",
+ "cardHeadTeacher": "Head teacher",
+ "cardGrade": "Grade",
+ "sendEmail": "Send email",
+ "colRoom": "Room",
+ "colGrade": "Grade"
},
"detail": {
"title": "Course Detail",
@@ -2033,6 +4518,18 @@
"sectionSchedule": "Class Schedule",
"viewFullSchedule": "View full schedule",
"viewHomework": "View homework",
+ "colWeekday": "Weekday",
+ "colPeriod": "Period",
+ "colSubject": "Subject",
+ "colTime": "Time",
+ "colCourse": "Course",
+ "weekdayMon": "Mon",
+ "weekdayTue": "Tue",
+ "weekdayWed": "Wed",
+ "weekdayThu": "Thu",
+ "weekdayFri": "Fri",
+ "weekdaySat": "Sat",
+ "weekdaySun": "Sun",
"notFound": "Course not found"
}
},
@@ -2041,11 +4538,24 @@
"title": "Course Plans",
"description": "View course plans",
"total": "{count} records",
+ "searchPlaceholder": "Search plan/subject/class/teacher...",
+ "filterByStatus": "Filter by status",
+ "statusAll": "All statuses",
+ "statusActive": "In progress",
+ "statusCompleted": "Completed",
+ "statusPlanning": "Planning",
+ "statusPaused": "Paused",
"colTitle": "Plan name",
"colSubject": "Subject",
"colGrade": "Grade",
"colStatus": "Status",
"colActions": "Actions",
+ "colClass": "Class",
+ "colTeacher": "Teacher",
+ "colSemester": "Semester",
+ "colCreated": "Created at",
+ "fieldProgress": "Progress",
+ "progressHours": "{completed} / {total} hours ({percent}%)",
"viewDetail": "View detail →",
"emptyTitle": "No course plans",
"mswNotice": "Student course plans list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
@@ -2053,13 +4563,49 @@
"detail": {
"title": "Course Plan Detail",
"backToList": "Back to list",
+ "notFound": "Course plan not found",
+ "mswNotice": "Detail query contract pending (@contract-pending).",
+ "headerTitle": "Course Plan Detail",
+ "badgeClass": "Class",
+ "badgeSubject": "Subject",
+ "badgeStatus": "Status",
+ "badgeSemester": "Semester",
+ "badgeNoClass": "No class",
+ "badgeUnknownSubject": "Unknown subject",
+ "fieldTeacher": "Teacher",
+ "fieldUnassigned": "Unassigned",
+ "fieldCreatedAt": "Created on {date}",
+ "fieldStartDate": "Starts on {date}",
+ "fieldEndDate": "Ends on {date}",
+ "fieldSemester": "Semester {semester}",
"sectionBasic": "Basic Info",
"fieldTitle": "Title",
"fieldSubject": "Subject",
"fieldGrade": "Grade",
"fieldDescription": "Description",
"sectionTextbooks": "Related Textbooks",
- "notFound": "Course plan not found"
+ "sectionProgress": "Progress",
+ "progressLabel": "Completion progress",
+ "progressHours": "{completed} / {total} hours ({percent}%)",
+ "progressItems": "{completed} / {total} week plans completed",
+ "sectionSyllabus": "Syllabus",
+ "sectionObjectives": "Objectives",
+ "emptyText": "None",
+ "sectionWeekPlans": "Weekly Plans",
+ "emptyWeekPlans": "No weekly plans",
+ "colWeek": "Week",
+ "colTopic": "Topic",
+ "colHours": "Hours",
+ "colChapter": "Chapter",
+ "colStatus": "Status",
+ "statusPlanned": "Planned",
+ "statusInProgress": "In progress",
+ "statusCompleted": "Completed",
+ "statusSkipped": "Skipped",
+ "statusUnknown": "Unknown",
+ "exportCsv": "Export CSV",
+ "toastExported": "Exported",
+ "toastExportFailed": "Export failed"
}
},
"lessonPlans": {
@@ -2073,15 +4619,23 @@
"colUpdatedAt": "Updated at",
"colActions": "Actions",
"viewDetail": "View lesson plan →",
+ "subjectFilter": "Filter by subject",
+ "allSubjects": "All subjects",
"emptyTitle": "No lesson plans",
"mswNotice": "Student lesson plans list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
},
"view": {
"title": "Lesson Plan View",
"backToList": "Back to list",
+ "sectionBasic": "Basic Information",
"sectionContent": "Lesson Content",
+ "fieldSubject": "Subject",
+ "fieldGrade": "Grade",
+ "fieldTextbook": "Textbook",
+ "fieldChapter": "Chapter",
"notFound": "Lesson plan not found or not published",
- "outOfScope": "Lesson plan is out of your grade scope"
+ "outOfScope": "Lesson plan is out of your grade scope",
+ "notPublished": "This lesson plan has not been published and cannot be viewed"
}
},
"textbooks": {
@@ -2100,14 +4654,43 @@
"colActions": "Actions",
"viewChapters": "View chapters →",
"emptyTitle": "No textbooks",
- "mswNotice": "Student textbooks list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "emptyFilteredTitle": "No matching textbooks",
+ "emptyFilteredDescription": "Try adjusting filters or clear them to retry.",
+ "clearFilters": "Clear filters",
+ "gradeNotSetTitle": "Student grade not set",
+ "gradeNotSetDescription": "No active class is linked, so textbooks cannot be filtered by grade. Please contact your teacher or administrator to complete class information.",
+ "mswNotice": "Student textbooks list contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "subjects": {
+ "chinese": "Chinese",
+ "math": "Math",
+ "english": "English",
+ "physics": "Physics",
+ "chemistry": "Chemistry",
+ "biology": "Biology"
+ },
+ "grades": {
+ "grade1": "Grade 1",
+ "grade2": "Grade 2",
+ "grade3": "Grade 3",
+ "grade7": "Grade 7",
+ "grade8": "Grade 8",
+ "grade9": "Grade 9"
+ }
},
"chapters": {
"title": "Textbook Reader",
"backToList": "Back to textbooks",
"sectionChapters": "Chapter List",
"sectionContent": "Reading Area",
- "notFound": "Textbook not found or grade mismatch"
+ "notFound": "Textbook not found or grade mismatch",
+ "mswNotice": "Chapter content contract pending. Currently served via MSW.",
+ "subjectBadge": "Subject",
+ "gradeBadge": "Grade",
+ "gradeMismatchWarning": "Textbook grade ({grade}) does not match your grade. For reference reading only.",
+ "prevChapter": "Previous",
+ "nextChapter": "Next",
+ "emptyChapters": "No chapter content available",
+ "chapterLabel": "Chapter {n}"
}
},
"errorBook": {
@@ -2130,6 +4713,7 @@
"sourceFilter": "Filter by source",
"allSources": "All sources",
"dueOnlyFilter": "Show only to-review",
+ "resetFilters": "Reset",
"total": "{count} records",
"colQuestion": "Question",
"colSubject": "Subject",
@@ -2140,8 +4724,18 @@
"colActions": "Actions",
"markMastered": "Mark as mastered",
"emptyTitle": "No errors",
+ "emptyDescription": "Adjust filters or add your first error",
"markSuccess": "Marked as mastered",
- "markError": "Mark failed"
+ "markError": "Mark failed",
+ "fieldTotalDesc": "All error records",
+ "fieldNewDesc": "Not yet reviewed",
+ "fieldLearningDesc": "Reviewing, not yet mastered",
+ "fieldMasteredDesc": "Mastery rate {rate}%",
+ "fieldToReviewDesc": "Due for review",
+ "sourceExam": "Exam",
+ "sourceHomework": "Homework",
+ "sourcePractice": "Practice",
+ "sourceManual": "Manual"
},
"learning": {
"title": "Learning Center",
@@ -2157,7 +4751,12 @@
"cardErrorBook": "Error Book",
"cardErrorBookDesc": "Errors to review",
"enter": "Enter →",
- "emptyTitle": "No learning resources"
+ "emptyTitle": "No learning resources",
+ "statCourses": "Enrolled in {count} classes",
+ "statHomework": "Pending {pending} · Due soon {dueSoon}",
+ "statTextbooks": "{count} textbooks available",
+ "noStudentTitle": "No student identity found",
+ "noStudentDesc": "Please confirm you have joined a class or contact admin"
},
"learningPath": {
"title": "Learning Path",
@@ -2192,7 +4791,28 @@
"colActions": "Actions",
"viewDetail": "View detail →",
"emptyTitle": "No practice records",
- "mswNotice": "Student practice contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "starterDescription": "Select knowledge points and question count to start",
+ "starterQuestionCount": "Question count",
+ "starterCreating": "Creating...",
+ "starterSelectKnowledgePoint": "Please select at least one knowledge point",
+ "starterCreated": "Practice session created",
+ "starterCreateFailed": "Create failed: {message}",
+ "mswNotice": "Student practice contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "knowledgePointLoading": "Loading knowledge points...",
+ "knowledgePointLoadFailed": "Failed to load knowledge points: {message}",
+ "knowledgePointEmpty": "No knowledge points available",
+ "subjectFilterAll": "All subjects",
+ "subjectFilterLabel": "Subject filter",
+ "selectedCount": "{count} knowledge point(s) selected",
+ "difficultyEasy": "Easy",
+ "difficultyMedium": "Medium",
+ "difficultyHard": "Hard",
+ "subjectMath": "Math",
+ "subjectChinese": "Chinese",
+ "subjectEnglish": "English",
+ "subjectPhysics": "Physics",
+ "subjectChemistry": "Chemistry",
+ "knowledgePointDescription": "Description"
},
"session": {
"title": "Practice Session",
@@ -2225,6 +4845,37 @@
"enroll": "Enroll",
"drop": "Drop",
"viewDetail": "View detail →",
+ "filterByStatus": "Filter by status",
+ "filterBySelectionMode": "Filter by selection mode",
+ "statusAll": "All statuses",
+ "statusOpen": "Open",
+ "statusClosed": "Closed",
+ "statusInProgress": "In progress",
+ "statusCompleted": "Completed",
+ "modeAll": "All modes",
+ "modeFcfs": "First come first served",
+ "modeLottery": "Lottery",
+ "badgeStatusOpen": "Open",
+ "badgeStatusClosed": "Closed",
+ "badgeStatusInProgress": "In progress",
+ "badgeStatusCompleted": "Completed",
+ "badgeModeFcfs": "FCFS",
+ "badgeModeLottery": "Lottery",
+ "fieldSubject": "Subject",
+ "fieldSchedule": "Schedule",
+ "fieldCredits": "Credits",
+ "fieldCapacity": "Capacity",
+ "fieldEnrolledCount": "Enrolled",
+ "fieldCategory": "Category",
+ "capacityFull": "Full",
+ "enrolledCount": "{enrolled}/{capacity}",
+ "dropDialogTitle": "Confirm drop",
+ "dropDialogDescription": "Are you sure you want to drop {courseName}? This cannot be undone.",
+ "dropDialogReasonLabel": "Reason (optional)",
+ "dropDialogReasonPlaceholder": "Please enter the reason...",
+ "dropDialogCancel": "Cancel",
+ "dropDialogConfirm": "Confirm drop",
+ "dropDialogLoading": "Processing...",
"emptyMySelections": "No selected courses",
"emptyAvailable": "No available courses",
"enrollSuccess": "Enrolled successfully",
@@ -2237,14 +4888,30 @@
"title": "Elective Detail",
"backToList": "Back to elective list",
"sectionBasic": "Course Info",
+ "sectionDescription": "Description",
+ "sectionSchedule": "Schedule",
"fieldName": "Name",
"fieldTeacher": "Teacher",
"fieldCapacity": "Capacity",
"fieldEnrolled": "Enrolled",
- "fieldSchedule": "Schedule",
"fieldCredits": "Credits",
"fieldCategory": "Category",
- "notFound": "Course not found"
+ "fieldSubject": "Subject",
+ "fieldGrade": "Grade",
+ "fieldClassroom": "Classroom",
+ "fieldSelectionMode": "Selection Mode",
+ "fieldStartDate": "Start Date",
+ "fieldEndDate": "End Date",
+ "fieldSelectionStartAt": "Selection Start",
+ "fieldSelectionEndAt": "Selection End",
+ "statusDraft": "Draft",
+ "statusOpen": "Open",
+ "statusClosed": "Closed",
+ "statusCancelled": "Cancelled",
+ "selectionModeFcfs": "First come first served",
+ "selectionModeLottery": "Lottery",
+ "notFound": "Course not found",
+ "mswNotice": "Elective detail contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
}
},
"leave": {
@@ -2274,7 +4941,20 @@
"statusRejected": "Rejected",
"emptyTitle": "No leave records",
"noActiveClass": "No active class, cannot submit leave request",
- "mswNotice": "Student leave contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "backToDashboard": "Back to student home",
+ "mswNotice": "Student leave contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "validation": {
+ "selectClass": "Please select a class",
+ "selectStartDate": "Please select a start date",
+ "selectEndDate": "Please select an end date",
+ "selectType": "Please select a leave type",
+ "fillReason": "Please fill in the leave reason",
+ "endDateBeforeStart": "End date cannot be earlier than start date"
+ },
+ "paginationRange": "Showing {start}-{end} of {total}",
+ "paginationPage": "Page {page}/{totalPages}",
+ "paginationPrev": "Previous",
+ "paginationNext": "Next"
},
"aiTutor": {
"title": "AI Tutor",
@@ -2334,6 +5014,110 @@
"retry": "Retry"
}
},
+ "shared": {
+ "profile": {
+ "title": "Profile",
+ "description": "View your profile and role-specific overview",
+ "editProfile": "Edit Profile",
+ "sectionPersonal": "Personal Information",
+ "sectionAccount": "Account Information",
+ "sectionRoleOverview": "Role Overview",
+ "fieldName": "Name",
+ "fieldGender": "Gender",
+ "fieldAge": "Age",
+ "fieldPhone": "Phone",
+ "fieldAddress": "Address",
+ "fieldEmail": "Email",
+ "fieldRole": "Role",
+ "fieldCreatedAt": "Registered At",
+ "fieldOnboardedAt": "Onboarded At",
+ "fieldClassName": "Class",
+ "fieldGrade": "Grade",
+ "fieldHeadTeacher": "Head Teacher",
+ "fieldAvgScore": "Average Score",
+ "fieldClassRank": "Class Rank",
+ "fieldClassCount": "Class Count",
+ "fieldStudentCount": "Student Count",
+ "fieldCourses": "Courses",
+ "rankValue": "No. {rank} / {total}",
+ "genderMale": "Male",
+ "genderFemale": "Female",
+ "genderOther": "Other",
+ "roleAdmin": "Admin",
+ "roleTeacher": "Teacher",
+ "roleStudent": "Student",
+ "roleParent": "Parent",
+ "noRoleOverview": "No role-specific overview available for current role",
+ "mswNotice": "User profile contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ },
+ "messages": {
+ "detail": {
+ "title": "Message Detail",
+ "backToList": "Back to messages",
+ "reply": "Reply",
+ "delete": "Delete",
+ "deleteConfirm": "Delete this message?",
+ "deleteSuccess": "Deleted",
+ "deleteError": "Delete failed",
+ "cancel": "Cancel",
+ "fieldFrom": "From",
+ "fieldTo": "To",
+ "fieldSubject": "Subject",
+ "fieldDate": "Date",
+ "fieldStatus": "Status",
+ "fieldBody": "Body",
+ "fieldStarred": "Starred",
+ "statusRead": "Read",
+ "statusUnread": "Unread",
+ "starredYes": "Yes",
+ "starredNo": "No",
+ "notFound": "Message not found",
+ "mswNotice": "Message detail contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ },
+ "compose": {
+ "title": "Compose Message",
+ "description": "Fill in recipient, subject and body",
+ "backToList": "Back to messages",
+ "fieldTo": "To",
+ "fieldSubject": "Subject",
+ "fieldBody": "Body",
+ "toPlaceholder": "Select recipient",
+ "subjectPlaceholder": "Enter subject",
+ "bodyPlaceholder": "Enter body",
+ "send": "Send",
+ "sending": "Sending...",
+ "cancel": "Cancel",
+ "selectRecipient": "Please select a recipient",
+ "subjectRequired": "Subject is required",
+ "bodyRequired": "Body is required",
+ "sendSuccess": "Sent successfully",
+ "sendError": "Send failed, please retry",
+ "noRecipients": "No recipients available",
+ "mswNotice": "Recipients and send-message contracts pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ },
+ "groupCompose": {
+ "title": "Group Message",
+ "description": "Send one message to multiple recipients",
+ "backToList": "Back to messages",
+ "fieldTo": "Recipients (multiple)",
+ "fieldSubject": "Subject",
+ "fieldBody": "Body",
+ "subjectPlaceholder": "Enter subject",
+ "bodyPlaceholder": "Enter body",
+ "send": "Send",
+ "sending": "Sending...",
+ "cancel": "Cancel",
+ "selectAtLeastOne": "Select at least one recipient",
+ "subjectRequired": "Subject is required",
+ "bodyRequired": "Body is required",
+ "sendSuccess": "Sent to {count} recipients",
+ "sendError": "Group send failed, please retry",
+ "noRecipients": "No recipients available",
+ "selectedCount": "{count} selected",
+ "mswNotice": "Recipients and send-message contracts pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ }
+ }
+ },
"admin": {
"users": {
"list": {
@@ -2346,10 +5130,16 @@
"colName": "Name",
"colEmail": "Email",
"colRole": "Role",
+ "colPhone": "Phone",
"colStatus": "Status",
"colCreatedAt": "Created at",
+ "colUpdatedAt": "Updated at",
+ "colUserType": "User type",
"colActions": "Actions",
"editRole": "Edit role",
+ "editUser": "Edit user",
+ "userTypeInternal": "Internal",
+ "userTypeExternal": "External",
"activate": "Activate",
"deactivate": "Deactivate",
"emptyTitle": "No users",
@@ -2383,7 +5173,7 @@
"required": "Required",
"optional": "Optional",
"uploadButton": "Select file to upload",
- "uploadHint": "Supports .csv files, up to 500 records per upload",
+ "uploadHint": "Supports .csv / .xlsx / .xls files, up to 500 records per upload",
"noticeTitle": "Notes",
"notice1": "Email format must be valid",
"notice2": "Role can only be teacher / student / parent / admin",
@@ -2395,7 +5185,26 @@
"resultTotal": "Total rows",
"resultSuccess": "Success",
"resultFailed": "Failed",
- "resultErrors": "Error details"
+ "resultErrors": "Error details",
+ "filesPreview": "File preview (first {count} rows)",
+ "importStatus": "Importing...",
+ "reselect": "Reselect",
+ "confirmImport": "Confirm import"
+ },
+ "assignDialog": {
+ "title": "Assign roles",
+ "description": "Assign roles to {name}. Changes take effect immediately after saving.",
+ "listAriaLabel": "Assignable roles for {name}",
+ "noEnabledRoles": "No assignable roles available",
+ "system": "System",
+ "locked": "Locked",
+ "disabledAssignedLabel": "The following roles are disabled but still assigned to this user:",
+ "disabledLabel": "Disabled",
+ "save": "Save",
+ "saving": "Saving...",
+ "success": "Roles assigned",
+ "adminWarningTitle": "Administrator role",
+ "adminWarning": "Administrators have the highest system privileges. Assigning this role grants the user full management capabilities. Proceed with caution."
},
"error": {
"title": "User module error",
@@ -2409,16 +5218,38 @@
"description": "Manage roles and permission assignments",
"newRole": "New role",
"createButton": "New role",
+ "totalBadge": "{count} roles",
+ "searchPlaceholder": "Search role name/description...",
+ "tableCaption": "Role list with name, description, type, status, user count, permission count, updated time and actions",
"colName": "Role name",
"colDescription": "Description",
"colIsLocked": "System locked",
+ "colType": "Type",
+ "colStatus": "Status",
+ "colUserCount": "Users",
"colPermissions": "Permission count",
+ "colValue": "Role value",
+ "colUpdatedAt": "Updated at",
"colActions": "Actions",
"viewDetail": "View detail",
"editRole": "Edit",
"editPermissions": "Edit permissions",
"deleteRole": "Delete",
+ "enableRole": "Enable",
+ "disableRole": "Disable",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "userCountValue": "{count}",
+ "typeSystem": "System",
+ "typeCustom": "Custom",
+ "deleteConfirmTitle": "Delete role",
+ "deleteConfirmDescription": "Are you sure you want to delete role \"{name}\"? This role is currently associated with {count} users; deletion will revoke their permissions. This action cannot be undone.",
+ "confirmDelete": "Delete",
+ "deleted": "Role deleted",
+ "enabledSuccess": "Role enabled",
+ "disabledSuccess": "Role disabled",
"lockedRole": "System role (cannot be deleted)",
+ "lockedRoleToggleWarn": "System locked role cannot be toggled",
"emptyTitle": "No roles",
"emptyDescription": "Create your first role to start managing permissions",
"emptyAction": "New role",
@@ -2428,16 +5259,19 @@
"detail": {
"title": "Role Detail",
"edit": "Edit",
+ "editRole": "Edit role",
"notFound": "Role not found",
"backToList": "Back to roles",
"sectionBasic": "Basic information",
"sectionPermissions": "Permission matrix",
"lockedNotice": "This is a built-in system role. Some fields cannot be modified.",
+ "adminLockedWarn": "admin is the highest-privilege system role. Adjusting its permissions may affect overall system availability. Proceed with caution.",
"fieldName": "Role name",
"fieldDescription": "Description",
"fieldIsLocked": "System locked",
"fieldPermissions": "Permission list",
- "noPermissions": "No permissions assigned to this role yet"
+ "noPermissions": "No permissions assigned to this role yet",
+ "sectionPermissionMatrix": "Permission action matrix"
},
"form": {
"titleCreate": "New role",
@@ -2446,7 +5280,50 @@
"fieldDescription": "Role description",
"fieldPermissions": "Permission assignments",
"submit": "Save",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "namePatternTitle": "Only lowercase letters, digits and underscores are allowed"
+ },
+ "createDialog": {
+ "titleCreate": "New role",
+ "titleEdit": "Edit role",
+ "descriptionCreate": "Create a new role. You can assign detailed action permissions via the matrix after creation.",
+ "descriptionEdit": "Update role name and description. Adjust permission points in the permission action matrix.",
+ "fieldName": "Role name",
+ "fieldNamePlaceholder": "e.g. Homeroom Teacher, Grade Lead",
+ "fieldDescription": "Role description",
+ "fieldDescriptionPlaceholder": "Briefly describe the responsibilities of this role (optional)",
+ "fieldValue": "Role value",
+ "fieldValuePlaceholder": "e.g. role:teacher",
+ "lockedNameNotice": "Built-in system role name cannot be modified",
+ "cancel": "Cancel",
+ "submitting": "Submitting...",
+ "submitCreate": "Create role",
+ "submitEdit": "Save changes",
+ "successCreated": "Role created successfully",
+ "successUpdated": "Role updated",
+ "errorNameRequired": "Please enter a role name",
+ "namePatternTitle": "Only lowercase letters, digits and underscores are allowed",
+ "errorNamePattern": "Role name can only contain lowercase letters, digits and underscores"
+ },
+ "matrix": {
+ "title": "{roleName} · Permission action matrix",
+ "locked": "Locked",
+ "permissionCount": "{count} permission points",
+ "loading": "Loading permission matrix...",
+ "empty": "No configurable permission points for this role",
+ "colPermission": "Permission point",
+ "actionRead": "View",
+ "actionCreate": "Create",
+ "actionUpdate": "Edit",
+ "actionDelete": "Delete",
+ "moduleLabel": "Module: {module}",
+ "save": "Save matrix",
+ "saving": "Saving...",
+ "successSaved": "Permission matrix saved",
+ "searchPlaceholder": "Search permission/module...",
+ "collapse": "Collapse",
+ "expand": "Expand",
+ "userImpactNotice": "This role is currently associated with {count} users. Permission changes will affect them immediately."
},
"error": {
"title": "Role module error",
@@ -2463,8 +5340,11 @@
"colPermission": "Permission point",
"colResource": "Resource",
"colAction": "Action",
+ "colValue": "Permission value",
+ "colKey": "Key",
"colRoleCount": "Associated roles",
"groupResource": "Group by resource",
+ "countLabel": "{count} items",
"emptyTitle": "No permission points",
"emptyDescription": "Permission catalog will be displayed after syncing with IAM service",
"mswNotice": "Permission catalog query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
@@ -2495,6 +5375,9 @@
"allStatuses": "All statuses",
"export": "Export CSV",
"exportCsv": "Export CSV",
+ "exportSuccess": "Successfully exported {count} records",
+ "exportEmpty": "No data to export",
+ "exportFailed": "Export failed: {message}",
"colTimestamp": "Time",
"colUser": "User",
"colUserId": "User ID",
@@ -2506,7 +5389,10 @@
"colResourceId": "Resource ID",
"colIp": "IP",
"colDetails": "Details",
+ "colActions": "Actions",
"total": "{count} records",
+ "resetFilter": "Reset",
+ "pageOf": "Page {page} / {totalPages}",
"emptyTitle": "No audit logs",
"emptyDescription": "Adjust filters or change the date range and retry",
"mswNotice": "Audit log query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
@@ -2525,6 +5411,19 @@
"statsToday": "Today's logs",
"statsErrors": "Error logs",
"statsUsers": "Active users",
+ "auditEventsToday": "Today's audit events",
+ "failedLoginsToday": "Today's failed logins",
+ "dataChangesToday": "Today's data changes",
+ "totalAuditLogs": "Total audit logs",
+ "sectionError": "Section failed to load",
+ "sectionRetry": "Retry",
+ "chartEmpty": "No data",
+ "quickLinkAuditLogs": "Audit Logs",
+ "quickLinkAuditLogsDesc": "View all user action audit records",
+ "quickLinkLoginLogs": "Login Logs",
+ "quickLinkLoginLogsDesc": "View signin/signout/signup records",
+ "quickLinkDataChanges": "Data Changes",
+ "quickLinkDataChangesDesc": "View table change records and stats",
"trendTitle": "Last 7 days trend",
"trendLast7Days": "Last 7 days",
"distributionTitle": "Data change action distribution",
@@ -2542,6 +5441,9 @@
"filterAction": "Filter by action",
"filterStatus": "Filter by status",
"filterUser": "Filter by user",
+ "filterDateRange": "Date range",
+ "startDate": "Start date",
+ "endDate": "End date",
"actionFilter": "Filter by action",
"statusFilter": "Filter by status",
"allActions": "All actions",
@@ -2553,15 +5455,22 @@
"statusFailure": "Failed",
"export": "Export CSV",
"exportCsv": "Export CSV",
+ "exportSuccess": "Successfully exported {count} records",
+ "exportEmpty": "No data to export",
+ "exportFailed": "Export failed: {message}",
"colTimestamp": "Time",
"colUser": "User",
"colUserId": "User ID",
"colUserName": "Username",
"colAction": "Action",
"colStatus": "Status",
+ "colErrorMessage": "Failure reason",
"colIp": "IP",
"colUserAgent": "User Agent",
+ "colActions": "Actions",
"total": "{count} records",
+ "resetFilter": "Reset",
+ "pageOf": "Page {page} / {totalPages}",
"emptyTitle": "No login logs",
"emptyDescription": "Adjust filters and retry",
"mswNotice": "Login log query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
@@ -2573,6 +5482,9 @@
"filterTable": "Filter by table",
"filterAction": "Filter by action",
"filterUser": "Filter by user",
+ "filterDateRange": "Date range",
+ "startDate": "Start date",
+ "endDate": "End date",
"tableFilter": "Filter by table",
"actionFilter": "Filter by action",
"allTables": "All tables",
@@ -2582,11 +5494,19 @@
"actionDelete": "Delete",
"export": "Export CSV",
"exportCsv": "Export CSV",
+ "exportSuccess": "Successfully exported {count} records",
+ "exportEmpty": "No data to export",
+ "exportFailed": "Export failed: {message}",
"sectionStats": "Change statistics",
"statsTitle": "Change statistics",
"statsAction": "Action",
"statsCount": "Count",
"statsLastChange": "Last change",
+ "topTables": "Top 8 tables",
+ "expandDetail": "View change diff",
+ "collapseDetail": "Collapse change diff",
+ "oldValue": "Before",
+ "newValue": "After",
"colTimestamp": "Time",
"colTable": "Table",
"colRecordId": "Record ID",
@@ -2595,11 +5515,51 @@
"colUserId": "User ID",
"colUserName": "Username",
"colChanges": "Changes",
+ "colActions": "Actions",
"total": "{count} records",
+ "resetFilter": "Reset",
+ "pageOf": "Page {page} / {totalPages}",
"emptyTitle": "No data change logs",
"emptyDescription": "Adjust filters and retry",
"mswNotice": "Data change log query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
},
+ "detail": {
+ "title": "Audit Log Detail",
+ "description": "View audit log details",
+ "viewDetail": "View detail",
+ "userId": "User ID",
+ "userName": "Username",
+ "action": "Action",
+ "module": "Module",
+ "resourceId": "Resource ID",
+ "tableName": "Table",
+ "recordId": "Record ID",
+ "changes": "Changes",
+ "oldValue": "Before",
+ "newValue": "After",
+ "errorMessage": "Failure reason",
+ "ipAddress": "IP Address",
+ "userAgent": "User Agent",
+ "status": "Status",
+ "details": "Details",
+ "createdAt": "Created At"
+ },
+ "retention": {
+ "title": "Audit Log Retention Settings",
+ "description": "Configure retention days and auto cleanup",
+ "retentionDays": "Audit log retention days",
+ "retentionDaysDescription": "Range 7-3650 days, default 90",
+ "loginLogRetentionDays": "Login log retention days",
+ "loginLogRetentionDaysDescription": "Range 7-3650 days, default 365",
+ "autoCleanupEnabled": "Enable auto cleanup",
+ "autoCleanupEnabledDescription": "Auto clean expired logs by retention",
+ "save": "Save",
+ "purge": "Purge now",
+ "saveSuccess": "Retention config saved",
+ "purgeConfirm": "Purge expired logs? Cannot undo.",
+ "purgeSuccess": "Purged: {auditLogsDeleted} audit, {loginLogsDeleted} login, {dataChangeLogsDeleted} data change logs",
+ "loadFailed": "Failed to load retention config"
+ },
"error": {
"title": "Audit log module error",
"unknown": "Unknown error in audit log module",
@@ -2626,6 +5586,10 @@
"copyCode": "Copy",
"allStatuses": "All statuses",
"total": "{count} records",
+ "statsTotal": "Total codes",
+ "statsUsed": "Used",
+ "statsUnused": "Unused",
+ "statsExpired": "Expired",
"statusActive": "Active",
"statusUnused": "Unused",
"statusUsed": "Used up",
@@ -2634,7 +5598,12 @@
"emptyTitle": "No invitation codes",
"emptyDescription": "Generate your first invitation code to invite users",
"emptyAction": "Generate invitation code",
- "mswNotice": "Invitation code list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Invitation code list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "deleteSelected": "Delete selected ({count})",
+ "deleteSuccess": "Deleted {count} invitation codes",
+ "deleteConfirmTitle": "Confirm batch delete",
+ "deleteConfirmDesc": "About to delete {count} invitation codes. This action cannot be undone.",
+ "confirmDelete": "Confirm delete"
},
"generateForm": {
"title": "Generate invitation code",
@@ -2644,6 +5613,35 @@
"submit": "Generate",
"cancel": "Cancel"
},
+ "generateDialog": {
+ "title": "Generate invitation codes in batch",
+ "description": "Generate multiple invitation codes at once to invite teachers, students, or parents to join the system.",
+ "fieldCount": "Count",
+ "countHint": "Up to 100 codes per batch",
+ "fieldRole": "Role",
+ "roles": {
+ "teacher": "Teacher",
+ "student": "Student",
+ "parent": "Parent",
+ "admin": "Admin"
+ },
+ "fieldExpireDays": "Validity (days)",
+ "expireDaysOption": "{days} days",
+ "fieldNote": "Note",
+ "noteHint": "Optional, used only to record the purpose of this batch, up to 500 characters",
+ "cancel": "Cancel",
+ "submit": "Generate",
+ "generating": "Generating…",
+ "success": "Successfully generated {count} invitation codes",
+ "errorCountRange": "Count must be between 1 and 100",
+ "fieldEmail": "Recipient email",
+ "fieldEmailPlaceholder": "e.g. teacher@example.com (optional)",
+ "fieldClassId": "Linked class",
+ "fieldClassIdPlaceholder": "e.g. cls-001 (optional)",
+ "resultTitle": "Generated results",
+ "copyCode": "Copy",
+ "done": "Done"
+ },
"error": {
"title": "Invitation code module error",
"unknown": "Unknown error in invitation code module",
@@ -2772,7 +5770,8 @@
"fieldCurrentYear": "Current academic year",
"fieldCurrentTerm": "Current term",
"submit": "Save",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "fieldCode": "School code"
},
"deleteConfirm": {
"title": "Delete school",
@@ -2784,7 +5783,9 @@
"title": "School module error",
"unknown": "Unknown error in school module",
"retry": "Retry"
- }
+ },
+ "colCode": "School code",
+ "colUpdatedAt": "Updated at"
},
"classes": {
"title": "Class Management",
@@ -2814,6 +5815,111 @@
"title": "Class module error",
"unknown": "Unknown error in class module",
"retry": "Retry"
+ },
+ "manageSchedule": "Manage schedule",
+ "manageInvitation": "Invitation",
+ "schedule": {
+ "form": {
+ "titleCreate": "New schedule entry",
+ "titleEdit": "Edit schedule entry",
+ "classLabel": "Class",
+ "fieldWeekday": "Weekday",
+ "fieldPeriod": "Period",
+ "fieldSubject": "Subject",
+ "fieldTeacher": "Teacher",
+ "fieldClassroom": "Classroom",
+ "fieldStartTime": "Start time",
+ "fieldEndTime": "End time",
+ "subjectPlaceholder": "e.g. Math",
+ "teacherPlaceholder": "e.g. Mr. Smith",
+ "classroomPlaceholder": "e.g. Room 301",
+ "periodN": "Period {n}",
+ "save": "Save",
+ "create": "Create",
+ "createSuccess": "Schedule entry created",
+ "editSuccess": "Schedule entry updated",
+ "deleteTitle": "Delete schedule entry",
+ "deleteMessage": "Delete {weekday} period {period} {subject}?",
+ "deleteConfirm": "Delete",
+ "deleteSuccess": "Schedule entry deleted"
+ },
+ "manager": {
+ "title": "Schedule management",
+ "add": "Add entry",
+ "colPeriod": "Period",
+ "colSubject": "Subject",
+ "colTeacher": "Teacher",
+ "colClassroom": "Classroom",
+ "colTime": "Time",
+ "colActions": "Actions",
+ "empty": "No schedule entries"
+ },
+ "weekday": {
+ "1": "Mon",
+ "2": "Tue",
+ "3": "Wed",
+ "4": "Thu",
+ "5": "Fri",
+ "6": "Sat",
+ "7": "Sun"
+ }
+ },
+ "invitation": {
+ "title": "Invitation code management",
+ "classLabel": "Class",
+ "generate": "Generate invitation code",
+ "generateWithCustom": "Custom invitation code",
+ "generateSuccess": "Invitation code generated",
+ "defaultDuration": "Leave empty for no expiration",
+ "defaultMaxUses": "Leave empty for unlimited uses",
+ "expiresInHours": "Validity (hours)",
+ "maxUsesLabel": "Max uses",
+ "customNote": "Note",
+ "customNotePlaceholder": "e.g. Summer course invitation",
+ "copy": "Copy",
+ "copied": "Copied to clipboard",
+ "revoke": "Revoke",
+ "revokeSuccess": "Invitation code revoked",
+ "revokeConfirm": "Revoke this invitation code? This cannot be undone.",
+ "neverExpires": "Never expires",
+ "empty": "No invitation codes",
+ "colCode": "Code",
+ "colStatus": "Status",
+ "colUsedCount": "Used count",
+ "colExpiresAt": "Expires at",
+ "colNote": "Note",
+ "colActions": "Actions",
+ "status": {
+ "active": "Active",
+ "used": "Used up",
+ "expired": "Expired",
+ "revoked": "Revoked"
+ }
+ },
+ "colHomeroomLabel": "Homeroom label",
+ "colRoom": "Classroom",
+ "colSubjectTeachers": "Subject teachers",
+ "colUpdatedAt": "Updated at",
+ "form": {
+ "titleCreate": "New class",
+ "titleEdit": "Edit class",
+ "fieldName": "Class name",
+ "fieldSchool": "School",
+ "fieldGrade": "Grade",
+ "fieldHeadTeacher": "Head teacher",
+ "fieldHomeroomLabel": "Homeroom label",
+ "fieldHomeroomLabelPlaceholder": "e.g. Class 1, Class 2",
+ "fieldRoom": "Classroom",
+ "fieldRoomPlaceholder": "e.g. Room 301",
+ "fieldHomeroom": "Homeroom teacher",
+ "submit": "Save",
+ "cancel": "Cancel"
+ },
+ "deleteConfirm": {
+ "title": "Delete class",
+ "message": "Are you sure you want to delete class \"{name}\"? This action cannot be undone.",
+ "confirm": "Confirm delete",
+ "cancel": "Cancel"
}
},
"departments": {
@@ -2842,7 +5948,8 @@
"fieldSchool": "School",
"fieldHead": "Head ID (optional)",
"submit": "Save",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "fieldDescription": "Description"
},
"deleteConfirm": {
"title": "Delete department",
@@ -2854,7 +5961,9 @@
"title": "Department module error",
"unknown": "Unknown error in department module",
"retry": "Retry"
- }
+ },
+ "colDescription": "Description",
+ "colUpdatedAt": "Updated at"
},
"academicYear": {
"title": "Academic Year Management",
@@ -2887,7 +5996,8 @@
"fieldEndDate": "End date",
"fieldIsActive": "Set as currently active",
"submit": "Save",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "fieldIsActiveHint": "Once activated, this academic year will be the current academic year"
},
"deleteConfirm": {
"title": "Delete academic year",
@@ -2899,7 +6009,10 @@
"title": "Academic year module error",
"unknown": "Unknown error in academic year module",
"retry": "Retry"
- }
+ },
+ "activeYearCardTitle": "Currently active academic year",
+ "activeYearCardDescription": "Shows the currently activated academic year information",
+ "activeYearCardEmpty": "No active academic year"
},
"grades": {
"title": "Grade Management",
@@ -2945,7 +6058,11 @@
"title": "Grade module error",
"unknown": "Unknown error in grade module",
"retry": "Retry"
- }
+ },
+ "insights": "Grade insights",
+ "gradeOverviewSection": "Grade overview cards",
+ "colTeachingHead": "Teaching head",
+ "notSet": "Not set"
}
},
"announcements": {
@@ -2975,7 +6092,8 @@
"emptyTitle": "No announcements",
"emptyDescription": "Adjust filters or create your first announcement",
"emptyAction": "New announcement",
- "mswNotice": "Announcement list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Announcement list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "pageIndicator": "Page {page} of {total}"
},
"detail": {
"title": "Announcement Detail",
@@ -3002,7 +6120,9 @@
"delete": "Delete",
"deleteConfirm": "Are you sure you want to delete this announcement? This action cannot be undone.",
"fieldGrades": "Grade",
- "fieldClasses": "Class"
+ "fieldClasses": "Class",
+ "fieldReadCount": "Read count",
+ "deleteConfirmDesc": "This action cannot be undone. The announcement will be permanently deleted."
},
"form": {
"titleCreate": "New announcement",
@@ -3028,8 +6148,18 @@
"fieldStatus": "Status",
"fieldAudience": "Audience",
"fieldPinned": "Pin",
+ "fieldGrades": "Associated grades",
+ "fieldGradesHint": "Separate multiple grade IDs with commas (e.g., grade-1,grade-2)",
"errorTitleRequired": "Please enter a title",
- "errorContentRequired": "Please enter the content"
+ "errorContentRequired": "Please enter the content",
+ "multiSelect": {
+ "placeholder": "Select grades",
+ "selected": "{count} selected",
+ "toggle": "Toggle dropdown",
+ "loading": "Loading...",
+ "empty": "No grades available",
+ "remove": "Remove {name}"
+ }
},
"error": {
"title": "Announcement module error",
@@ -3047,6 +6177,13 @@
"statByType": "By type",
"uploadButton": "Upload file",
"searchPlaceholder": "Search file name...",
+ "fileTypeFilter": "Filter by file type",
+ "allTypes": "All types",
+ "typeImage": "Image",
+ "typeDocument": "Document",
+ "typeVideo": "Video",
+ "typeAudio": "Audio",
+ "typeOther": "Other",
"colName": "File name",
"colSize": "Size",
"colMimeType": "Type",
@@ -3060,7 +6197,57 @@
"emptyTitle": "No files",
"emptyDescription": "Adjust filters or upload new files",
"emptyAction": "Upload file",
- "mswNotice": "File list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "File list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "selectAll": "Select all",
+ "selectRow": "Select row",
+ "statTopType": "Top 2 types",
+ "statTopTypeHint": "By file count"
+ },
+ "upload": {
+ "button": "Upload file",
+ "uploading": "Uploading...",
+ "success": "Upload succeeded",
+ "ariaLabel": "Select a file to upload",
+ "dragDrop": "Drag files here or click to select",
+ "dropHere": "Drop to upload",
+ "progress": "Upload progress {percent}%",
+ "dragActive": "Drag active",
+ "multiUpload": "{count} files selected",
+ "remaining": "{count} remaining"
+ },
+ "batch": {
+ "selectAll": "Select all",
+ "selectedCount": "{count} selected",
+ "clearSelection": "Clear selection",
+ "batchDelete": "Batch delete",
+ "confirming": "Processing...",
+ "deleteSuccess": "Deleted {count} files",
+ "confirmTitle": "Confirm batch delete",
+ "confirmDescription": "About to delete {count} files. This action cannot be undone.",
+ "confirmCancel": "Cancel",
+ "confirmSubmit": "Confirm delete"
+ },
+ "preview": {
+ "trigger": "Preview",
+ "title": "File preview",
+ "download": "Download",
+ "zoomIn": "Zoom in",
+ "zoomOut": "Zoom out",
+ "text": {
+ "title": "Text preview",
+ "hint": "Click the button below to load text content",
+ "load": "Load content",
+ "loading": "Loading...",
+ "error": "Failed to load: {message}"
+ },
+ "office": {
+ "title": "Online preview is not supported for Office files",
+ "hint": "Please download to view"
+ },
+ "other": {
+ "title": "Online preview is not supported for this file type",
+ "hint": "Please download to view"
+ }
},
"error": {
"title": "File module error",
@@ -3087,8 +6274,15 @@
"edit": "Edit",
"delete": "Delete",
"testConnection": "Test connection",
+ "testing": "Testing...",
+ "testSuccess": "Connected, latency {latency} ms",
+ "testFailed": "Connection failed: {message}",
"active": "Enable",
"inactive": "Disable",
+ "colVisibility": "Visibility",
+ "colIsDefault": "Default",
+ "defaultProvider": "Default",
+ "nonDefaultProvider": "Not default",
"emptyTitle": "No AI providers",
"emptyDescription": "Add your first AI provider to enable AI capabilities",
"emptyAction": "New provider",
@@ -3110,17 +6304,52 @@
"colDate": "Date",
"mswNotice": "AI settings query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
},
+ "deleteDialog": {
+ "title": "Delete AI Provider",
+ "warning": "This action is irreversible. All configuration for provider \"{name}\" will be permanently lost.",
+ "confirmPrompt": "Type the provider name \"{name}\" to confirm deletion:",
+ "confirmInputPlaceholder": "Enter provider name",
+ "confirmInputHint": "The entered name must exactly match the name shown above.",
+ "cancel": "Cancel",
+ "confirm": "Delete",
+ "deleting": "Deleting...",
+ "success": "Deleted provider \"{name}\""
+ },
"form": {
"titleCreate": "New AI Provider",
"titleEdit": "Edit AI Provider",
+ "description": "Configure AI Provider connection parameters. Fields marked with * are required.",
"fieldName": "Name",
+ "fieldNamePlaceholder": "e.g. Default OpenAI",
"fieldType": "Type",
"fieldScope": "Scope",
+ "fieldScopePlaceholder": "e.g. global / school_",
"fieldModel": "Model",
+ "fieldModelPlaceholder": "e.g. gpt-4o-mini",
"fieldApiBase": "API Base",
- "fieldIsActive": "Enable",
+ "fieldApiBasePlaceholder": "e.g. https://api.openai.com/v1 (optional)",
+ "fieldApiKey": "API Key",
+ "fieldApiKeyPlaceholder": "Enter API key",
+ "fieldApiKeyPlaceholderEdit": "Leave empty to keep existing key",
+ "fieldApiKeyHint": "In edit mode, leaving this empty keeps the existing key.",
+ "fieldIsActive": "Enable this provider",
"submit": "Save",
- "cancel": "Cancel"
+ "saving": "Saving...",
+ "cancel": "Cancel",
+ "errorNameRequired": "Name is required",
+ "errorModelRequired": "Model is required",
+ "errorApiBaseInvalid": "API Base must be a valid http(s) URL",
+ "createSuccess": "AI provider created",
+ "updateSuccess": "AI provider updated",
+ "fieldVisibility": "Visibility",
+ "fieldIsDefault": "Set as default provider",
+ "visibilityPrivate": "Private",
+ "visibilityShared": "Shared",
+ "visibilityPublic": "Public",
+ "testConnection": "Test connection",
+ "testing": "Testing...",
+ "testSuccess": "Connected, latency {latency} ms",
+ "testFailed": "Connection failed: {message}"
},
"error": {
"title": "AI settings module error",
@@ -3355,7 +6584,65 @@
"sectionContent": "Plan content",
"fieldStatus": "Status",
"fieldCreatedAt": "Created at",
- "fieldUpdatedAt": "Updated at"
+ "fieldUpdatedAt": "Updated at",
+ "sectionBasic": "Basic info",
+ "sectionSchedule": "Teaching schedule",
+ "sectionGoals": "Objectives",
+ "sectionResources": "Resources",
+ "fieldTitle": "Title",
+ "fieldDescription": "Description",
+ "fieldGrade": "Grade",
+ "fieldClass": "Class",
+ "fieldSubject": "Subject",
+ "fieldTeacher": "Teacher",
+ "fieldAcademicYear": "Academic year",
+ "edit": "Edit",
+ "notFound": "Course plan not found",
+ "emptySchedule": "No schedule data",
+ "emptyGoals": "No objectives",
+ "emptyResources": "No resources",
+ "weekPlansHint": "{count} weekly plans",
+ "addWeekPlan": "Add weekly plan",
+ "emptyWeekPlans": "No weekly plans",
+ "emptyWeekPlansCta": ", click the button above to create the first weekly plan",
+ "colWeek": "Week",
+ "colTopic": "Topic",
+ "colHours": "Hours",
+ "colChapter": "Chapter",
+ "colActions": "Actions",
+ "statusCompleted": "Completed",
+ "statusPending": "Pending",
+ "notesLabel": "Notes: {notes}",
+ "moveUpAria": "Move week {week} up",
+ "moveDownAria": "Move week {week} down",
+ "editItem": "Edit",
+ "reorderSuccess": "Order updated"
+ },
+ "itemEditor": {
+ "createTitle": "New weekly plan",
+ "editTitle": "Edit weekly plan",
+ "week": "Week",
+ "hours": "Hours",
+ "topic": "Topic",
+ "topicPlaceholder": "Enter weekly topic",
+ "content": "Content",
+ "contentPlaceholder": "Enter weekly teaching content",
+ "chapter": "Textbook chapter",
+ "chapterPlaceholder": "e.g. Chapter 3",
+ "completedAt": "Completed date",
+ "notes": "Notes",
+ "notesPlaceholder": "Optional notes",
+ "cancel": "Cancel",
+ "save": "Save",
+ "saving": "Saving...",
+ "delete": "Delete",
+ "markComplete": "Mark complete",
+ "markIncomplete": "Mark incomplete",
+ "errorTopicRequired": "Topic is required",
+ "createSuccess": "Weekly plan created",
+ "updateSuccess": "Weekly plan updated",
+ "deleteSuccess": "Weekly plan deleted",
+ "toggleSuccess": "Completion status updated"
},
"create": {
"title": "New course plan",
@@ -3366,12 +6653,101 @@
"fieldAcademicYear": "Academic year",
"fieldContent": "Content",
"submit": "Create",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "description": "Fill in basic course plan information",
+ "success": "Course plan created",
+ "error": "Failed to create course plan",
+ "fieldTitle": "Title",
+ "fieldDescription": "Description",
+ "fieldGradeId": "Grade",
+ "fieldClassId": "Class",
+ "fieldSubjectId": "Subject",
+ "fieldTeacherId": "Teacher",
+ "fieldAcademicYearId": "Academic year",
+ "fieldStatus": "Status",
+ "errorTitleRequired": "Title is required",
+ "errorGradeRequired": "Grade is required",
+ "errorClassRequired": "Class is required",
+ "errorSubjectRequired": "Subject is required",
+ "contractPending": "Course plan create contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "fromTemplate": "From template",
+ "fromTemplateHint": "Pick a template to prefill form fields",
+ "openTemplatePicker": "Choose template",
+ "templateApplied": "Template applied",
+ "fieldSemester": "Semester",
+ "fieldSyllabus": "Syllabus",
+ "fieldObjectives": "Objectives",
+ "fieldTotalHours": "Total hours",
+ "fieldWeeklyHours": "Weekly hours",
+ "fieldStartDate": "Start date",
+ "fieldEndDate": "End date"
},
"error": {
"title": "Course plan module error",
"unknown": "Unknown error in course plan module",
"retry": "Retry"
+ },
+ "edit": {
+ "title": "Edit course plan",
+ "description": "Modify course plan content and status",
+ "submit": "Save",
+ "cancel": "Cancel",
+ "success": "Course plan updated",
+ "error": "Failed to update course plan",
+ "fieldTitle": "Title",
+ "fieldDescription": "Description",
+ "fieldGradeId": "Grade",
+ "fieldClassId": "Class",
+ "fieldSubjectId": "Subject",
+ "fieldTeacherId": "Teacher",
+ "fieldAcademicYearId": "Academic year",
+ "fieldStatus": "Status",
+ "errorTitleRequired": "Title is required",
+ "errorGradeRequired": "Grade is required",
+ "errorClassRequired": "Class is required",
+ "errorSubjectRequired": "Subject is required",
+ "notFound": "Course plan not found",
+ "backToDetail": "Back to detail",
+ "sectionSchedule": "Teaching schedule",
+ "scheduleNotice": "Schedule reordering is local only; not synced on save",
+ "scheduleEmpty": "No schedule data",
+ "colWeek": "Week",
+ "colTopic": "Topic",
+ "colHours": "Hours",
+ "colActions": "Actions"
+ },
+ "sortableWeekRow": {
+ "notes": "Notes: {notes}",
+ "moveUpAria": "Move week {week} up",
+ "moveDownAria": "Move week {week} down"
+ },
+ "templates": {
+ "title": "Create from template",
+ "description": "Pick a template to prefill form fields",
+ "searchPlaceholder": "Search templates...",
+ "loading": "Loading templates...",
+ "empty": "No templates available",
+ "cancel": "Cancel",
+ "confirm": "Apply template",
+ "errorNoSelection": "Please select a template first",
+ "errorApply": "Failed to apply template"
+ },
+ "export": {
+ "button": "Export CSV",
+ "filename": "course-plans",
+ "colName": "Name",
+ "colClass": "Class",
+ "colSubject": "Subject",
+ "colTeacher": "Teacher",
+ "colAcademicYear": "Academic year",
+ "colStatus": "Status",
+ "colCreatedAt": "Created at",
+ "statusDraft": "Draft",
+ "statusPublished": "Published",
+ "statusArchived": "Archived",
+ "success": "Export succeeded",
+ "error": "Export failed: {message}",
+ "errorEmpty": "No data to export"
}
},
"curriculumMap": {
@@ -3385,11 +6761,30 @@
"statPublished": "Published",
"statSubmitted": "Submitted",
"statStandardsLinked": "Linked standards",
+ "statsTeachers": "Total teachers",
+ "statsLessonPlans": "Total lesson plans",
+ "statsPublished": "Published",
+ "statsSubmitted": "Submitted",
+ "statsLinkedStandards": "Linked standards",
"colStandard": "Standard",
"colGrade": "Grade",
"colCoverageRate": "Coverage rate",
"colLessonPlanCount": "Lesson plans",
+ "heatmapTitle": "Standard coverage heatmap",
+ "heatmapStandards": "Standard",
+ "heatmapGrades": "Grade",
+ "heatmapCoverage": "Coverage rate",
+ "heatmapLinkedTotalHint": "Cell shows coverage rate and linked/total lesson plans",
+ "heatmapCellTooltip": "{standard} / {grade}: {rate} ({linked}/{total})",
+ "legendTitle": "Legend",
+ "legendHigh": "High (≥80%)",
+ "legendMedium": "Medium (50-80%)",
+ "legendLow": "Low (20-50%)",
+ "legendCritical": "Very low (<20%)",
+ "legendNone": "No coverage",
"emptyHeatmap": "No coverage data",
+ "emptyTitle": "No coverage data",
+ "emptyDescription": "No standard coverage data yet. Try again later or check contract status.",
"mswNotice": "Curriculum map query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
},
"error": {
@@ -3480,7 +6875,79 @@
"emptyTitle": "No questions",
"emptyDescription": "Adjust filters or create the first question.",
"emptyAction": "New Question",
- "mswNotice": "Question bank list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "mswNotice": "Question bank list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "selectAll": "Select all on this page",
+ "selectRow": "Select this row"
+ },
+ "detailDialog": {
+ "title": "Question detail",
+ "close": "Close",
+ "retry": "Retry",
+ "type": "Type",
+ "difficulty": "Difficulty",
+ "status": "Status",
+ "subject": "Subject",
+ "textbook": "Textbook",
+ "knowledgePoint": "Knowledge point",
+ "content": "Question content",
+ "noContent": "No content available",
+ "answer": "Correct answer",
+ "explanation": "Explanation",
+ "source": "Source",
+ "createdBy": "Created by",
+ "createdAt": "Created at",
+ "updatedAt": "Updated at",
+ "noData": "No detail data"
+ },
+ "createDialog": {
+ "title": "New Question",
+ "description": "Fill in the question content, answer, and explanation. It will appear in the question bank after submission.",
+ "fieldType": "Type",
+ "fieldContent": "Content",
+ "fieldAnswer": "Answer",
+ "fieldExplanation": "Explanation",
+ "fieldDifficulty": "Difficulty",
+ "fieldKnowledgePoint": "Knowledge point ID",
+ "fieldKnowledgePointPlaceholder": "e.g. kp-001",
+ "fieldSource": "Source",
+ "fieldSourcePlaceholder": "e.g. PEP compulsory 1 (optional)",
+ "types": {
+ "single_choice": "Single choice",
+ "multiple_choice": "Multiple choice",
+ "fill_blank": "Fill in the blank",
+ "short_answer": "Short answer",
+ "essay": "Essay",
+ "true_false": "True / False"
+ },
+ "difficultyEasy": "Easy",
+ "difficultyMedium": "Medium",
+ "difficultyHard": "Hard",
+ "cancel": "Cancel",
+ "submit": "Submit",
+ "submitting": "Submitting...",
+ "success": "Question created successfully",
+ "errorContentRequired": "Question content is required",
+ "errorAnswerRequired": "Answer is required",
+ "errorKnowledgePointRequired": "Knowledge point ID is required"
+ },
+ "importExport": {
+ "import": "Import",
+ "export": "Export",
+ "importSuccess": "Imported {imported} records, skipped {skipped}",
+ "exportEmpty": "No questions to export under current filters",
+ "exportSuccess": "Exported {count} questions",
+ "exportFailed": "Export failed, please retry"
+ },
+ "batch": {
+ "selectedCount": "{count} selected",
+ "clearSelection": "Clear selection",
+ "batchDelete": "Batch delete",
+ "confirmTitle": "Confirm batch delete",
+ "confirmDescription": "You are about to delete {count} questions. This action cannot be undone. Continue?",
+ "confirmCancel": "Cancel",
+ "confirmSubmit": "Confirm delete",
+ "confirming": "Deleting...",
+ "deleteSuccess": "Deleted {deleted} records, {failed} failed"
},
"error": {
"title": "Question bank module error",
@@ -3500,7 +6967,8 @@
"statusPublished": "Published",
"statusArchived": "Archived",
"statusSubmitted": "Submitted",
- "statsTotal": "Total lesson plans",
+ "statsTitle": "Lesson Plan Statistics Overview",
+ "statsTotal": "Total",
"statsPublished": "Published",
"statsDraft": "Draft",
"statsArchived": "Archived",
@@ -3543,6 +7011,15 @@
"emptyMaterials": "No teaching materials",
"contractPending": "Resource fields contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
},
+ "delete": {
+ "button": "Delete",
+ "title": "Delete lesson plan",
+ "description": "Are you sure you want to delete this lesson plan? It will be marked as archived and no longer shown.",
+ "confirm": "Confirm delete",
+ "cancel": "Cancel",
+ "success": "Lesson plan deleted",
+ "error": "Failed to delete lesson plan, please retry later"
+ },
"error": {
"title": "Lesson plan module error",
"unknown": "Unknown error in lesson plan module",
@@ -3553,23 +7030,56 @@
"list": {
"title": "Error Book Analysis",
"description": "School-wide error book aggregate analysis (limited to 500 students)",
- "sectionStats": "Statistics overview",
- "statTotalStudents": "Total students",
- "statTotalErrorQuestions": "Total error questions",
- "statTotalErrorCount": "Error count",
- "statAvgErrorRate": "Average error rate",
- "sectionBySubject": "By subject distribution",
- "sectionTopStudents": "Top 50 students",
- "sectionTopWrongQuestions": "Top 10 high-frequency error questions",
- "colSubject": "Subject",
- "colErrorCount": "Error count",
- "colQuestionCount": "Questions",
- "colErrorRate": "Error rate",
- "colStudent": "Student",
- "colClass": "Class",
- "colQuestion": "Question",
+ "statsTotalStudents": "Total students",
+ "statsTotalErrors": "Total errors",
+ "statsAvgPerStudent": "Avg per student",
+ "statsHighFreqErrors": "High-freq errors",
+ "statsTotalErrorQuestions": "Total error questions",
+ "subjectTabsAll": "All",
+ "subjectTabs": "Subject",
+ "distributionTitle": "Subject distribution",
+ "distributionSubject": "Subject",
+ "distributionCount": "Error count",
+ "chapterWeaknessTitle": "Chapter weaknesses",
+ "chapterWeaknessChapter": "Chapter",
+ "chapterWeaknessErrorRate": "Error rate",
+ "knowledgeWeaknessTitle": "Knowledge point weaknesses",
+ "knowledgeWeaknessPoint": "Knowledge point",
+ "knowledgeWeaknessErrorRate": "Error rate",
+ "topStudentsTitle": "Top 50 students",
+ "topStudentsRank": "Rank",
+ "topStudentsName": "Student",
+ "topStudentsErrorCount": "Error count",
+ "topWrongQuestionsTitle": "Top 10 high-frequency error questions",
+ "topWrongQuestionsContent": "Question content",
+ "topWrongQuestionsErrorCount": "Error count",
"emptyTitle": "No error data",
- "mswNotice": "Error book analysis query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "emptyDescription": "No error book stats loaded yet. Please retry later or adjust filters.",
+ "mswNotice": "Error book analysis query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "topWrongQuestionsActions": "Actions",
+ "viewDetail": "View detail",
+ "classDistributionTitle": "Class distribution",
+ "classDistributionClass": "Class",
+ "classDistributionCount": "Error count",
+ "exportCsv": "Export CSV",
+ "exporting": "Exporting...",
+ "exportSuccess": "Exported {count} error records",
+ "total": "{count} records"
+ },
+ "detailDialog": {
+ "title": "Error Detail",
+ "close": "Close",
+ "retry": "Retry",
+ "studentName": "Student",
+ "className": "Class",
+ "errorCount": "Error Count",
+ "question": "Question",
+ "noContent": "No content available",
+ "correctAnswer": "Correct Answer",
+ "analysis": "Analysis",
+ "lastErrorTime": "Last Error Time",
+ "knowledgePoint": "Knowledge Point",
+ "noData": "No detail data"
},
"error": {
"title": "Error book analysis module error",
@@ -3631,35 +7141,278 @@
"list": {
"title": "Attendance Management",
"description": "School-wide attendance aggregate analysis",
- "sectionStats": "Statistics overview",
- "statTotalRecords": "Total records",
- "statPresentRate": "Attendance rate",
- "statAbsentRate": "Absentee rate",
- "statLateRate": "Late rate",
- "statEarlyLeaveRate": "Early leave rate",
- "sectionByClass": "Class comparison",
- "sectionCorrelation": "Attendance-score correlation analysis",
- "filterClass": "Filter by class",
- "filterStatus": "Filter by status",
- "filterDate": "Filter by date",
- "colClass": "Class",
- "colPresentRate": "Attendance rate",
- "colAbsentRate": "Absentee rate",
- "colAvgScore": "Average score",
- "colCorrelation": "Correlation coefficient",
+ "gradeFilter": "Filter by grade",
+ "classFilter": "Filter by class",
+ "statusFilter": "Filter by status",
+ "dateFilter": "Filter by date",
+ "allGrades": "All grades",
+ "allClasses": "All classes",
+ "allStatuses": "All statuses",
+ "statusPresent": "Present",
+ "statusAbsent": "Absent",
+ "statusLate": "Late",
+ "statusLeave": "Leave",
+ "statsTotalRecords": "Total records",
+ "statsPresentRate": "Attendance rate",
+ "statsAbsentRate": "Absentee rate",
+ "statsLateRate": "Late rate",
+ "statsEarlyLeaveRate": "Early leave rate",
+ "statsAbnormalRate": "Abnormal rate",
+ "statsAvgCorrelation": "Avg correlation",
+ "recordsTitle": "Attendance records",
+ "recordsDescription": "Attendance details by filters",
+ "recordsTotal": "{count} records",
+ "recordsEmptyTitle": "No attendance records",
+ "recordsEmptyDescription": "No attendance records loaded. Please retry later or adjust filters.",
"colStudent": "Student",
+ "colClass": "Class",
"colDate": "Date",
"colStatus": "Status",
- "colRecordedBy": "Recorded by",
- "total": "{count} records",
+ "colNote": "Note",
+ "colRecorder": "Recorder",
+ "classComparisonTitle": "Class comparison",
+ "classComparisonClass": "Class",
+ "classComparisonRate": "Attendance rate",
+ "correlationTitle": "Attendance-grade correlation analysis",
+ "correlationAttendance": "Attendance rate",
+ "correlationGrade": "Average score",
"emptyTitle": "No attendance data",
- "mswNotice": "Attendance statistics query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled."
+ "emptyDescription": "No attendance stats loaded yet. Please retry later or adjust filters.",
+ "mswNotice": "Attendance statistics query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "total": "{count} records"
+ },
+ "classComparison": {
+ "title": "Class Attendance Comparison",
+ "description": "Cross-class attendance rate comparison and ranking",
+ "updatedAt": "Updated at {time}",
+ "errorNotification": "Failed to load class comparison data",
+ "errorTitle": "Load failed",
+ "errorDescription": "Failed to load class comparison data. Please retry later.",
+ "emptyTitle": "No class comparison data",
+ "emptyDescription": "No class comparison data available. Please retry later.",
+ "seriesRate": "Attendance rate",
+ "colRank": "Rank",
+ "colClass": "Class",
+ "colTotal": "Total students",
+ "colPresent": "Present students",
+ "colRate": "Attendance rate",
+ "colBadge": "Tier",
+ "badgeHigh": "Excellent",
+ "badgeMid": "Average",
+ "badgeLow": "Low"
+ },
+ "gradeCorrelation": {
+ "title": "Attendance-Grade Correlation Analysis",
+ "description": "Correlation analysis between class attendance rate and average score",
+ "errorNotification": "Failed to load attendance-grade correlation data",
+ "errorTitle": "Load failed",
+ "errorDescription": "Failed to load attendance-grade correlation data. Please retry later.",
+ "emptyTitle": "No correlation data",
+ "emptyDescription": "No attendance-grade correlation data available. Please retry later.",
+ "summaryAvgCorrelation": "Avg. correlation",
+ "summaryAvgCorrelationDesc": "Linear correlation between attendance and grades",
+ "summaryStrong": "Strong correlation classes",
+ "summaryMedium": "Medium correlation classes",
+ "summaryWeak": "Weak correlation classes",
+ "scatterTitle": "Attendance-Score scatter plot",
+ "xAxisLabel": "Attendance rate",
+ "yAxisLabel": "Average score",
+ "scatterSeries": "Classes",
+ "legendStrong": "Strong (≥0.7)",
+ "legendMedium": "Medium (0.4-0.7)",
+ "legendWeak": "Weak (<0.4)",
+ "detailsTitle": "Class details",
+ "colClass": "Class",
+ "colAttendanceRate": "Attendance rate",
+ "colAvgScore": "Average score",
+ "colCorrelation": "Correlation",
+ "colTier": "Correlation tier",
+ "badgeStrong": "Strong",
+ "badgeMedium": "Medium",
+ "badgeWeak": "Weak"
},
"error": {
"title": "Attendance management module error",
"unknown": "Unknown error in attendance management module",
"retry": "Retry"
}
+ },
+ "elective": {
+ "list": {
+ "title": "Elective Management",
+ "description": "School-wide elective aggregate view",
+ "createButton": "New Elective",
+ "searchPlaceholder": "Search course name...",
+ "statusFilter": "Filter by status",
+ "allStatuses": "All statuses",
+ "statusDraft": "Draft",
+ "statusOpen": "Open",
+ "statusClosed": "Closed",
+ "statusFull": "Full",
+ "total": "{count} records",
+ "colName": "Name",
+ "colSubject": "Subject",
+ "colGrade": "Grade",
+ "colTeacher": "Teacher",
+ "colCapacity": "Capacity",
+ "colEnrolled": "Enrolled",
+ "colStatus": "Status",
+ "colActions": "Actions",
+ "viewDetail": "View",
+ "edit": "Edit",
+ "emptyTitle": "No electives",
+ "emptyDescription": "Adjust filters or create the first elective",
+ "emptyAction": "New Elective",
+ "mswNotice": "Elective list query contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "statTotalCourses": "Total courses",
+ "statTotalCapacity": "Total capacity",
+ "statTotalEnrolled": "Total enrolled",
+ "statTotalDraft": "Draft count",
+ "statTotalOpen": "Open count",
+ "statsTitle": "Elective overview",
+ "colClassroom": "Classroom",
+ "colSchedule": "Schedule",
+ "colCredit": "Credit",
+ "colSelectionMode": "Selection mode",
+ "openSelection": "Open selection",
+ "closeSelection": "Close selection",
+ "runLottery": "Run lottery",
+ "delete": "Delete",
+ "deleteSuccess": "Elective deleted",
+ "deleteFailed": "Delete failed",
+ "openSuccess": "Selection opened",
+ "closeSuccess": "Selection closed",
+ "lotterySuccess": "Lottery completed",
+ "lotteryFailed": "Lottery failed",
+ "confirmOpenTitle": "Confirm open selection",
+ "confirmOpenDescription": "Students can start selecting after opening. Continue?",
+ "confirmCloseTitle": "Confirm close selection",
+ "confirmCloseDescription": "Students cannot select after closing. Continue?",
+ "confirmLotteryTitle": "Confirm lottery",
+ "confirmLotteryDescription": "Lottery will assign spots to all applicants. This cannot be undone. Continue?",
+ "confirmDeleteTitle": "Delete elective",
+ "confirmDeleteDescription": "Are you sure you want to delete this elective? This action cannot be undone.",
+ "confirmCancel": "Cancel",
+ "confirmSubmit": "Confirm",
+ "confirming": "Processing...",
+ "selectionModeFcfs": "First come first served",
+ "selectionModeLottery": "Lottery"
+ },
+ "create": {
+ "title": "New Elective",
+ "description": "Fill in elective basic information",
+ "submit": "Save",
+ "cancel": "Cancel",
+ "success": "Elective created",
+ "error": "Creation failed",
+ "fieldName": "Name",
+ "fieldDescription": "Description",
+ "fieldSubjectId": "Subject",
+ "fieldGradeId": "Grade",
+ "fieldTeacherId": "Teacher",
+ "fieldCapacity": "Capacity",
+ "fieldStartDate": "Start date",
+ "fieldEndDate": "End date",
+ "fieldStatus": "Status",
+ "errorNameRequired": "Name is required",
+ "errorSubjectRequired": "Please select a subject",
+ "errorCapacityInvalid": "Capacity must be a positive integer",
+ "contractPending": "Elective creation contract pending. Ensure NEXT_PUBLIC_MSW=1 is enabled.",
+ "fieldCredit": "Credit",
+ "fieldClassroom": "Classroom",
+ "fieldSchedule": "Schedule",
+ "fieldSelectionMode": "Selection mode",
+ "fieldSelectionStartAt": "Selection start time",
+ "fieldSelectionEndAt": "Selection end time",
+ "fieldDropDeadline": "Drop deadline",
+ "selectionModeFcfs": "First come first served",
+ "selectionModeLottery": "Lottery"
+ },
+ "detail": {
+ "title": "Elective Detail",
+ "edit": "Edit",
+ "notFound": "Elective not found",
+ "backToList": "Back to list",
+ "sectionBasic": "Basic information",
+ "sectionSchedule": "Schedule",
+ "sectionEnrollment": "Enrollment records",
+ "fieldName": "Name",
+ "fieldDescription": "Description",
+ "fieldSubject": "Subject",
+ "fieldGrade": "Grade",
+ "fieldTeacher": "Teacher",
+ "fieldCapacity": "Capacity",
+ "fieldEnrolled": "Enrolled",
+ "fieldStatus": "Status",
+ "fieldStartDate": "Start date",
+ "fieldEndDate": "End date",
+ "enrollmentTitle": "Enrolled students",
+ "enrollmentStudentName": "Student name",
+ "enrollmentStudentNo": "Student No.",
+ "enrollmentEnrolledAt": "Enrolled at",
+ "emptyEnrollment": "No students enrolled",
+ "fieldClassroom": "Classroom",
+ "fieldSchedule": "Schedule",
+ "fieldCredit": "Credit",
+ "fieldSelectionMode": "Selection mode",
+ "fieldSelectionStart": "Selection start time",
+ "fieldSelectionEnd": "Selection end time",
+ "fieldDropDeadline": "Drop deadline",
+ "fieldPriority": "Priority",
+ "selectionModeFcfs": "First come first served",
+ "selectionModeLottery": "Lottery",
+ "enrollmentPriority": "Priority",
+ "delete": "Delete",
+ "deleteTitle": "Delete elective",
+ "deleteDescription": "Are you sure you want to delete this elective? This action cannot be undone.",
+ "deleteConfirm": "Confirm delete",
+ "deleteCancel": "Cancel",
+ "deleteSuccess": "Elective deleted",
+ "deleteFailed": "Delete failed",
+ "openSelection": "Open selection",
+ "closeSelection": "Close selection",
+ "runLottery": "Run lottery",
+ "openSuccess": "Selection opened",
+ "closeSuccess": "Selection closed",
+ "lotterySuccess": "Lottery completed",
+ "lotteryFailed": "Lottery failed"
+ },
+ "edit": {
+ "title": "Edit Elective",
+ "description": "Modify elective information",
+ "submit": "Save",
+ "cancel": "Cancel",
+ "success": "Elective updated",
+ "error": "Update failed",
+ "fieldName": "Name",
+ "fieldDescription": "Description",
+ "fieldSubjectId": "Subject",
+ "fieldGradeId": "Grade",
+ "fieldTeacherId": "Teacher",
+ "fieldCapacity": "Capacity",
+ "fieldStartDate": "Start date",
+ "fieldEndDate": "End date",
+ "fieldStatus": "Status",
+ "errorNameRequired": "Name is required",
+ "errorSubjectRequired": "Please select a subject",
+ "errorCapacityInvalid": "Capacity must be a positive integer",
+ "notFound": "Elective not found",
+ "backToDetail": "Back to detail",
+ "fieldCredit": "Credit",
+ "fieldClassroom": "Classroom",
+ "fieldSchedule": "Schedule",
+ "fieldSelectionMode": "Selection mode",
+ "fieldSelectionStartAt": "Selection start time",
+ "fieldSelectionEndAt": "Selection end time",
+ "fieldDropDeadline": "Drop deadline",
+ "selectionModeFcfs": "First come first served",
+ "selectionModeLottery": "Lottery"
+ },
+ "error": {
+ "title": "Elective module error",
+ "unknown": "Unknown error in elective module",
+ "retry": "Retry"
+ }
}
}
}
diff --git a/apps/portal-shell/src/messages/zh-CN.json b/apps/portal-shell/src/messages/zh-CN.json
index f414253..9d10ad3 100644
--- a/apps/portal-shell/src/messages/zh-CN.json
+++ b/apps/portal-shell/src/messages/zh-CN.json
@@ -6,10 +6,13 @@
"cancel": "取消",
"edit": "编辑",
"export": "导出",
+ "import": "导入",
"search": "搜索",
"delete": "删除",
"retry": "重试",
- "back": "返回"
+ "back": "返回",
+ "prev": "上一页",
+ "next": "下一页"
},
"label": {
"search": "搜索",
@@ -30,6 +33,7 @@
},
"error": {
"loadFailed": "加载失败:{message}",
+ "operationFailed": "操作失败:{message}",
"pageError": "页面出错了"
},
"nav": {
@@ -45,7 +49,8 @@
},
"empty": {
"title": "暂无数据",
- "description": "调整筛选条件后重试,或新建第一条记录"
+ "description": "调整筛选条件后重试,或新建第一条记录",
+ "searchResult": "没有匹配的权限点"
},
"navLabel": {
"dashboard": "仪表盘",
@@ -120,11 +125,16 @@
"subtitle": "{email} · 角色:{roles} · 数据范围:{dataScope}",
"subtitleNoRoles": "无",
"error": {
- "loadFailed": "仪表盘加载失败:{message}"
+ "loadFailed": "仪表盘加载失败:{message}",
+ "loadFailedGeneric": "加载失败",
+ "loadFailedDesc": "数据暂时无法加载,请重试",
+ "retry": "重试"
},
"empty": {
"title": "暂无数据",
- "description": "仪表盘数据尚未就绪"
+ "description": "仪表盘数据尚未就绪",
+ "noNotifications": "暂无通知",
+ "noNotificationsDesc": "新通知将显示在这里"
},
"stats": {
"classes": "班级总数",
@@ -145,6 +155,11 @@
"title": "教师仪表盘",
"description": "今日教学概览",
"loadFailed": "仪表盘数据加载失败,请稍后重试。",
+ "greeting": {
+ "morning": "早上好",
+ "afternoon": "下午好",
+ "evening": "晚上好"
+ },
"stats": {
"totalClasses": "班级总数",
"totalStudents": "学生总数",
@@ -164,6 +179,74 @@
"currentValue": "当前值",
"threshold": "阈值"
}
+ },
+ "teacherCards": {
+ "quickActions": {
+ "createAssignment": "创建作业",
+ "grade": "批改作业",
+ "myClasses": "我的班级"
+ },
+ "todo": {
+ "title": "待办",
+ "empty": "今日无待办"
+ },
+ "classes": {
+ "title": "我的班级",
+ "viewAll": "查看全部",
+ "emptyTitle": "暂无班级",
+ "emptyDescription": "请联系管理员分配班级",
+ "createClass": "管理班级",
+ "homeroom": "班主任",
+ "room": "教室"
+ },
+ "homework": {
+ "title": "作业",
+ "createNewAssignment": "新建作业",
+ "emptyTitle": "暂无作业",
+ "emptyDescription": "点击 + 创建首个作业",
+ "create": "创建作业",
+ "noDueDate": "无截止日期",
+ "viewAllAssignments": "查看全部作业"
+ },
+ "schedule": {
+ "title": "今日课表",
+ "emptyTitle": "今日无课",
+ "emptyDescription": "享受空闲的一天",
+ "viewSchedule": "查看课表",
+ "live": "进行中",
+ "scrollForMore": "向下滚动查看更多",
+ "noMoreClasses": "今日无更多课程"
+ },
+ "gradeTrends": {
+ "title": "班级成绩趋势",
+ "description": "近 {count} 次作业平均得分率",
+ "emptyTitle": "暂无成绩数据",
+ "emptyDescription": "完成批改后将展示趋势",
+ "averageScorePercent": "平均得分率",
+ "submittedCount": "提交 {submitted}/{total}"
+ },
+ "recentSubmissions": {
+ "title": "最近提交",
+ "viewAll": "查看全部",
+ "emptyTitle": "暂无新提交",
+ "emptyDescription": "全部已批改完成",
+ "colStudent": "学生",
+ "colAssignment": "作业",
+ "colSubmitted": "提交时间",
+ "colAction": "操作",
+ "late": "迟到",
+ "grade": "批改"
+ }
+ },
+ "timeRange": {
+ "label": "时间范围",
+ "today": "今天",
+ "week": "本周",
+ "month": "本月"
+ },
+ "sections": {
+ "notifications": "通知",
+ "viewAllNotifications": "查看全部通知"
}
},
"classes": {
@@ -431,6 +514,102 @@
"title": "考试页面出错了",
"unknown": "未知错误",
"retry": "重试"
+ },
+ "actions": {
+ "view": "查看",
+ "build": "组卷",
+ "analytics": "分析",
+ "delete": "删除",
+ "confirmDelete": "确认删除",
+ "cancel": "取消",
+ "deleteSuccess": "删除成功",
+ "deleteFailed": "删除失败:{message}",
+ "readOnly": "只读"
+ },
+ "table": {
+ "selectAll": "全选",
+ "selectRow": "选择此行",
+ "selectedCount": "已选 {selected}/{total} 行",
+ "noResults": "暂无数据"
+ },
+ "viewer": {
+ "section": "大题",
+ "group": "小题",
+ "unknown": "未知题型",
+ "scoreLabel": "分值:{score}",
+ "noQuestions": "暂无题目"
+ },
+ "paper": {
+ "section": "大题",
+ "group": "小题",
+ "scoreWithUnit": "({score}分)",
+ "subject": "科目",
+ "grade": "年级",
+ "time": "时长",
+ "minutes": "分钟",
+ "total": "总分",
+ "pts": "分",
+ "class": "班级",
+ "name": "姓名",
+ "no": "考号",
+ "empty": "暂无题目"
+ },
+ "editor": {
+ "contentPlaceholder": "开始输入试卷内容...",
+ "loading": "编辑器加载中..."
+ },
+ "richEditor": {
+ "bold": "加粗",
+ "italic": "斜体",
+ "strike": "删除线",
+ "dotted": "加点字",
+ "bulletList": "无序列表",
+ "orderedList": "有序列表",
+ "quote": "引用",
+ "undo": "撤销",
+ "redo": "重做",
+ "markBlank": "标记填空"
+ },
+ "selectionToolbar": {
+ "ariaLabel": "选区快捷工具栏",
+ "blankShortAnswer": "简答题",
+ "composite": "复合题",
+ "defaultGroupTitle": "大题",
+ "defaultSectionTitle": "分卷",
+ "groupLabel": "分组",
+ "image": "图片",
+ "sectionLabel": "分卷",
+ "singleChoice": "单选题"
+ },
+ "richForm": {
+ "titlePlaceholder": "请输入试卷标题",
+ "classIdPlaceholder": "班级 ID",
+ "subjectIdPlaceholder": "科目 ID",
+ "difficulty": "难度",
+ "difficultyLevel1": "1星",
+ "difficultyLevel2": "2星",
+ "difficultyLevel3": "3星",
+ "difficultyLevel4": "4星",
+ "difficultyLevel5": "5星",
+ "totalScore": "满分",
+ "durationMin": "时长(分钟)",
+ "examDate": "考试日期",
+ "back": "返回",
+ "save": "保存",
+ "saving": "保存中...",
+ "emptyContent": "编辑器内容为空",
+ "titleRequired": "请填写试卷标题",
+ "missingExamId": "缺少考试 ID",
+ "classSubjectRequired": "请填写班级和科目",
+ "examDateRequired": "请选择考试日期",
+ "saveSuccess": "保存成功",
+ "saveFailed": "保存失败:{message}",
+ "createSuccess": "创建成功",
+ "createFailed": "创建失败:{message}",
+ "editorArea": "编辑区",
+ "previewArea": "预览区",
+ "previewSummary": "共 {count} 题 · 总分 {total}",
+ "emptyPreview": "在左侧编辑器中输入内容后,这里将显示预览"
}
},
"homework": {
@@ -638,10 +817,184 @@
"suggestedScore": "建议分数",
"confidence": "置信度"
},
+ "scanUploader": {
+ "scanTitle": "上传扫描图",
+ "dragDropHint": "拖拽图片到此处上传",
+ "fileTypesHint": "支持 JPG / PNG / WebP / PDF",
+ "uploading": "上传中...",
+ "uploadFailed": "上传失败",
+ "uploadSuccess": "已上传 {count} 张扫描图",
+ "selectImageFiles": "请选择图片或 PDF 文件",
+ "pageLabel": "第 {page} 页",
+ "moveUp": "上移",
+ "moveDown": "下移",
+ "deleteScan": "删除扫描图",
+ "noScans": "暂无扫描图"
+ },
+ "scanViewer": {
+ "noImages": "暂无扫描图",
+ "noImagesHint": "请先上传学生答题扫描图",
+ "zoomIn": "放大",
+ "zoomOut": "缩小",
+ "rotate": "旋转",
+ "fullscreen": "全屏",
+ "pageIndicator": "第 {current}/{total} 页",
+ "answerImageAlt": "第 {page} 页答题图",
+ "thumbnailAlt": "第 {page} 页缩略图"
+ },
+ "result": {
+ "scoreRate": "得分率",
+ "fullyGraded": "所有题目已批改完成",
+ "partiallyGraded": "部分题目仍待批改",
+ "correctCount": "答对",
+ "incorrectCount": "答错",
+ "partialCount": "部分正确",
+ "pendingCount": "待批改",
+ "wrongAnswersTitle": "错题预览",
+ "wrongAnswersDesc": "以下是答错或部分正确的题目",
+ "backToList": "返回列表",
+ "viewErrorBook": "查看错题本",
+ "yourAnswer": "你的答案",
+ "correctAnswer": "正确答案",
+ "teacherFeedback": "教师反馈",
+ "correctAnswerTrue": "正确",
+ "correctAnswerFalse": "错误"
+ },
"error": {
"title": "作业页面出错了",
"unknown": "未知错误",
"retry": "重试"
+ },
+ "review": {
+ "gradedReport": "批改报告",
+ "submissionDetails": "提交详情",
+ "questionsUnit": "题",
+ "backToList": "返回列表",
+ "assignmentInfo": "作业信息",
+ "status": "状态",
+ "description": "说明",
+ "noDescription": "无说明",
+ "totalScore": "总分",
+ "questionBreakdown": "题目分布",
+ "responseSummary": "作答概览"
+ },
+ "grade": {
+ "correct": "正确",
+ "partial": "部分正确",
+ "incorrect": "错误"
+ },
+ "take": {
+ "back": "返回",
+ "questions": "题",
+ "notStarted": "未开始",
+ "timedExam": "限时考试:{minutes} 分钟",
+ "starting": "开始中...",
+ "startAssignment": "开始作答",
+ "timeRemaining": "剩余时间",
+ "submitting": "提交中...",
+ "submitAssignment": "提交作业",
+ "saveFailed": "保存失败",
+ "startSuccess": "已开始作答",
+ "startFailed": "开始失败",
+ "saved": "已保存",
+ "submitSuccess": "已提交",
+ "submitFailed": "提交失败",
+ "timeUpAutoSubmit": "时间到,已自动提交",
+ "readyToStart": "准备开始",
+ "readyDescription": "点击下方按钮开始作答",
+ "startNow": "立即开始",
+ "confirmSubmit": "确认提交",
+ "unansweredWarning": "还有 {count} 题未作答",
+ "confirmSubmitDescription": "确认要提交作业吗?提交后将无法修改。",
+ "cancel": "取消",
+ "confirmSubmitAction": "确认提交",
+ "assignmentInfo": "作业信息",
+ "status": "状态",
+ "dueDate": "截止时间",
+ "overdue": "已逾期",
+ "lessThanOneHour": "不足 1 小时",
+ "hoursLeft": "剩余 {hours} 小时",
+ "attempts": "尝试次数",
+ "attemptsUsed": "已用 {used}/{max} 次",
+ "attemptsRemaining": "剩余 {remaining} 次",
+ "description": "说明",
+ "noDescription": "无说明",
+ "progress": "作答进度",
+ "jumpToQuestion": "跳转到第 {index} 题",
+ "answered": "已答",
+ "unanswered": "未答",
+ "submitAll": "提交全部",
+ "makeSureAnswered": "请确认所有题目已作答",
+ "saveAnswer": "保存答案",
+ "scanTitle": "扫描上传",
+ "scanDescription": "上传手写作业扫描件"
+ }
+ },
+ "examHomework": {
+ "homework": {
+ "analytics": {
+ "examContent": "考试内容",
+ "questionPreview": "题目预览",
+ "errorAnalysis": "错误分析",
+ "errorRateOverview": "错误率概览",
+ "errorRateAriaLabel": "错误率 {rate}%",
+ "question": "题目",
+ "errors": "错误数",
+ "errorRateLabel": "错误率",
+ "wrongAnswersWithCount": "错答 ({count})",
+ "wrongAnswers": "错答",
+ "noWrongAnswers": "暂无错答记录。",
+ "studentAnswer": "学生答案",
+ "studentCount": "{count} 名学生",
+ "notAnswered": "未作答",
+ "selectQuestionHint": "请从左侧选择题目",
+ "selectQuestionHintDesc": "查看错误分析",
+ "noGradedSubmissions": "暂无已批改的提交。"
+ },
+ "take": {
+ "true": "正确",
+ "false": "错误"
+ },
+ "excellent": {
+ "title": "优秀作业展示",
+ "description": "本作业中得分率达到 {minPercentage}% 及以上的优秀样例。",
+ "empty": "暂无符合条件的优秀作业。",
+ "emptyHint": "完成批改后将自动汇总展示。",
+ "studentAnon": "同学",
+ "rank": "第 {rank} 名",
+ "lateTag": "迟交",
+ "submittedAt": "提交于 {date}",
+ "scoreValue": "{score} / {max}",
+ "percentage": "{value}%"
+ },
+ "form": {
+ "createTitle": "创建作业",
+ "quickMode": "快速作业",
+ "quickModeDescription": "直接输入标题和描述,无需建题",
+ "examMode": "考试派生作业",
+ "examModeDescription": "从已有考试派生作业",
+ "class": "班级",
+ "selectClass": "选择班级",
+ "sourceExam": "来源考试",
+ "selectExam": "选择考试",
+ "assignmentTitle": "作业标题",
+ "titlePlaceholderQuick": "例如:背诵第三课课文",
+ "titlePlaceholderExam": "默认使用考试标题",
+ "description": "描述(可选)",
+ "descriptionPlaceholderQuick": "输入作业要求、题目内容或说明...",
+ "availableAt": "开放时间(可选)",
+ "dueAt": "截止时间(可选)",
+ "allowLate": "允许迟交",
+ "lateDueAt": "迟交截止时间(可选)",
+ "maxAttempts": "最大尝试次数",
+ "submit": "创建作业",
+ "submitting": "创建中...",
+ "creating": "正在创建作业...",
+ "selectExamRequired": "请选择考试",
+ "titleRequired": "请输入标题",
+ "selectClassRequired": "请选择班级",
+ "createFailed": "创建失败"
+ }
}
},
"grades": {
@@ -733,7 +1086,18 @@
"colMinScore": "最低分",
"colPassRate": "及格率",
"colPassCount": "及格人数",
- "colFailCount": "不及格人数"
+ "colFailCount": "不及格人数",
+ "noData": "暂无数据",
+ "average": "平均分",
+ "median": "中位数",
+ "max": "最高分",
+ "min": "最低分",
+ "stdDev": "标准差",
+ "stdDevHint": "反映成绩离散程度",
+ "passRateHint": "及格率(≥60分)",
+ "excellentRate": "优秀率",
+ "excellentRateHint": "优秀率(≥85分)",
+ "count": "记录数"
},
"reportCard": {
"title": "学生成绩单",
@@ -763,6 +1127,49 @@
"title": "成绩页面出错了",
"unknown": "未知错误",
"retry": "重试"
+ },
+ "classReport": {
+ "studentCountInfo": "应参加 {studentCount} 人 · 已录入 {recordCount} 条",
+ "noDataTitle": "暂无班级成绩数据",
+ "noDataDescription": "请先录入成绩后再查看班级报告",
+ "classRanking": "班级排名",
+ "caption": "班级成绩排名表",
+ "rankColumn": "名次",
+ "recordsColumn": "记录数"
+ },
+ "growthArchive": {
+ "title": "学生成长档案",
+ "description": "覆盖 {years} 个学年 · {records} 条记录 · {subjects} 个学科",
+ "descriptionEmpty": "暂无成长档案数据",
+ "emptyTitle": "暂无成长数据",
+ "emptyDescription": "至少需要两个学期的成绩数据才能展示成长趋势",
+ "deltaUp": "较上期提升 {delta} 分",
+ "deltaDown": "较上期下降 {delta} 分",
+ "deltaStable": "与上期持平",
+ "overallAverage": "总体均分 {score}",
+ "averageScore": "平均分",
+ "ariaLabelNonEmpty": "成长趋势图,共 {count} 个数据点",
+ "ariaLabelEmpty": "成长趋势图为空",
+ "statsAverage": "平均分 {score}",
+ "statsPassRate": "及格率 {rate}%",
+ "statsRecords": "{count} 条记录"
+ },
+ "knowledgePointMastery": {
+ "title": "知识点掌握度",
+ "description": "共 {count} 个知识点 · 平均掌握度 {avg}%",
+ "descriptionEmpty": "暂无知识点掌握度数据",
+ "emptyTitle": "暂无掌握度数据",
+ "emptyDescription": "需要先有练习或考试数据才能统计掌握度",
+ "viewDetail": "查看详情",
+ "ariaLabel": "知识点掌握度柱状图,共 {count} 个知识点",
+ "averageMastery": "平均掌握度",
+ "tooltipMastery": "掌握度:{value}%",
+ "tooltipStudents": "已掌握 {mastered} / {total} 人",
+ "weakPointsAriaLabel": "薄弱知识点列表",
+ "weakPointsTitle": "薄弱知识点"
+ },
+ "summary": {
+ "averageScore": "平均分"
}
},
"analytics": {
@@ -1001,7 +1408,261 @@
"error": {
"title": "AI 模块页面出错了",
"unknown": "AI 模块发生未知错误",
- "retry": "重试"
+ "retry": "重试",
+ "invalidInput": "输入数据无效",
+ "chatFailed": "AI 请求失败",
+ "suggestionFailed": "AI 建议失败",
+ "gradingFailed": "AI 批改失败",
+ "contentFailed": "内容生成失败",
+ "variantFailed": "题目变体生成失败",
+ "analysisFailed": "薄弱点分析失败",
+ "statsFailed": "AI 使用统计查询失败",
+ "boundaryTitle": "AI 功能错误",
+ "boundaryDescription": "处理 AI 请求时发生错误,请重试。",
+ "unauthorized": "您没有使用 AI 功能的权限",
+ "providerNotConfigured": "AI 服务商未配置,请联系管理员。"
+ },
+ "chat": {
+ "title": "AI 助手",
+ "placeholder": "请输入您的问题...",
+ "inputLabel": "消息输入",
+ "send": "发送",
+ "thinking": "AI 正在思考...",
+ "streaming": "AI 正在输入...",
+ "stopGeneration": "停止生成",
+ "maxReached": "已达到最大消息数",
+ "clear": "清空对话",
+ "clearConfirm": "确认清空所有消息?",
+ "copy": "复制",
+ "copied": "已复制!",
+ "suggestedPrompts": {
+ "title": "试试问我...",
+ "teacher": [
+ "帮我批改这道题",
+ "生成一个课堂活动",
+ "创建一道测验题"
+ ],
+ "student": [
+ "解释这个概念",
+ "给我一道练习题",
+ "帮我复习"
+ ],
+ "parent": [
+ "我孩子学得怎么样?",
+ "在家应该关注什么?"
+ ],
+ "admin": [
+ "显示 AI 使用统计",
+ "哪些老师最常使用 AI?"
+ ],
+ "context": {
+ "teacherGrading": [
+ "这类题目常见错误有哪些?",
+ "如何给出建设性反馈?"
+ ],
+ "teacherLesson": [
+ "为这节课建议一个导入",
+ "有哪些分层教学策略?"
+ ],
+ "teacherExam": [
+ "生成一道相关题目",
+ "分析难度分布"
+ ],
+ "studentHomework": [
+ "给我提示,不要答案",
+ "帮我理解这个概念"
+ ]
+ }
+ },
+ "contextMessage": {
+ "teacherGrading": "当前页面:作业批改视图",
+ "teacherLesson": "当前页面:备课编辑器",
+ "teacherExam": "当前页面:试卷组卷",
+ "studentErrorBook": "当前页面:错题本(学生视图)",
+ "studentHomework": "当前页面:学生作业视图",
+ "parent": "当前页面:家长面板",
+ "admin": "当前页面:管理员面板"
+ }
+ },
+ "provider": {
+ "label": "AI 服务商",
+ "placeholder": "选择服务商",
+ "loading": "加载服务商中...",
+ "default": "默认",
+ "description": "选择本次操作使用的 AI 配置。",
+ "manage": "管理",
+ "manageTitle": "AI 服务商设置",
+ "manageDescription": "新建服务商或更新已有配置。"
+ },
+ "suggestion": {
+ "title": "AI 建议",
+ "generate": "生成建议",
+ "regenerate": "重新生成",
+ "loading": "AI 思考中...",
+ "empty": "暂无建议",
+ "error": "生成建议失败",
+ "loaded": "建议已加载",
+ "selected": "已选择建议",
+ "select": "选择",
+ "difficulty": "难度",
+ "practiceNow": "立即练习",
+ "addAll": "全部添加"
+ },
+ "grading": {
+ "title": "AI 批改建议",
+ "description": "AI 驱动的主观题评分与反馈",
+ "suggestedScore": "建议分数",
+ "confidence": "置信度",
+ "feedback": "反馈",
+ "reasoning": "评分依据",
+ "applyScore": "应用分数",
+ "applyFeedback": "应用反馈",
+ "loading": "AI 批改中...",
+ "error": "AI 批改失败",
+ "notAvailable": "此题型不支持 AI 批改",
+ "batchTitle": "批量 AI 批改",
+ "batchDescription": "一次性为所有主观题生成 AI 建议",
+ "batchGenerate": "生成全部建议",
+ "batchProgress": "处理中 {done}/{total}",
+ "currentScore": "当前分数",
+ "scoreDifference": "差值"
+ },
+ "errorBook": {
+ "similarQuestions": "相似题目",
+ "weaknessAnalysis": "薄弱点分析",
+ "studyPlan": "学习计划",
+ "recommendedResources": "推荐资源",
+ "weakAreas": "薄弱领域",
+ "severity": {
+ "high": "高",
+ "medium": "中",
+ "low": "低"
+ }
+ },
+ "lessonPrep": {
+ "generateContent": "生成内容",
+ "description": "AI 驱动的教学内容生成",
+ "generateActivity": "建议活动",
+ "generateAssessment": "生成评估",
+ "generateQuestion": "生成讨论题",
+ "loading": "生成中...",
+ "error": "内容生成失败",
+ "additionalContext": "附加上下文",
+ "additionalContextPlaceholder": "添加特定要求或上下文信息...",
+ "insertContent": "插入内容",
+ "editBeforeInsert": "插入前编辑",
+ "history": "生成历史",
+ "clearHistory": "清空历史"
+ },
+ "exam": {
+ "generate": "生成",
+ "generating": "生成中...",
+ "preview": "预览",
+ "queue": "加入队列",
+ "queueRunning": "运行中",
+ "queueQueued": "排队中",
+ "backgroundTasks": "后台任务",
+ "taskStatus": {
+ "queued": "排队中",
+ "running": "生成中",
+ "success": "已完成",
+ "failed": "失败"
+ },
+ "openPreview": "打开预览",
+ "sourceText": "试卷原文",
+ "sourceTextPlaceholder": "粘贴试卷文本以解析为题目",
+ "sourceTextDesc": "AI 将从文本中提取题目和结构。",
+ "generationTitle": "AI 生成",
+ "generationDesc": "粘贴试卷文本并生成结构化预览。",
+ "variantType": {
+ "label": "变体类型",
+ "same_knowledge_point": "同知识点,不同情境",
+ "different_difficulty": "不同难度",
+ "different_format": "不同题型"
+ },
+ "targetDifficulty": "目标难度",
+ "addVariant": "添加变体"
+ },
+ "parent": {
+ "summary": "AI 学情摘要",
+ "summaryDescription": "AI 生成的子女学习进度概览",
+ "generateSummary": "生成摘要",
+ "weaknessHint": "需关注领域",
+ "suggestion": "家庭辅导建议",
+ "loading": "生成摘要中...",
+ "error": "生成摘要失败"
+ },
+ "admin": {
+ "usageDashboard": "AI 使用仪表盘",
+ "dashboardDescription": "监控全校 AI 使用情况",
+ "totalCalls": "AI 调用总数",
+ "activeUsers": "活跃用户",
+ "costEstimate": "预估成本",
+ "topUsers": "高频用户",
+ "byCapability": "按能力分类",
+ "byRole": "按角色分类",
+ "recentActivity": "最近活动",
+ "noData": "暂无 AI 使用数据",
+ "callsToday": "今日调用",
+ "callsThisWeek": "本周调用",
+ "errorRate": "错误率",
+ "avgDuration": "平均耗时",
+ "settings": {
+ "title": "AI 配置",
+ "description": "统一管理 AI 服务商、API 密钥与使用统计。",
+ "descriptionAdmin": "统一管理 AI 服务商、API 密钥与使用统计(管理员视图)。",
+ "descriptionUser": "管理你的 AI 服务商与 API 密钥。"
+ }
+ },
+ "studyPath": {
+ "title": "你的学习路径",
+ "description": "AI 个性化学习建议",
+ "nextSteps": "推荐下一步",
+ "mastered": "已掌握",
+ "inProgress": "学习中",
+ "needsWork": "需要加强",
+ "generate": "生成学习路径",
+ "loading": "生成学习路径中...",
+ "error": "生成学习路径失败",
+ "startLearning": "开始学习"
+ },
+ "widget": {
+ "title": "AI 助手",
+ "open": "打开 AI 助手",
+ "close": "关闭",
+ "contextAware": "上下文感知",
+ "dragHint": "拖动移动 · 长按边缘隐藏",
+ "hidden": "已隐藏",
+ "show": "显示",
+ "hide": "隐藏",
+ "resetPosition": "重置位置",
+ "welcome": "你好,我是 AI 助手",
+ "welcomeDesc": "有什么可以帮你的吗?",
+ "newChat": "新对话",
+ "history": "历史记录",
+ "online": "在线",
+ "tokens": "{count} 字"
+ },
+ "safety": {
+ "blocked": "您的消息被安全过滤器拦截,请保持教育性对话。",
+ "dailyLimit": "今日 AI 使用次数已达上限,请明天再试。",
+ "studentMode": "AI 处于学生模式,将引导你自主找到答案。",
+ "contentFiltered": "AI 回复中的不当内容已被过滤。"
+ },
+ "capability": {
+ "chat": "AI 对话",
+ "examGenerate": "AI 出题",
+ "gradingAssist": "AI 辅助批改",
+ "lessonContent": "AI 备课内容",
+ "questionVariant": "AI 题目变体",
+ "similarQuestion": "AI 相似题",
+ "weaknessAnalysis": "AI 薄弱点分析",
+ "childSummary": "AI 子女摘要",
+ "studyPath": "AI 学习路径",
+ "explainError": "AI 错题解释"
+ },
+ "chart": {
+ "parseError": "图表数据格式错误,无法渲染"
}
},
"attendance": {
@@ -1030,7 +1691,12 @@
"colStatus": "状态",
"colRemark": "备注",
"colRecordedBy": "记录人",
- "colUpdatedAt": "更新时间"
+ "colUpdatedAt": "更新时间",
+ "columns": {
+ "student": "学生",
+ "reason": "原因",
+ "status": "状态"
+ }
},
"sheet": {
"title": "点名表",
@@ -1049,7 +1715,14 @@
"errorNoEntries": "无考勤条目可保存",
"success": "点名表保存成功",
"error": "保存失败",
- "loadFailed": "加载失败:{message}"
+ "loadFailed": "加载失败:{message}",
+ "selectClass": "请选择班级",
+ "reasonPlaceholder": "请输入原因",
+ "saved": "点名表已保存",
+ "saving": "保存中...",
+ "noStudents": "该班级暂无学生",
+ "confirmClassSwitch": "切换班级将丢失未保存的修改,确认切换?",
+ "confirmClassSwitchAction": "确认切换"
},
"report": {
"title": "考勤报表",
@@ -1070,7 +1743,31 @@
"colAbsent": "缺勤",
"colLate": "迟到",
"colLeave": "请假",
- "colAttendanceRate": "出勤率"
+ "colAttendanceRate": "出勤率",
+ "printing": "打印中...",
+ "relationship": "与学生关系",
+ "period": "统计周期",
+ "parentSignature": "家长签字单",
+ "type": "报表类型",
+ "noData": "暂无报表数据",
+ "controls": "报表控制",
+ "noDataDescription": "请调整筛选条件后重试",
+ "parentComment": "家长意见",
+ "studentDetails": "学生明细",
+ "footer": "本报表由系统自动生成,仅供参考。",
+ "generatedAt": "生成日期",
+ "signature": "签字",
+ "parentName": "家长姓名",
+ "summary": "汇总统计",
+ "startDate": "开始日期",
+ "endDate": "结束日期",
+ "print": "打印",
+ "parentSignatureNotice": "请家长仔细核对以上考勤记录,并签字确认。",
+ "signDate": "签字日期",
+ "types": {
+ "weekly": "周报",
+ "monthly": "月报"
+ }
},
"stats": {
"title": "考勤统计",
@@ -1096,6 +1793,41 @@
"title": "考勤模块出错了",
"unknown": "考勤模块发生未知错误",
"retry": "重试"
+ },
+ "filters": {
+ "class": "班级",
+ "date": "日期"
+ },
+ "description": {
+ "teacherRecords": "教师记录学生每日出勤"
+ },
+ "actions": {
+ "cancel": "取消",
+ "save": "保存",
+ "markAllPresent": "全部标记为出勤"
+ },
+ "errors": {
+ "invalidForm": "表单填写有误",
+ "unexpected": "操作失败,请稍后重试"
+ },
+ "period": {
+ "label": "节次",
+ "full_day": "全天",
+ "morning_reading": "早读",
+ "morning": "上午",
+ "afternoon": "下午",
+ "evening": "晚上"
+ },
+ "rules": {
+ "title": "考勤规则",
+ "lateThreshold": "迟到阈值(分钟)",
+ "earlyLeaveThreshold": "早退阈值(分钟)",
+ "enableAutoMark": "启用自动标记",
+ "attendanceRateThreshold": "出勤率阈值(%)",
+ "attendanceRateThresholdHint": "低于此阈值将触发预警",
+ "consecutiveAbsenceThreshold": "连续缺勤阈值(次)",
+ "consecutiveAbsenceThresholdHint": "超过此次数将触发预警",
+ "saved": "规则已保存"
}
},
"questions": {
@@ -1137,7 +1869,121 @@
"error": {
"title": "题库页面出错了",
"unknown": "未知错误",
- "retry": "重试"
+ "retry": "重试",
+ "createdSuccess": "题目创建成功",
+ "updatedSuccess": "题目更新成功",
+ "unexpected": "发生未知错误"
+ },
+ "content": {
+ "empty": "暂无题干内容",
+ "answer": "答案",
+ "explanation": "解析"
+ },
+ "actions": {
+ "menuLabel": "操作菜单",
+ "viewDetails": "查看详情",
+ "delete": "删除",
+ "deleteConfirmTitle": "确认删除",
+ "deleteConfirmDesc": "确定要删除此题目吗?此操作不可撤销。",
+ "deleteConfirmCancel": "取消",
+ "deleting": "删除中...",
+ "deleteSuccess": "删除成功",
+ "deleteFailed": "删除失败",
+ "copyId": "复制 ID",
+ "copyIdSuccess": "已复制到剪贴板",
+ "copyIdFailed": "复制失败",
+ "close": "关闭",
+ "detailsTitle": "题目详情",
+ "detailsId": "题目 ID",
+ "detailsType": "题型",
+ "detailsDifficulty": "难度",
+ "detailsContent": "题干"
+ },
+ "batch": {
+ "selected": "已选 {count} 项",
+ "delete": "批量删除",
+ "deleteConfirmTitle": "确认批量删除",
+ "deleteConfirmDesc": "确定要删除选中的 {count} 道题目吗?此操作不可撤销。",
+ "deleting": "删除中...",
+ "deleteSuccess": "批量删除成功",
+ "deleteFailed": "批量删除失败",
+ "cancel": "取消",
+ "clear": "清除选择",
+ "deleteConfirmAction": "确认删除"
+ },
+ "cascade": {
+ "textbook": "教材",
+ "textbookAll": "全部教材",
+ "chapter": "章节",
+ "chapterAll": "全部章节",
+ "knowledgePoint": "知识点",
+ "knowledgePointAll": "全部知识点",
+ "loading": "加载中..."
+ },
+ "importExport": {
+ "import": "导入",
+ "export": "导出",
+ "importing": "导入中...",
+ "exporting": "导出中...",
+ "importSuccess": "导入成功",
+ "importFailed": "导入失败",
+ "exportSuccess": "导出成功",
+ "exportFailed": "导出失败",
+ "confirmTitle": "确认导入",
+ "confirmDesc": "将覆盖现有题目,确定继续?",
+ "cancel": "取消",
+ "invalidFile": "无效的文件格式",
+ "readFailed": "文件读取失败",
+ "confirmImport": "确认导入"
+ },
+ "table": {
+ "type": "题型",
+ "content": "题干",
+ "difficulty": "难度",
+ "knowledgePoints": "知识点",
+ "created": "创建时间",
+ "noResults": "暂无结果"
+ },
+ "type": {
+ "single_choice": "单选题",
+ "multiple_choice": "多选题",
+ "judgment": "判断题",
+ "text": "简答题",
+ "composite": "复合题"
+ },
+ "difficulty": {
+ "1": "简单",
+ "2": "中等",
+ "3": "困难",
+ "4": "较难",
+ "5": "很难"
+ },
+ "dialog": {
+ "editTitle": "编辑题目",
+ "createTitle": "新建题目",
+ "editDesc": "编辑以下题目信息。",
+ "createDesc": "填写以下题目信息。",
+ "questionType": "题型",
+ "difficulty": "难度",
+ "questionContent": "题干",
+ "contentPlaceholder": "输入题干内容...",
+ "answer": "答案",
+ "explanation": "解析",
+ "cancel": "取消",
+ "updating": "更新中...",
+ "creating": "创建中...",
+ "update": "更新",
+ "create": "创建",
+ "addOption": "添加选项",
+ "markCorrect": "标记为正确答案",
+ "options": "选项",
+ "loading": "加载中...",
+ "knowledgePoints": "知识点",
+ "knowledgePointsOptional": "知识点(可选)",
+ "knowledgePointsSelected": "已选 {count} 个知识点",
+ "noKnowledgePoints": "暂无知识点",
+ "searchKnowledgePoints": "搜索知识点...",
+ "optionPlaceholder": "选项 {index}"
}
},
"textbooks": {
@@ -1185,12 +2031,151 @@
"colChapterStatus": "状态",
"noChapters": "暂无章节",
"noChaptersAction": "返回教材列表",
- "chaptersMswNotice": "章节列表查询契约待补齐,当前通过 MSW 兜底。"
+ "chaptersMswNotice": "章节列表查询契约待补齐,当前通过 MSW 兜底。",
+ "sectionKnowledgeGraph": "知识图谱"
},
"error": {
"title": "教材页面出错了",
"unknown": "未知错误",
"retry": "重试"
+ },
+ "graph": {
+ "viewModeStructure": "结构视图",
+ "viewModeStudentMastery": "学生掌握度",
+ "viewModeClassMastery": "班级掌握度",
+ "searchPlaceholder": "搜索知识点名称...",
+ "resetView": "重置视图",
+ "refreshing": "刷新中...",
+ "refresh": "刷新",
+ "edit": "编辑",
+ "delete": "删除",
+ "createQuestion": "创建题目",
+ "questionCount": "{count} 道题目",
+ "layout": {
+ "hierarchical": "分层布局",
+ "force": "力导向布局"
+ },
+ "error": {
+ "loadFailed": "知识图谱加载失败"
+ },
+ "node": {
+ "questions": "题目",
+ "mastery": "掌握度",
+ "prerequisite": "前置知识点",
+ "successor": "后置知识点"
+ },
+ "detail": {
+ "title": "知识点详情",
+ "close": "关闭",
+ "description": "描述",
+ "noDescription": "暂无描述",
+ "correctRate": "正确率",
+ "masteryNotAssessed": "未测评",
+ "totalQuestions": "总题数",
+ "viewAllQuestions": "查看全部题目",
+ "addPrerequisite": "添加前置",
+ "removePrerequisite": "移除前置依赖",
+ "noPrerequisites": "暂无前置知识点",
+ "noSuccessors": "暂无后置知识点",
+ "prerequisiteRemoveFailed": "移除前置依赖失败"
+ }
+ },
+ "knowledge": {
+ "title": "知识点",
+ "create": "新建知识点",
+ "empty": "暂无知识点",
+ "difficulty": "难度 {level}"
+ },
+ "card": {
+ "gradeNA": "未设置",
+ "version": "v{version}",
+ "moreOptions": "更多操作",
+ "editContent": "编辑内容",
+ "delete": "删除"
+ },
+ "reader": {
+ "contents": "目录",
+ "noChapters": "暂无章节",
+ "selectChapter": "请选择章节阅读",
+ "chapters": "章节",
+ "deleteFailed": "删除教材失败",
+ "emptyKnowledge": "暂无知识点",
+ "emptyKnowledgeDesc": "该教材尚未录入知识点,请先在章节下创建知识点。",
+ "loadingKnowledge": "正在加载知识点..."
+ },
+ "action": {
+ "updateNotSupported": "更新功能尚未支持(契约待补齐)。",
+ "kpCreateNotSupported": "创建知识点尚未支持(契约待补齐)。",
+ "deleteNotSupported": "删除功能尚未支持(契约待补齐)。",
+ "updateFailedGeneric": "更新失败",
+ "deleteFailed": "删除失败",
+ "errorOccurred": "发生错误",
+ "prerequisiteAddNotSupported": "添加前置依赖暂未支持(对话框待迁移)。",
+ "prerequisiteDeleteNotSupported": "删除前置依赖暂未支持(mutation 待补齐)。"
+ },
+ "dialog": {
+ "chapter": {
+ "createTitle": "新建章节",
+ "titlePlaceholder": "章节标题...",
+ "cancel": "取消",
+ "create": "创建",
+ "cannotDeleteWithSubchapters": "无法删除含有子章节的章节,请先删除子章节。"
+ },
+ "knowledge": {
+ "editTitle": "编辑知识点",
+ "nameLabel": "名称",
+ "cancel": "取消",
+ "saving": "保存中...",
+ "save": "保存",
+ "deleteTitle": "删除知识点",
+ "deleteDesc": "确定要删除“{name}”吗?此操作不可撤销。",
+ "delete": "删除"
+ },
+ "settings": {
+ "title": "教材设置",
+ "save": "保存",
+ "deleteConfirmTitle": "确认删除",
+ "deleteConfirmDesc": "确定要删除此教材吗?此操作不可撤销。",
+ "processing": "处理中...",
+ "delete": "删除"
+ },
+ "textbook": {
+ "titleLabel": "书名",
+ "titlePlaceholder": "输入教材名称...",
+ "subjectLabel": "学科",
+ "subjectPlaceholder": "选择学科",
+ "gradeLabel": "年级",
+ "gradePlaceholder": "选择年级",
+ "versionLabel": "版本",
+ "versionPlaceholder": "输入版本...",
+ "editTitle": "编辑教材",
+ "createTitle": "新建教材",
+ "save": "保存"
+ }
+ },
+ "subject": {
+ "chinese": "语文",
+ "mathematics": "数学",
+ "physics": "物理",
+ "chemistry": "化学",
+ "biology": "生物",
+ "english": "英语",
+ "history": "历史",
+ "geography": "地理"
+ },
+ "grade": {
+ "grade1": "一年级",
+ "grade2": "二年级",
+ "grade3": "三年级",
+ "grade4": "四年级",
+ "grade5": "五年级",
+ "grade6": "六年级",
+ "grade7": "七年级",
+ "grade8": "八年级",
+ "grade9": "九年级",
+ "grade10": "高一",
+ "grade11": "高二",
+ "grade12": "高三"
}
},
"lessonPlans": {
@@ -1310,56 +2295,357 @@
"title": "教案页面出错了",
"unknown": "发生未知错误",
"retry": "重试"
+ },
+ "action": {
+ "close": "关闭",
+ "cancel": "取消",
+ "confirm": "确认",
+ "delete": "删除"
+ },
+ "attachment": {
+ "title": "素材库",
+ "add": "上传附件",
+ "libraryLabel": "素材库",
+ "loading": "加载中...",
+ "empty": "暂无素材",
+ "delete": "删除",
+ "uploadFailed": "上传失败",
+ "uploadSuccess": "上传成功",
+ "type": {
+ "material": "教材",
+ "resource": "资源",
+ "other": "其他"
+ }
+ },
+ "knowledgePoint": {
+ "title": "选择知识点",
+ "empty": "暂无知识点"
+ },
+ "consistency": {
+ "title": "一致性校验",
+ "loading": "正在校验...",
+ "allPassed": "全部通过",
+ "hasErrors": "存在错误",
+ "hasWarnings": "存在警告"
+ },
+ "aiDifferentiation": {
+ "title": "AI 差异化教学",
+ "loading": "AI 分析中...",
+ "empty": "暂无建议",
+ "covered": "已覆盖 {count} 个",
+ "missed": "未覆盖 {count} 个",
+ "tabs": {
+ "differentiation": "差异化建议",
+ "curriculum": "课标核对",
+ "assessment": "可解释评估"
+ },
+ "level": {
+ "basic": "基础",
+ "intermediate": "进阶",
+ "advanced": "拓展"
+ }
+ },
+ "feedback": {
+ "title": "AI 反馈",
+ "loading": "AI 分析中...",
+ "empty": "暂无反馈",
+ "apply": "应用",
+ "category": {
+ "strengths": "优点",
+ "improvements": "改进建议",
+ "alignment": "目标对齐",
+ "differentiation": "差异化教学"
+ }
+ },
+ "banner": {
+ "anchorMigration": "检测到旧版锚点数据,已自动迁移到新格式。"
+ },
+ "exercise": {
+ "difficulty": "难度",
+ "contentPlaceholder": "输入题目内容...",
+ "questionId": "题目 #{id}",
+ "inlineQuestion": "内联题目",
+ "questionType": {
+ "single_choice": "单选题",
+ "multiple_choice": "多选题",
+ "text": "填空题",
+ "judgment": "判断题",
+ "composite": "复合题"
+ }
+ },
+ "detail": {
+ "selectNodeHint": "选择左侧节点查看详情",
+ "aiAssist": "AI 助手",
+ "aiGenerateLayered": "生成分层练习",
+ "aiFillExpected": "填充预期答案",
+ "aiOptimizeFollowup": "优化追问",
+ "aiGenerate": "AI 生成",
+ "aiOptimize": "AI 优化",
+ "aiDifferentiation": "AI 差异化",
+ "designIntent": "设计意图",
+ "qaDialog": "问答对话",
+ "addTurn": "添加问答",
+ "turnTeacher": "教师",
+ "turnStudent": "学生",
+ "round": "第 {n} 轮",
+ "turnContentPlaceholder": "输入发言内容...",
+ "expectedAnswer": "预期答案...",
+ "stageLabel": "教学阶段",
+ "differentiationLabel": "差异化层级",
+ "titleLabel": "标题",
+ "stageNone": "(未分组)",
+ "typeLabel": "类型",
+ "stage": {
+ "import": "导入",
+ "new_teaching": "新授",
+ "consolidation": "巩固",
+ "summary": "小结"
+ }
+ },
+ "version": {
+ "historyTitle": "版本历史",
+ "empty": "暂无历史版本",
+ "auto": "自动保存",
+ "revert": "回滚",
+ "compare": "对比",
+ "diffCount": "{count} 处差异"
+ },
+ "dialog": {
+ "versions": "版本历史",
+ "print": "打印预览",
+ "schedule": "排期",
+ "consistency": "一致性校验",
+ "aiFeedback": "AI 反馈",
+ "aiDifferentiation": "AI 差异化"
+ },
+ "homework": {
+ "type": {
+ "exercise": "练习",
+ "reading": "阅读",
+ "writing": "写作"
+ }
+ },
+ "blackboard": {
+ "layout": {
+ "structure": "结构式",
+ "mindmap": "思维导图",
+ "text": "文本"
+ }
+ },
+ "keyPoint": {
+ "type": {
+ "key": "重点",
+ "difficult": "难点"
+ }
+ },
+ "import": {
+ "method": {
+ "question": "提问导入",
+ "situation": "情境导入",
+ "review": "复习导入",
+ "other": "其他"
+ }
+ },
+ "objective": {
+ "dimension": {
+ "knowledge": "知识与技能",
+ "process": "过程与方法",
+ "emotion": "情感态度价值观"
+ }
+ },
+ "reflection": {
+ "aspect": {
+ "effectiveness": "教学效果",
+ "problems": "存在问题",
+ "improvements": "改进措施"
+ }
+ },
+ "newTeaching": {
+ "pointIndex": "知识点 {index}"
+ },
+ "paper": {
+ "textbookHeader": "教材正文",
+ "textbookPlaceholder": "输入教材正文...",
+ "expandedCount": "已展开 {count} 个节点",
+ "insertNode": "插入节点",
+ "consistencyCheck": "一致性检查"
}
},
"coursePlans": {
"title": "课程计划",
"list": {
- "title": "课程计划",
- "description": "查看和管理所有课程计划",
- "searchPlaceholder": "搜索计划名称...",
- "gradePlaceholder": "年级 ID",
- "subjectPlaceholder": "科目 ID",
- "statusFilter": "按状态筛选",
- "statusAll": "全部状态",
- "statusDraft": "草稿",
- "statusInProgress": "进行中",
- "statusCompleted": "已完成",
- "statusArchived": "已归档",
- "total": "共 {count} 条",
- "colName": "名称",
- "colSemester": "学期",
- "colStatus": "状态",
- "colUpdatedAt": "更新时间",
- "colActions": "操作",
- "viewDetail": "查看详情 →",
- "mswNotice": "列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "new": "新建计划",
+ "empty": "暂无课程计划",
+ "emptyFiltered": "无符合条件的计划",
+ "noClass": "未分配班级",
+ "unknownSubject": "未知学科",
+ "semester": "第 {semester} 学期",
+ "created": "创建于 {date}"
},
"detail": {
- "title": "课程计划详情",
- "notFound": "未找到课程计划,可能已被删除",
- "backToList": "返回列表",
- "createdAtPrefix": "创建于 {date}",
- "mswNotice": "详情查询契约待补齐(@contract-pending)。",
- "sectionBasic": "基本信息",
- "sectionUnits": "单元列表",
- "noUnits": "暂无单元",
- "fieldName": "名称",
- "fieldGradeId": "年级 ID",
- "fieldSubjectId": "科目 ID",
- "fieldSemester": "学期",
- "fieldStatus": "状态",
- "fieldProgress": "总体进度",
- "fieldDescription": "描述",
- "noDescription": "暂无描述",
- "fieldObjectives": "教学目标",
- "noObjectives": "暂无目标",
- "fieldCreatedAt": "创建时间",
- "fieldUpdatedAt": "更新时间",
- "colUnitOrder": "序号",
- "colUnitTitle": "单元标题",
- "colUnitProgress": "进度",
- "colUnitStatus": "状态"
+ "back": "返回",
+ "heading": "课程计划详情",
+ "edit": "编辑",
+ "delete": "删除",
+ "deleteTitle": "删除课程计划",
+ "deleteDescription": "确定要删除此课程计划吗?此操作不可撤销。",
+ "week": "周次",
+ "topic": "主题",
+ "hours": "课时",
+ "chapter": "教材章节",
+ "statusCol": "状态",
+ "weekPlans": "周计划",
+ "addWeekPlan": "添加周计划",
+ "emptyWeekPlans": "暂无周计划",
+ "emptyWeekPlansCta": ",点击上方按钮添加",
+ "reorderSaved": "排序已保存",
+ "reorderFailed": "排序保存失败",
+ "noClass": "未分配班级",
+ "unknownSubject": "未知学科",
+ "unknownSubjectHeading": "未知学科",
+ "semester": "第 {semester} 学期",
+ "teacher": "教师:{name}",
+ "unassigned": "未分配教师",
+ "created": "创建于 {date}",
+ "startDate": "开始于 {date}",
+ "endDate": "结束于 {date}",
+ "syllabus": "教学大纲",
+ "objectives": "教学目标",
+ "completed": "已完成",
+ "pending": "待完成",
+ "selectWeekAria": "选择第 {week} 周",
+ "viewHomeworkAria": "查看作业",
+ "viewTextbookAria": "查看教材章节 {chapter}",
+ "dragHandle": "拖拽排序",
+ "notes": "备注:{notes}"
+ },
+ "progress": {
+ "label": "进度",
+ "hours": "{completed}/{total} 课时 · {percent}%",
+ "weekPlansCompleted": "{completed}/{total} 周计划已完成"
+ },
+ "status": {
+ "planning": "规划中",
+ "active": "进行中",
+ "completed": "已完成",
+ "paused": "已暂停"
+ },
+ "filter": {
+ "placeholder": "按状态筛选",
+ "all": "全部状态"
+ },
+ "calendar": {
+ "title": "教学日历",
+ "weekShort": [
+ "一",
+ "二",
+ "三",
+ "四",
+ "五",
+ "六",
+ "日"
+ ],
+ "monthTitle": "{year} 年 {month} 月",
+ "prevMonth": "上一月",
+ "nextMonth": "下一月",
+ "today": "今天",
+ "week": "第 {week} 周",
+ "hours": "{hours} 课时",
+ "completed": "已完成",
+ "pending": "待完成",
+ "noStartDate": "课程计划未设置起始日期,无法显示日历视图",
+ "noPlans": "本月无教学计划"
+ },
+ "form": {
+ "new": "新建课程计划",
+ "edit": "编辑课程计划",
+ "class": "班级",
+ "selectClass": "选择班级",
+ "subject": "学科",
+ "selectSubject": "选择学科",
+ "teacher": "教师",
+ "selectTeacher": "选择教师",
+ "academicYear": "学年",
+ "optional": "可选",
+ "semester": "学期",
+ "semester1": "第一学期",
+ "semester2": "第二学期",
+ "status": "状态",
+ "selectStatus": "选择状态",
+ "totalHours": "总课时",
+ "weeklyHours": "周课时",
+ "startDate": "开始日期",
+ "endDate": "结束日期",
+ "syllabus": "教学大纲",
+ "syllabusPlaceholder": "教学大纲与范围...",
+ "objectives": "教学目标",
+ "objectivesPlaceholder": "教学目标与预期成果...",
+ "cancel": "取消",
+ "create": "创建",
+ "save": "保存",
+ "saving": "保存中...",
+ "invalidState": "表单状态无效",
+ "saveFailed": "保存失败"
+ },
+ "templates": {
+ "title": "从模板创建",
+ "createFromTemplate": "从现有计划复制",
+ "searchPlaceholder": "搜索班级、学科或教师...",
+ "empty": "无可用模板",
+ "cancel": "取消",
+ "confirm": "复制",
+ "cloning": "复制中...",
+ "cloneSuccess": "已从模板创建新计划",
+ "cloneFailed": "复制失败"
+ },
+ "export": {
+ "csv": "导出 CSV",
+ "filename": "{subject}_{className}_课程计划",
+ "content": "教学内容",
+ "notes": "备注",
+ "exported": "已导出",
+ "exportFailed": "导出失败"
+ },
+ "toast": {
+ "deleted": "课程计划已删除",
+ "deleteFailed": "删除失败",
+ "bulkMarked": "已批量标记 {count} 条",
+ "bulkFailed": "批量操作失败"
+ },
+ "bulk": {
+ "markComplete": "批量标记完成"
+ },
+ "loading": {
+ "title": "加载中..."
+ },
+ "item": {
+ "addTitle": "新建周计划",
+ "editTitle": "编辑周计划",
+ "week": "周次",
+ "hours": "课时",
+ "topic": "主题",
+ "topicPlaceholder": "请输入本周主题",
+ "content": "内容",
+ "contentPlaceholder": "请输入本周教学内容",
+ "chapter": "教材章节",
+ "chapterPlaceholder": "如:第3章",
+ "completedAt": "完成日期",
+ "notes": "备注",
+ "notesPlaceholder": "可选备注信息",
+ "cancel": "取消",
+ "save": "保存",
+ "saving": "保存中...",
+ "delete": "删除",
+ "markComplete": "标记完成",
+ "markIncomplete": "取消完成",
+ "invalidState": "表单状态无效",
+ "saveFailed": "保存失败",
+ "deleteFailed": "删除失败",
+ "updateFailed": "更新失败",
+ "createSuccess": "周计划已创建",
+ "updateSuccess": "周计划已更新",
+ "deleteSuccess": "周计划已删除",
+ "toggleSuccess": "完成状态已更新"
},
"error": {
"title": "课程计划模块出错了",
@@ -1441,11 +2727,458 @@
"error": {
"title": "诊断报告页面出错了",
"unknown": "诊断报告模块发生未知错误",
- "retry": "重试"
+ "retry": "重试",
+ "generateClassFailed": "生成班级诊断报告失败",
+ "loadFailed": "数据加载失败"
+ },
+ "classDiagnostic": {
+ "noClassDataTitle": "暂无班级诊断数据",
+ "heatmapDescription": "颜色越深表示掌握度越低,可点击单元格查看明细",
+ "heatmapAriaLabel": "知识点掌握度热力图,共 {count} 个知识点",
+ "heatmapCellAriaLabel": "{name}:{level}%,{label},已掌握 {mastered} 人 / 共 {total} 人",
+ "masteryLevelExcellent": "优秀",
+ "masteryLevelGood": "良好",
+ "masteryLevelNeedsImprovement": "待提升",
+ "masteryLevelWeak": "薄弱",
+ "legendLabel": "图例:",
+ "filterByKpTitle": "按知识点筛选学生",
+ "filterByKpDescription": "选择知识点查看该知识点上所有学生的掌握度",
+ "kpFilterLabel": "知识点",
+ "kpFilterPlaceholder": "请选择知识点",
+ "kpFilterAll": "全部知识点",
+ "filtering": "正在筛选...",
+ "avgMasteryColumn": "平均掌握度",
+ "totalQuestionsColumn": "总题数",
+ "correctQuestionsColumn": "答对题数",
+ "statusColumn": "状态",
+ "needsAttention": "需关注",
+ "mastered": "已掌握",
+ "viewAriaLabel": "查看 {studentName} 的诊断详情",
+ "viewAction": "查看",
+ "noStudentsForKp": "该知识点下暂无学生数据",
+ "knowledgePointColumn": "知识点",
+ "masteredColumn": "已掌握人数",
+ "notMasteredColumn": "未掌握人数",
+ "noRankingData": "暂无排名数据",
+ "studentsNeedingAttentionTitle": "需重点关注的学生",
+ "studentsNeedingAttentionDescription": "掌握度低于阈值的学生列表",
+ "allStudentsAboveThreshold": "所有学生掌握度均达到阈值",
+ "weakPointsColumn": "薄弱知识点数",
+ "generateDescription": "选择周期并生成班级诊断报告",
+ "periodLabel": "周期",
+ "generating": "生成中...",
+ "generateButton": "生成报告"
+ },
+ "empty": {
+ "noClassData": "尚未分配班级或班级暂无诊断数据",
+ "noData": "暂无诊断数据"
+ },
+ "summary": {
+ "class": "班级",
+ "students": "学生数",
+ "avgMastery": "平均掌握度",
+ "needAttention": "需关注",
+ "student": "学生",
+ "overallMastery": "总体掌握度",
+ "strengths": "强项",
+ "weaknesses": "弱项"
+ },
+ "chart": {
+ "heatmapTitle": "知识点掌握度热力图",
+ "rankingTitle": "知识点掌握度排名",
+ "radarTitle": "知识点掌握度雷达图",
+ "radarDescriptionNonEmpty": "学生分数与班级平均对比",
+ "radarEmptyTitle": "暂无掌握度数据",
+ "radarAriaLabelEmpty": "雷达图为空,学生暂无掌握度数据",
+ "radarAriaLabelNonEmpty": "雷达图,共 {count} 个知识点{withClassAverage}",
+ "withClassAverage": "(含班级平均)",
+ "studentSeries": "学生",
+ "classAvgSeries": "班级平均",
+ "noMasteryDataForStudent": "该学生暂无掌握度数据"
+ },
+ "strengths": {
+ "title": "强项知识点"
+ },
+ "weaknesses": {
+ "title": "薄弱知识点",
+ "practice": "练习"
+ },
+ "status": {
+ "draft": "草稿",
+ "published": "已发布",
+ "archived": "已归档"
+ },
+ "type": {
+ "individual": "个人",
+ "class": "班级",
+ "grade": "年级"
+ },
+ "report": {
+ "generateClass": "生成班级诊断报告",
+ "recommendations": "学习建议",
+ "history": "历史报告"
+ },
+ "reportList": {
+ "actionsColumn": "操作",
+ "allStatuses": "全部状态",
+ "allTypes": "全部类型",
+ "cancel": "取消",
+ "caption": "诊断报告列表",
+ "classReportPlaceholder": "班级报告",
+ "confidenceColumn": "置信度",
+ "confidenceHigh": "高置信度",
+ "confidenceInsufficient": "数据不足",
+ "confidenceLow": "低置信度",
+ "confidenceMedium": "中置信度",
+ "confidenceHighHint": "数据样本充足,结论可信度高",
+ "confidenceMediumHint": "数据样本一般,结论仅供参考",
+ "confidenceLowHint": "数据样本不足,建议补充数据后复查",
+ "confidenceAriaLabel": "置信度:{level}",
+ "dateColumn": "生成时间",
+ "deleteAction": "删除",
+ "deleteConfirmation": "确认删除该诊断报告?此操作不可撤销。",
+ "deleteSuccess": "诊断报告已删除",
+ "deleteTitle": "删除报告",
+ "deleting": "删除中...",
+ "emptyNoReports": "暂无诊断报告",
+ "exportAction": "导出",
+ "exportPending": "导出契约未就绪(@contract-pending),稍后通过 GraphQL 接入",
+ "filterReportType": "按类型筛选",
+ "filterStatus": "按状态筛选",
+ "generatedByColumn": "生成人",
+ "gradeReportPlaceholder": "年级报告",
+ "noReportsDescription": "学生完成作业或考试后,诊断报告会显示在这里",
+ "periodColumn": "周期",
+ "publishAction": "发布",
+ "publishConfirmation": "确认发布该诊断报告?发布后将对相关用户可见。",
+ "publishing": "发布中...",
+ "publishSuccess": "诊断报告已发布",
+ "publishTitle": "发布报告",
+ "reportType": "报告类型",
+ "scoreColumn": "分数",
+ "status": "状态",
+ "statusColumn": "状态",
+ "studentTargetColumn": "对象",
+ "typeColumn": "类型"
+ },
+ "studentDiagnostic": {
+ "noDataDescription": "该学生暂无诊断数据,请先完成相关作业或考试",
+ "strengthsDescription": "掌握度 ≥ 80% 的知识点",
+ "noStrengths": "暂无强项知识点",
+ "strengthsListAriaLabel": "强项知识点列表",
+ "weaknessesDescription": "掌握度 < 80% 的知识点,可点击练习强化",
+ "noWeaknesses": "暂无薄弱知识点",
+ "weaknessesListAriaLabel": "薄弱知识点列表",
+ "practiceAriaLabel": "练习知识点 {name}",
+ "diagnosticReportTitle": "诊断报告",
+ "reportMeta": "周期:{period} · 分数:{score}",
+ "recommendationsListAriaLabel": "学习建议列表",
+ "historyDescription": "历史诊断报告列表",
+ "untitledPeriod": "未命名周期",
+ "historyReportMeta": "生成于 {date} · 分数:{score}"
}
},
"errorBook": {
"title": "错题本",
+ "description": "自动收录考试与作业中的错题,科学复习,攻克薄弱点",
+ "stats": {
+ "total": "错题总数",
+ "new": "待学习",
+ "learning": "学习中",
+ "mastered": "已掌握",
+ "dueReview": "待复习",
+ "masteredRate": "掌握率",
+ "totalDesc": "累计收录的错题",
+ "newDesc": "尚未开始复习",
+ "learningDesc": "正在复习掌握",
+ "masteredDesc": "掌握率 {rate}%",
+ "dueReviewDesc": "今日到期复习",
+ "totalErrorQuestions": "错题总数",
+ "totalErrorCount": "错误次数",
+ "recent7dErrors": "近 7 天错误",
+ "knowledgePointCount": "知识点数"
+ },
+ "status": {
+ "new": "待学习",
+ "learning": "学习中",
+ "mastered": "已掌握",
+ "archived": "已归档"
+ },
+ "source": {
+ "exam": "考试",
+ "homework": "作业",
+ "manual": "手动添加"
+ },
+ "review": {
+ "again": "重来",
+ "hard": "困难",
+ "good": "良好",
+ "easy": "简单",
+ "againDesc": "完全不会,明天再复习",
+ "hardDesc": "勉强答对,2 天后复习",
+ "goodDesc": "正常答对,4 天后复习",
+ "easyDesc": "轻松答对,7 天后复习"
+ },
+ "actions": {
+ "add": "手动添加",
+ "viewDetail": "查看详情",
+ "saveNote": "保存笔记",
+ "archive": "归档",
+ "delete": "删除",
+ "collect": "采集错题",
+ "cancel": "取消",
+ "adding": "添加中..."
+ },
+ "fields": {
+ "question": "选择题目",
+ "note": "学习笔记",
+ "errorTags": "错误原因标签",
+ "masteryLevel": "掌握度",
+ "reviewCount": "复习次数",
+ "nextReview": "下次复习",
+ "createdAt": "添加时间",
+ "student": "学生",
+ "className": "班级"
+ },
+ "masteryLevel": {
+ "0": "未学习",
+ "1": "入门",
+ "2": "了解",
+ "3": "熟悉",
+ "4": "熟练",
+ "5": "掌握"
+ },
+ "questionType": {
+ "single_choice": "单选",
+ "multiple_choice": "多选",
+ "judgment": "判断",
+ "text": "简答",
+ "composite": "复合"
+ },
+ "errorTags": {
+ "concept": "概念不清",
+ "calculation": "计算错误",
+ "careless": "粗心大意",
+ "misread": "审题不清",
+ "method": "方法不当",
+ "memory": "记忆错误",
+ "time": "时间不足"
+ },
+ "itemCard": {
+ "questionDeleted": "题目已删除",
+ "questionContent": "题目内容",
+ "difficulty": "难度 {level}",
+ "mastery": "掌握度: {level}",
+ "reviewTimes": "复习 {count} 次",
+ "needReview": "需复习",
+ "nextReview": "下次 {date}",
+ "addedAt": "添加于 {date}",
+ "masteryOutOf": "掌握度: {level}/5"
+ },
+ "detailDialog": {
+ "question": "题目",
+ "myAnswer": "我的答案",
+ "correctAnswer": "正确答案",
+ "aiAnalysis": "AI 智能分析",
+ "reviewSelf": "复习自评",
+ "studyNote": "学习笔记",
+ "notePlaceholder": "记录你的反思、解题思路、易错点...",
+ "errorTagsLabel": "错误原因标签",
+ "reviewHistory": "复习历史",
+ "questionDeleted": "题目已删除",
+ "variantPractice": "错题变式练习",
+ "variantPracticeDesc": "从当前错题出发,进行针对性练习"
+ },
+ "filters": {
+ "searchPlaceholder": "搜索笔记内容...",
+ "status": "状态",
+ "source": "来源",
+ "review": "复习",
+ "allStatus": "全部状态",
+ "allSource": "全部来源",
+ "allErrors": "全部错题",
+ "dueOnly": "仅看待复习"
+ },
+ "addDialog": {
+ "title": "添加错题",
+ "description": "从题库中选择题目,添加到你的错题本。你也可以在完成作业/考试后自动采集。",
+ "selectQuestion": "选择题目",
+ "selectPlaceholder": "从题库中选择...",
+ "noteLabel": "学习笔记(可选)",
+ "notePlaceholder": "记录错误原因、解题思路...",
+ "errorTagsLabel": "错误原因标签",
+ "questionPreview": "题目"
+ },
+ "empty": {
+ "title": "错题本为空",
+ "description": "完成考试或作业后,错题会自动收录到这里。你也可以手动添加错题。"
+ },
+ "teacher": {
+ "title": "错题分析",
+ "description": "按学科、班级查看学生的错题统计与薄弱知识点,辅助精准教学",
+ "descriptionShort": "按学科、班级查看学生的错题统计与薄弱知识点。",
+ "coverage": "覆盖学生",
+ "totalErrors": "错题总数",
+ "avgMastery": "平均掌握率",
+ "weakPoints": "薄弱知识点",
+ "subjectDist": "学科错题分布",
+ "studentDetail": "学生错题详情",
+ "topWrong": "高频错题",
+ "noClass": "暂无可查看的班级",
+ "noClassDesc": "您还未被分配到任何班级,无法查看错题分析数据。",
+ "noStudent": "班级暂无学生",
+ "noStudentDesc": "班级中没有学生,无法查看错题分析数据。",
+ "noChapterDataTitle": "暂无章节错题数据",
+ "noChapterDataDesc": "尚未关联知识点到章节,无法显示章节维度统计。",
+ "noKpDataTitle": "暂无知识点数据",
+ "noKpDataDesc": "错题尚未关联知识点,无法显示薄弱知识点统计。",
+ "noStudentErrorsTitle": "暂无学生错题",
+ "noStudentErrorsDesc": "所选范围内没有学生错题数据。",
+ "studentsCount": "共 {total} 名学生,{withErrors} 名有错题"
+ },
+ "parent": {
+ "title": "子女错题本",
+ "description": "查看子女的错题情况与学习进度",
+ "noChild": "暂无子女关联",
+ "noChildDesc": "您的账号尚未关联子女,请联系学校管理员进行关联。",
+ "unknown": "未知",
+ "totalErrors": "错题总数",
+ "dueReview": "待复习",
+ "newItems": "待学习",
+ "mastered": "已掌握",
+ "mastery": "{rate}% 掌握",
+ "weakPoints": "薄弱知识点",
+ "errorsAndMastery": "{count} 错 · {rate}% 掌握"
+ },
+ "admin": {
+ "title": "全校错题分析",
+ "description": "全校错题统计与薄弱知识点分析,辅助教学决策",
+ "description2": "按学科查看全校学生的错题统计与薄弱知识点。",
+ "noPermissionTitle": "权限不足",
+ "noPermissionDescription": "您没有权限查看全校错题分析数据。",
+ "noStudentsTitle": "暂无学生数据",
+ "noStudentsDescription": "系统中还没有学生用户,无法查看错题分析。",
+ "topStudents": "错题最多的学生 Top 50",
+ "studentsWithErrors": "共 {count} 名学生有错题",
+ "noChapterDataTitle": "暂无章节错题数据",
+ "noChapterDataDescription": "尚未关联知识点到章节,无法显示章节维度统计。",
+ "noKnowledgePointDataTitle": "暂无知识点数据",
+ "noKnowledgePointDataDescription": "错题尚未关联知识点,无法显示薄弱知识点统计。",
+ "noStudentErrorsTitle": "暂无学生错题",
+ "noStudentErrorsDescription": "所选学科下没有学生错题数据。"
+ },
+ "analyticsStats": {
+ "coverage": "覆盖学生",
+ "coverageSub": "/ {total} 人",
+ "totalErrors": "错题总数",
+ "totalErrorsSub": "人均 {avg} 题",
+ "avgMastery": "平均掌握率",
+ "avgMasteryGood": "整体良好",
+ "avgMasteryNeedImprove": "需加强",
+ "dueReview": "待复习",
+ "dueReviewNeedAttention": "需要关注",
+ "dueReviewNone": "无到期",
+ "knowledgePoints": "涉及知识点",
+ "knowledgePointsWide": "范围较广",
+ "knowledgePointsFocused": "集中"
+ },
+ "subjectTabs": {
+ "all": "全部学科",
+ "dueReview": "待复习 {count}"
+ },
+ "classFilter": {
+ "all": "全部班级",
+ "errorCount": "{count} 错题",
+ "dueReview": "{count} 待复习"
+ },
+ "topWrong": {
+ "title": "高频错题",
+ "topTitle": "高频错题 Top 10",
+ "emptyTitle": "暂无高频错题",
+ "emptyDesc": "学生完成作业或考试后,错频统计会显示在这里。",
+ "errorCount": "{count} 人错",
+ "masteredCount": "{count} 人已掌握",
+ "masteryRate": "掌握率 {rate}%"
+ },
+ "weaknessChart": {
+ "title": "薄弱知识点 Top {count}",
+ "errorCount": "错题数",
+ "chapterLabel": "所属章节:{title}",
+ "masteredLabel": "已掌握",
+ "masteryRateLabel": "掌握率",
+ "unclassified": "未分类"
+ },
+ "chapterChart": {
+ "title": "章节错题分布(哪些课在错)",
+ "errorCount": "错题数",
+ "masteredLabel": "已掌握",
+ "masteryRateLabel": "掌握率",
+ "knowledgePointCount": "知识点数",
+ "weakKpsLabel": "薄弱知识点:",
+ "knowledgePointBadge": "{count} 个知识点"
+ },
+ "classErrorBar": {
+ "title": "各班级错题数对比",
+ "errorCount": "错题总数",
+ "studentCount": "学生数",
+ "avgPerStudent": "人均错题",
+ "avgMastery": "平均掌握率",
+ "dueReview": "待复习"
+ },
+ "subjectDistChart": {
+ "title": "各学科错题分布",
+ "errorCount": "错题数",
+ "masteredLabel": "已掌握",
+ "masteryRateLabel": "掌握率"
+ },
+ "groupedTable": {
+ "unclassified": "未分班",
+ "studentCount": "{count} 人",
+ "studentsWithErrors": "{count} 人有错题",
+ "totalErrors": "错题总数",
+ "avgMastery": "平均掌握率",
+ "student": "学生",
+ "new": "待学习",
+ "learning": "学习中",
+ "mastered": "已掌握",
+ "dueReview": "待复习",
+ "masteryRate": "掌握率",
+ "unknown": "未知"
+ },
+ "classOverview": {
+ "coverage": "覆盖学生",
+ "coverageDesc": "有错题记录的学生数",
+ "totalErrors": "错题总数",
+ "totalErrorsDesc": "班级累计错题",
+ "avgMastery": "平均掌握率",
+ "avgMasteryDesc": "已掌握错题占比",
+ "weakPoints": "薄弱知识点",
+ "weakPointsDesc": "需重点讲解",
+ "weakPointsTitle": "薄弱知识点 Top 10",
+ "subjectDist": "学科错题分布",
+ "noData": "暂无数据",
+ "errorsAndMastery": "{count} 错 · {rate}% 掌握",
+ "noStudentData": "暂无学生错题数据",
+ "noStudentDataDesc": "学生完成作业或考试后,错题数据会自动汇总到这里。"
+ },
+ "messages": {
+ "added": "错题已添加",
+ "noteSaved": "笔记已保存",
+ "reviewRecorded": "复习结果已记录",
+ "archived": "错题已归档",
+ "deleted": "错题已删除",
+ "collected": "已采集 {count} 道错题",
+ "noNewErrors": "没有新的错题需要采集",
+ "addFailed": "添加错题失败",
+ "saveFailed": "保存失败",
+ "deleteFailed": "删除失败",
+ "archiveFailed": "归档失败",
+ "collectFailed": "采集错题失败",
+ "notFound": "错题不存在或无权访问",
+ "selectQuestion": "请选择题目",
+ "recordFailed": "记录失败",
+ "addedShort": "已添加"
+ },
"list": {
"title": "错题本",
"description": "查看学生的错题记录与知识点统计",
@@ -1459,12 +3192,6 @@
"colContent": "内容",
"noContent": "无内容"
},
- "stats": {
- "totalErrorQuestions": "错题总数",
- "totalErrorCount": "错误次数",
- "recent7dErrors": "近 7 天错误",
- "knowledgePointCount": "知识点数"
- },
"error": {
"title": "错题本页面出错了",
"unknown": "错题本模块发生未知错误",
@@ -1473,6 +3200,19 @@
},
"practice": {
"title": "练习分析",
+ "tabs": {
+ "assignments": "练习与作业",
+ "adaptive": "自适应练习"
+ },
+ "adaptive": {
+ "description": "选择知识点与题型,发起一次针对性自适应练习;下方为本人的练习历史。",
+ "starterSection": "发起练习",
+ "historySection": "练习历史",
+ "knowledgePointsLoading": "知识点加载中...",
+ "knowledgePointsEmpty": "暂无知识点,无法发起练习",
+ "historyLoading": "练习历史加载中...",
+ "historyError": "练习历史加载失败"
+ },
"list": {
"title": "练习分析",
"description": "查看和管理所有练习与作业",
@@ -1504,6 +3244,92 @@
"title": "练习分析页面出错了",
"unknown": "练习分析模块发生未知错误",
"retry": "重试"
+ },
+ "starter": {
+ "title": "发起专项练习",
+ "description": "选择知识点与题型,开始一次针对性练习",
+ "type": "练习类型",
+ "knowledgePoints": "知识点",
+ "difficulty": "难度",
+ "anyDifficulty": "不限难度",
+ "questionCount": "题目数量",
+ "start": "开始练习",
+ "creating": "创建中..."
+ },
+ "types": {
+ "error_variant": "错题变式",
+ "knowledge_point": "知识点专项",
+ "weak_chapter": "薄弱章节",
+ "ai_recommended": "AI 推荐"
+ },
+ "status": {
+ "in_progress": "进行中",
+ "completed": "已完成",
+ "abandoned": "已放弃"
+ },
+ "toasts": {
+ "created": "练习会话已创建",
+ "createFailed": "创建练习会话失败",
+ "selectKnowledgePoint": "请至少选择一个知识点",
+ "selectWeakKnowledgePoint": "请至少选择一个薄弱知识点",
+ "submitted": "答案已提交",
+ "submitFailed": "提交答案失败",
+ "completed": "练习已完成",
+ "completeFailed": "完成练习失败",
+ "abandoned": "练习已放弃",
+ "abandonFailed": "放弃练习失败"
+ },
+ "errors": {
+ "SESSION_NOT_FOUND": "练习会话不存在",
+ "SESSION_NOT_IN_PROGRESS": "练习会话不在进行中",
+ "ANSWER_NOT_FOUND": "答题记录不存在",
+ "QUESTION_NOT_FOUND": "题目不存在",
+ "INSUFFICIENT_QUESTIONS": "题目数量不足",
+ "INVALID_INPUT": "输入参数不合法",
+ "UNAUTHORIZED": "无权操作此练习会话"
+ },
+ "session": {
+ "progress": "进度",
+ "previous": "上一题",
+ "next": "下一题",
+ "abandon": "放弃",
+ "abandonConfirm": "确认放弃练习?",
+ "abandonDescription": "放弃后已答内容将保留,但不能再继续答题。",
+ "cancel": "取消",
+ "confirmAbandon": "确认放弃",
+ "complete": "完成练习",
+ "empty": "本次练习暂无题目",
+ "question": "题目",
+ "difficulty": "难度",
+ "variant": "变式",
+ "skip": "跳过",
+ "retry": "重试",
+ "submit": "提交",
+ "submitting": "提交中...",
+ "submitFailedDescription": "提交失败,请重试或检查网络连接。",
+ "true": "正确",
+ "false": "错误",
+ "textPlaceholder": "请输入答案...",
+ "correct": "回答正确",
+ "incorrect": "回答错误",
+ "pendingReview": "待批阅",
+ "skipped": "已跳过",
+ "yourAnswer": "你的答案"
+ },
+ "result": {
+ "title": "练习结果",
+ "answered": "已答题数",
+ "correct": "正确数",
+ "accuracy": "正确率",
+ "review": "逐题回顾"
+ },
+ "history": {
+ "empty": "暂无练习历史"
+ },
+ "reasons": {
+ "student_initiated": "学生自主发起",
+ "teacher_assigned": "教师布置",
+ "parent_suggested": "家长建议"
}
},
"elective": {
@@ -1528,7 +3354,11 @@
"colUpdatedAt": "更新时间",
"colActions": "操作",
"edit": "编辑",
- "mswNotice": "列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "adminList": "门选修课",
+ "empty": "暂无选修课",
+ "emptyDescription": "点击新建按钮创建第一门选修课",
+ "emptyStudent": "暂无可选课程"
},
"create": {
"title": "新建选修课",
@@ -1572,6 +3402,118 @@
"title": "选修课模块出错了",
"unknown": "选修课模块发生未知错误",
"retry": "重试"
+ },
+ "errors": {
+ "unexpected": "发生未知错误,请稍后重试",
+ "notFound": "未找到课程"
+ },
+ "actions": {
+ "create": "新建",
+ "edit": "编辑",
+ "delete": "删除",
+ "openSelection": "开放选课",
+ "closeSelection": "关闭选课",
+ "runLottery": "运行抽签",
+ "select": "选课",
+ "drop": "退课",
+ "cancel": "取消"
+ },
+ "fields": {
+ "credit": "学分",
+ "teacher": "任课老师",
+ "selectionMode": "选课模式",
+ "capacity": "容量",
+ "classroom": "教室",
+ "schedule": "上课时间",
+ "subject": "科目",
+ "grade": "年级",
+ "startDate": "开始日期",
+ "endDate": "结束日期",
+ "selectionStart": "选课开始",
+ "selectionEnd": "选课结束",
+ "description": "描述",
+ "enrolled": "已报名",
+ "dropReason": "退课理由"
+ },
+ "status": {
+ "draft": "草稿",
+ "open": "报名中",
+ "closed": "已关闭",
+ "cancelled": "已取消"
+ },
+ "selectionMode": {
+ "fcfs": "先到先得",
+ "lottery": "抽签"
+ },
+ "selectionStatus": {
+ "selected": "已选",
+ "enrolled": "已录取",
+ "waitlist": "候补",
+ "dropped": "已退",
+ "rejected": "已拒绝"
+ },
+ "student": {
+ "capacityFull": "已满",
+ "mySelections": "我的选课",
+ "availableCourses": "可选课程",
+ "selected": "已选",
+ "selectSuccess": "选课成功",
+ "dropSuccess": "退课成功",
+ "confirmDrop": "确认退课?",
+ "dropReasonPlaceholder": "请输入退课理由(可选)"
+ },
+ "parent": {
+ "noRecordsTitle": "暂无选课记录",
+ "noRecordsDescription": "该学生尚未选课"
+ },
+ "description": {
+ "detail": "选修课详情",
+ "student": "选修课选课"
+ },
+ "detail": {
+ "back": "返回",
+ "editCourse": "编辑课程",
+ "studentsTitle": "选课名单",
+ "noStudents": "暂无学生",
+ "noStudentsDescription": "暂无学生选课",
+ "studentName": "学生",
+ "priority": "优先级",
+ "selectedAt": "选课时间",
+ "enrolledAt": "录取时间"
+ },
+ "export": {
+ "statusHeader": "状态",
+ "selectedAtHeader": "选课时间"
+ },
+ "form": {
+ "createTitle": "新建选修课",
+ "editTitle": "编辑选修课",
+ "nameLabel": "名称",
+ "subjectLabel": "科目",
+ "selectSubjectPlaceholder": "选择科目",
+ "gradeLabel": "年级",
+ "selectGradePlaceholder": "选择年级",
+ "teacherLabel": "任课老师",
+ "selectTeacherPlaceholder": "选择老师",
+ "capacityLabel": "容量",
+ "classroomLabel": "教室",
+ "scheduleLabel": "上课时间",
+ "schedulePlaceholder": "例如:周一 3-4 节",
+ "creditLabel": "学分",
+ "startDateLabel": "开始日期",
+ "endDateLabel": "结束日期",
+ "selectionStartLabel": "选课开始时间",
+ "selectionEndLabel": "选课结束时间",
+ "dropDeadlineLabel": "退课截止时间",
+ "dropDeadlineHint": "超过此时间后学生无法退课,留空表示不限制",
+ "descriptionLabel": "描述",
+ "descriptionPlaceholder": "课程简介、适用人群等",
+ "cancelButton": "取消",
+ "createButton": "创建",
+ "saveButton": "保存",
+ "savingButton": "保存中...",
+ "invalidFormState": "表单状态无效",
+ "saveFailed": "保存失败"
}
},
"leave": {
@@ -1634,6 +3576,7 @@
"emptyTitle": "暂无调课申请",
"emptyDescription": "调整筛选条件后重试,或发起第一条申请",
"emptyAction": "清空筛选",
+ "createAction": "发起调课",
"mswNotice": "调课列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
"colSummary": "摘要",
"colClassName": "班级",
@@ -1643,7 +3586,134 @@
"colReason": "事由",
"colStatus": "状态",
"colApplicant": "申请人",
- "colCreatedAt": "创建时间"
+ "colCreatedAt": "创建时间",
+ "colActions": "操作"
+ },
+ "form": {
+ "title": "调课申请",
+ "dialogTitle": "发起调课申请",
+ "dialogDescription": "填写表单发起调课、停课、代课或合课申请",
+ "classLabel": "班级",
+ "classPlaceholder": "选择班级",
+ "typeLabel": "调课类型",
+ "typeReschedule": "调课",
+ "typeCancel": "停课",
+ "typeSubstitute": "代课",
+ "typeMerge": "合课",
+ "originalLessonLabel": "原课程",
+ "originalLessonPlaceholder": "请输入原课程名称",
+ "originalTeacherLabel": "原任课教师",
+ "originalTeacherPlaceholder": "选择原任课教师",
+ "substituteTeacherLabel": "代课教师",
+ "substituteTeacherPlaceholder": "选择代课教师",
+ "originalDateLabel": "原日期",
+ "newDateLabel": "新日期",
+ "newStartLabel": "新开始时间",
+ "newEndLabel": "新结束时间",
+ "reasonLabel": "事由",
+ "reasonPlaceholder": "请说明调课原因...",
+ "cancel": "取消",
+ "submit": "提交申请",
+ "submitting": "提交中...",
+ "createSuccess": "调课申请提交成功",
+ "errors": {
+ "classRequired": "请选择班级",
+ "originalLessonRequired": "请输入原课程",
+ "reasonRequired": "请填写事由",
+ "submitFailed": "提交失败"
+ }
+ },
+ "review": {
+ "approve": "批准",
+ "reject": "驳回",
+ "approveTitle": "批准调课申请",
+ "rejectTitle": "驳回调课申请",
+ "approveDescription": "确认要批准此调课申请吗?",
+ "rejectDescription": "请提供驳回原因(可选)",
+ "commentLabel": "审批意见",
+ "commentPlaceholder": "请输入审批意见...",
+ "commentRequired": "驳回时审批意见必填",
+ "cancel": "取消",
+ "stubSuccess": "审批已处理(stub)",
+ "errors": {
+ "submitFailed": "审批失败"
+ }
+ },
+ "grid": {
+ "title": "课表网格",
+ "emptyClasses": "暂无可用班级,请先创建班级。",
+ "selectClass": "选择班级",
+ "periodColumn": "节次",
+ "periodLabel": "第 {n} 节",
+ "legend": "图例"
+ },
+ "conflicts": {
+ "title": "冲突检测",
+ "classLabel": "班级",
+ "classPlaceholder": "选择班级",
+ "checkButton": "检测冲突",
+ "checking": "检测中...",
+ "resultsTitle": "检测结果",
+ "conflictCount": "{count} 个冲突",
+ "noConflicts": "未检测到冲突",
+ "typeTeacherOverlap": "教师冲突",
+ "typeClassroomOverlap": "教室冲突",
+ "typeClassOverlap": "班级冲突",
+ "typeRuleViolation": "规则违反",
+ "errors": {
+ "classRequired": "请选择班级",
+ "checkFailed": "检测失败"
+ }
+ },
+ "rules": {
+ "title": "排课规则",
+ "classLabel": "班级",
+ "classPlaceholder": "选择班级",
+ "maxDailyHours": "每日最大课时",
+ "maxContinuousHours": "最大连续课时",
+ "morningStart": "上午开始",
+ "afternoonEnd": "下午结束",
+ "lunchBreakStart": "午休开始",
+ "lunchBreakEnd": "午休结束",
+ "avoidBackToBack": "避免连续排课",
+ "balancedSubjects": "科目均衡分布",
+ "cancel": "取消",
+ "save": "保存规则",
+ "saving": "保存中...",
+ "errors": {
+ "classRequired": "请选择班级",
+ "saveFailed": "保存失败"
+ }
+ },
+ "auto": {
+ "title": "自动排课",
+ "classLabel": "班级",
+ "classPlaceholder": "选择班级",
+ "previewButton": "预览排课",
+ "previewing": "生成中...",
+ "applyButton": "应用到班级",
+ "applying": "应用中...",
+ "previewSummary": "已生成 {scheduled} 节,{conflicts} 个冲突",
+ "applySuccess": "排课已应用",
+ "errors": {
+ "classRequired": "请选择班级",
+ "previewFailed": "预览失败",
+ "applyFailed": "应用失败"
+ }
+ },
+ "autoResult": {
+ "title": "生成课表",
+ "sessionCount": "{count} 节",
+ "conflictCount": "{count} 个冲突",
+ "noSessions": "未生成任何课程",
+ "colDay": "星期",
+ "colStart": "开始",
+ "colEnd": "结束",
+ "colCourse": "课程",
+ "colLocation": "教室",
+ "conflictsTitle": "冲突与警告",
+ "readyToApply": "未检测到冲突,排课可应用",
+ "unknownDay": "第 {n} 天"
},
"error": {
"title": "调课申请页面出错了",
@@ -1778,7 +3848,64 @@
"labelScoreValue": "{score} 分",
"emptyWeakPoints": "暂无薄弱知识点数据",
"emptyTrends": "暂无趋势数据",
- "loadFailed": "仪表盘数据加载失败,请稍后重试。"
+ "loadFailed": "仪表盘数据加载失败,请稍后重试。",
+ "statEnrolledClasses": "已加入班级",
+ "descActiveEnrollments": "进行中的课程",
+ "statGraded": "已评分",
+ "descCompletedAssignments": "已完成作业",
+ "statDueSoon": "即将到期",
+ "descNext7Days": "未来 7 天",
+ "statOverdue": "已逾期",
+ "descNeedsAttention": "需要关注",
+ "descOverallPerformance": "整体表现",
+ "descNoGradesYet": "暂无成绩",
+ "descCurrentPosition": "当前位置",
+ "descNoRankingYet": "暂无排名",
+ "sectionUpcomingAssignments": "待办作业",
+ "sectionRecentGrades": "近期成绩",
+ "sectionTodaySchedule": "今日课表",
+ "emptyNoAssignments": "暂无待办作业",
+ "emptyNoAssignmentsDesc": "暂无需要完成的作业",
+ "emptyNoGradedWork": "暂无评分记录",
+ "emptyNoGradedWorkDesc": "暂无已评分的作业",
+ "emptyNoClassesToday": "今日无课程",
+ "emptyNoClassesTodayDesc": "今天没有安排课程",
+ "colTitle": "标题",
+ "colSubject": "学科",
+ "colStatus": "状态",
+ "colDue": "截止时间",
+ "colScore": "得分",
+ "colAction": "操作",
+ "colAssignment": "作业",
+ "colWhen": "时间",
+ "colClass": "班级",
+ "colTime": "时间",
+ "colLocation": "地点",
+ "actionViewAll": "查看全部",
+ "actionViewSchedule": "查看课表",
+ "actionStart": "开始",
+ "actionContinue": "继续",
+ "actionReview": "查看",
+ "badgeInProgress": "进行中",
+ "badgeUpNext": "下一节",
+ "badgeLate": "逾期",
+ "badgeGraded": "已评分",
+ "badgeSubmitted": "已提交",
+ "badgeNotStarted": "未开始",
+ "labelLatest": "最近",
+ "labelPoints": "得分",
+ "labelNoGrades": "暂无成绩",
+ "greetingMorning": "早上好",
+ "greetingNoon": "中午好",
+ "greetingAfternoon": "下午好",
+ "greetingEvening": "晚上好",
+ "greetingNight": "夜深了",
+ "greetingWithName": "{greeting},{name}",
+ "sectionGradeTrend": "成绩趋势",
+ "legendScore": "我的成绩",
+ "legendClassAvg": "班级均分",
+ "emptyGradeTrend": "暂无成绩趋势数据",
+ "labelUrgent": "紧急"
},
"trend": {
"title": "学习趋势",
@@ -1809,6 +3936,38 @@
"practiceNow": "去练习"
}
},
+ "diagnostic": {
+ "title": "学生诊断报告",
+ "description": "查看个人学习诊断、知识点掌握分布与历史报告",
+ "statStudentName": "学生姓名",
+ "statOverallMastery": "总体掌握率",
+ "statStrengthCount": "优势学科数",
+ "statWeaknessCount": "弱势学科数",
+ "sectionMasteryRadar": "知识点掌握度分布",
+ "sectionStrengths": "优势知识点",
+ "sectionWeakness": "弱势知识点",
+ "sectionLatestReport": "最新诊断报告",
+ "sectionHistoryReports": "历史报告",
+ "emptyDiagnostic": "暂无诊断数据",
+ "emptyStrengths": "暂无优势知识点",
+ "emptyWeaknessPoints": "暂无弱势知识点",
+ "emptyReports": "暂无历史报告",
+ "colKnowledgePoint": "知识点",
+ "colMastery": "掌握度",
+ "colActions": "操作",
+ "practiceNow": "去练习",
+ "badgePublished": "已发布",
+ "badgeArchived": "已归档",
+ "badgeDraft": "草稿",
+ "badgeGenerated": "已生成",
+ "fieldPeriod": "报告期",
+ "fieldScore": "得分",
+ "fieldSummary": "摘要",
+ "fieldRecommendations": "推荐建议",
+ "confidenceLabel": "置信度",
+ "confidenceTooltip": "置信度反映诊断结论的可信程度,0-1 区间,越高越可信",
+ "mswNotice": "诊断数据契约为 @contract-pending,当前通过 MSW 兜底。后端补齐后将切换为真实数据。"
+ },
"grades": {
"list": {
"title": "我的成绩",
@@ -1818,6 +3977,11 @@
"allSubjects": "全部学科",
"typeFilter": "按类型筛选",
"allTypes": "全部类型",
+ "semesterFilter": "按学期筛选",
+ "allSemesters": "全部学期",
+ "semester1": "第一学期",
+ "semester2": "第二学期",
+ "resetFilters": "重置",
"total": "共 {count} 条",
"colSubject": "学科",
"colType": "类型",
@@ -1829,7 +3993,59 @@
"colActions": "操作",
"viewReportCard": "查看报告卡 →",
"emptyTitle": "暂无成绩记录",
- "mswNotice": "成绩列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "emptyDescription": "调整筛选条件或等待成绩录入",
+ "mswNotice": "成绩列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "sectionSummary": "成绩汇总",
+ "fieldTotalRecords": "成绩记录数",
+ "fieldAverageScore": "平均得分率",
+ "fieldPassRate": "及格率",
+ "fieldExcellentRate": "优秀率",
+ "sectionTrend": "成绩趋势",
+ "sectionRankingTrend": "排名趋势",
+ "sectionDistribution": "班级分布",
+ "sectionGrowthArchive": "成长档案",
+ "trendEmptyTitle": "暂无趋势数据",
+ "trendEmptyDescription": "至少需要 1 条成绩记录才能绘制趋势",
+ "trendScorePercent": "得分率(%)",
+ "trendClassAverage": "班级平均",
+ "trendRangeAll": "全部",
+ "trendRangeDays7": "近 7 天",
+ "trendRangeDays30": "近 30 天",
+ "trendRangeDays90": "近 90 天",
+ "rankingEmptyTitle": "暂无排名数据",
+ "rankingEmptyDescription": "排名数据待后端契约补齐后展示",
+ "distributionEmptyTitle": "暂无分布数据",
+ "distributionEmptyDescription": "班级分布数据待后端契约补齐后展示",
+ "distributionYourPosition": "你的位置:得分 {score},位于 {bucket} 段",
+ "growthEmptyTitle": "暂无成长档案",
+ "growthEmptyDescription": "跨学期成长档案数据待后端契约补齐后展示",
+ "rankingLegend": "班级排名(越小越好)",
+ "rankingTotalStudents": "全班 {total} 人",
+ "rankingCurrentRank": "当前第 {rank} 名",
+ "distributionXAxis": "分数段",
+ "distributionYAxis": "人数",
+ "distributionBucketCount": "{count} 人",
+ "distributionStudentPosition": "你在 {bucket} 段,全班第 {rank} 名",
+ "growthArchiveXAxis": "学期",
+ "growthArchiveYAxis": "得分",
+ "growthArchiveSubjectLabel": "学科",
+ "subjects": {
+ "chinese": "语文",
+ "math": "数学",
+ "english": "英语",
+ "physics": "物理",
+ "chemistry": "化学",
+ "biology": "生物",
+ "history": "历史",
+ "geography": "地理",
+ "politics": "政治"
+ },
+ "types": {
+ "exam": "考试",
+ "homework": "作业",
+ "quiz": "测验",
+ "comprehensive": "综合"
+ }
},
"reportCard": {
"title": "成绩报告卡",
@@ -1845,20 +4061,65 @@
"colScoreRate": "得分率",
"colLevel": "等级",
"colTeacherComment": "教师评语",
- "emptyTitle": "暂无报告卡数据"
+ "emptyTitle": "暂无报告卡数据",
+ "emptyDescription": "请切换学年/学期或等待成绩录入",
+ "schoolName": "学校",
+ "reportCardTitle": "学期成绩报告卡",
+ "periodLabel": "学年 {year} · {semester}",
+ "allSemesters": "全部学期",
+ "studentNameLabel": "学生姓名",
+ "classNameLabel": "所在班级",
+ "generatedAtLabel": "生成时间",
+ "gradesSectionTitle": "各科成绩明细",
+ "summarySectionTitle": "综合统计",
+ "overallAverage": "总平均分",
+ "overallRank": "总排名",
+ "passRate": "及格率",
+ "excellentRate": "优秀率",
+ "commentsTitle": "教师评语",
+ "commentsPlaceholder": "(教师评语待录入)",
+ "signatureClassTeacher": "班主任签名",
+ "signatureParent": "家长签名",
+ "signaturePrincipal": "校长签名",
+ "footerNote": "本报告卡生成于 {date}",
+ "academicYearsCount": "共 {count} 个学年可选",
+ "periodSelectorTitle": "学年/学期切换",
+ "allAcademicYears": "全部学年",
+ "semester1": "第一学期",
+ "semester2": "第二学期",
+ "resetFilters": "重置",
+ "subjectCount": "科目数",
+ "signaturesTitle": "签名区",
+ "summaryRowLabel": "合计",
+ "classTeacherLabel": "班主任",
+ "studentInfoTitle": "学生基本信息",
+ "colAssessment": "评估名称",
+ "colType": "类型",
+ "colRank": "排名",
+ "colRemark": "备注",
+ "typeExam": "考试",
+ "typeHomework": "作业",
+ "typeQuiz": "测验",
+ "subjectAvgLabel": "学科平均",
+ "noRecords": "暂无记录",
+ "preparing": "准备中…",
+ "errorPrint": "打印失败,请重试",
+ "commentsAriaLabel": "教师评语区",
+ "rankFormat": "{rank} / {total}",
+ "classTotalStudentsLabel": "班级人数"
}
},
"exams": {
"list": {
"title": "我的考试",
- "description": "查看即将到来的考试与已结束考试",
+ "description": "按状态查看所有考试,进行中的考试可直接进入作答",
"statusFilter": "按状态筛选",
"allStatus": "全部状态",
"statusUpcoming": "即将开始",
"statusInProgress": "进行中",
"statusEnded": "已结束",
"statusScored": "已出分",
- "total": "共 {count} 条",
+ "total": "共 {count} 场",
"colTitle": "考试名称",
"colSubject": "学科",
"colExamDate": "考试时间",
@@ -1871,7 +4132,28 @@
"takeExam": "进入考试 →",
"viewResult": "查看结果 →",
"emptyTitle": "暂无考试",
- "mswNotice": "学生考试列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "学生考试列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "statusNotStarted": "未开始",
+ "statusExpired": "已结束",
+ "subjectFilter": "按学科筛选",
+ "allSubjects": "全部学科",
+ "groupInProgress": "进行中",
+ "groupInProgressDesc": "点击进入作答",
+ "groupUpcoming": "即将开始",
+ "groupUpcomingDesc": "等待开考",
+ "groupSubmitted": "已提交",
+ "groupSubmittedDesc": "等待批改",
+ "groupScored": "已出分",
+ "groupScoredDesc": "查看成绩",
+ "groupEnded": "已结束",
+ "groupEndedDesc": "考试已结束",
+ "countdownStartsIn": "{n} 天后开考",
+ "countdownHoursLeft": "{n} 小时后开考",
+ "countdownStarted": "已开考",
+ "countdownUrgent": "即将开考",
+ "colQuestionCount": "题量",
+ "colTotalScore": "满分",
+ "emptyDesc": "当老师发布考试后,这里会显示考试日程与作答入口"
},
"result": {
"title": "考试结果",
@@ -1883,7 +4165,7 @@
"fieldTotalScore": "满分",
"fieldRank": "班级排名",
"fieldClassAvg": "班级均分",
- "fieldDuration": "用时",
+ "fieldDuration": "用时(分钟)",
"colQuestionNo": "题号",
"colQuestion": "题目",
"colYourAnswer": "你的答案",
@@ -1894,7 +4176,15 @@
"colMastery": "掌握度",
"addToErrorBook": "加入错题本",
"emptyQuestions": "暂无题目数据",
- "notFound": "未找到考试结果"
+ "notFound": "未找到考试结果",
+ "sectionStats": "答题统计",
+ "sectionWrongQuestions": "错题预览",
+ "statsTotal": "总题数",
+ "statsCorrect": "答对",
+ "statsWrong": "答错",
+ "statsUnanswered": "未答",
+ "noWrongQuestions": "本次考试无错题",
+ "colQuestionType": "题型"
},
"take": {
"title": "考试作答",
@@ -1908,19 +4198,37 @@
"unanswered": "未答",
"marked": "已标记",
"notFound": "未找到考试或考试已结束",
- "submitError": "提交失败,请重试"
+ "submitError": "提交失败,请重试",
+ "totalScore": "满分",
+ "back": "返回列表",
+ "submitting": "提交中…",
+ "confirmSubmit": "确认提交",
+ "confirmSubmitDescription": "提交后无法修改,请确认所有题目已检查完毕。",
+ "unansweredWarning": "还有 {count} 题未作答,确定要提交吗?",
+ "confirmSubmitAction": "确认提交",
+ "autoSaveSaving": "正在保存…",
+ "autoSaveIdle": "未保存",
+ "timeUpAutoSubmit": "时间到,已自动提交",
+ "questionUnit": "题",
+ "makeSureAnswered": "提交前请确认所有题目已作答",
+ "questionType": "题型",
+ "score": "分值",
+ "prevQuestion": "上一题",
+ "nextQuestion": "下一题",
+ "jumpToQuestion": "跳转到第 {no} 题"
}
},
"homework": {
"list": {
"title": "我的作业",
- "description": "按学科分组查看作业列表",
+ "description": "按学科分组查看作业,待提交的作业可直接进入提交页面",
"statusFilter": "按状态筛选",
"allStatus": "全部状态",
"statusPending": "待提交",
"statusSubmitted": "已提交",
"statusGraded": "已批改",
- "total": "共 {count} 条",
+ "statusOverdue": "已逾期",
+ "total": "共 {count} 份",
"colTitle": "作业标题",
"colSubject": "学科",
"colDueDate": "截止时间",
@@ -1930,7 +4238,38 @@
"submitHomework": "去提交 →",
"viewAnalysis": "查看分析 →",
"emptyTitle": "暂无作业",
- "mswNotice": "学生作业列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "学生作业列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "subjectFilter": "按学科筛选",
+ "allSubjects": "全部学科",
+ "searchPlaceholder": "搜索作业标题…",
+ "groupPending": "待提交",
+ "groupPendingDesc": "点击进入提交",
+ "groupOverdue": "已逾期",
+ "groupOverdueDesc": "请尽快提交",
+ "groupSubmitted": "已提交",
+ "groupSubmittedDesc": "等待批改",
+ "groupGraded": "已批改",
+ "groupGradedDesc": "查看得分",
+ "bucketActive": "待处理",
+ "bucketDone": "已完成",
+ "urgent24h": "24h 内截止",
+ "overdueTag": "已逾期",
+ "emptyDesc": "当老师布置作业后,这里会显示作业列表与提交入口",
+ "viewCard": "卡片视图",
+ "viewTable": "表格视图",
+ "attempts": "尝试次数",
+ "attemptsValue": "{used} / {max}",
+ "latestScore": "最新成绩",
+ "actionStart": "开始",
+ "actionContinue": "继续",
+ "actionView": "查看",
+ "actionReview": "复核",
+ "overdueBadge": "已逾期",
+ "groupUnanswered": "未答",
+ "groupAnswered": "已答",
+ "noResults": "未找到匹配的作业",
+ "statusInProgress": "进行中",
+ "statusNotStarted": "未开始"
},
"submit": {
"title": "作业作答",
@@ -1939,7 +4278,39 @@
"submitSuccess": "作业已提交",
"autoSaveTip": "答案已自动保存",
"notFound": "未找到作业或作业已截止",
- "submitError": "提交失败,请重试"
+ "submitError": "提交失败,请重试",
+ "questionNav": "题目导航",
+ "answerPlaceholder": "请输入答案...",
+ "submitPanelTitle": "提交",
+ "answerProgress": "答题进度",
+ "storageRestoreFailed": "本地暂存数据已损坏,已回退到后端数据",
+ "storageSaveFailed": "本地暂存写入失败,不影响作答",
+ "reviewTitle": "作业复盘",
+ "takeMode": "作答模式",
+ "reviewMode": "只读复盘",
+ "description": "作业说明",
+ "attemptsUsed": "尝试次数",
+ "attemptsValue": "{used} / {max}",
+ "back": "返回列表",
+ "submitting": "提交中…",
+ "confirmSubmit": "确认提交",
+ "confirmSubmitDescription": "提交后无法修改,请确认所有题目已检查完毕。",
+ "unansweredWarning": "还有 {count} 题未作答,确定要提交吗?",
+ "confirmSubmitAction": "确认提交",
+ "autoSaveSaving": "正在保存…",
+ "autoSaveIdle": "未保存",
+ "questionType": "题型",
+ "fieldTitle": "作业标题",
+ "fieldDueDate": "截止时间",
+ "fieldSubmittedAt": "提交时间",
+ "fieldStatus": "状态",
+ "fieldScore": "得分",
+ "fieldTotalScore": "满分",
+ "viewAnalysis": "查看分析",
+ "teacherComment": "教师评语",
+ "noComment": "暂无评语",
+ "questionStem": "题干",
+ "maxAttempts": "最大尝试次数"
},
"analysis": {
"title": "作业分析",
@@ -1954,7 +4325,13 @@
"colCorrectAnswer": "参考答案",
"colScore": "得分",
"colIsCorrect": "对错",
- "notFound": "未找到作业分析数据"
+ "notFound": "未找到作业分析数据",
+ "sectionStats": "答题统计",
+ "sectionWrongQuestions": "错题预览",
+ "statsTotal": "总题数",
+ "statsCorrect": "答对",
+ "statsWrong": "答错",
+ "noWrongQuestions": "本次作业无错题"
}
},
"schedule": {
@@ -1968,7 +4345,21 @@
"colClassroom": "教室",
"colTime": "时间",
"emptyTitle": "暂无课表数据",
- "mswNotice": "学生课表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "学生课表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "weekdays": {
+ "monday": "周一",
+ "tuesday": "周二",
+ "wednesday": "周三",
+ "thursday": "周四",
+ "friday": "周五",
+ "saturday": "周六",
+ "sunday": "周日"
+ },
+ "emptyDescription": "尚未获取到课表数据,请稍后再试或联系管理员",
+ "today": "今天",
+ "noClasses": "今日无课",
+ "noStudentTitle": "未获取到学生身份",
+ "noStudentDesc": "请先登录学生账号或联系管理员补全学生身份信息"
},
"attendance": {
"title": "考勤记录",
@@ -1984,7 +4375,14 @@
"colStatus": "状态",
"colRemark": "备注",
"emptyTitle": "暂无考勤记录",
- "mswNotice": "学生考勤查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "学生考勤查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "status": {
+ "present": "出勤",
+ "late": "迟到",
+ "early_leave": "早退",
+ "leave": "请假",
+ "absent": "缺勤"
+ }
},
"classes": {
"title": "我的班级",
@@ -2016,7 +4414,32 @@
"colActions": "操作",
"viewDetail": "查看详情 →",
"emptyTitle": "暂无课程",
- "mswNotice": "学生课程列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "学生课程列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "viewModeLabel": "视图模式",
+ "viewCard": "卡片视图",
+ "viewTable": "表格视图",
+ "joinClass": "加入班级",
+ "joinClassTitle": "通过邀请码加入班级",
+ "joinClassDescription": "请输入教师提供的 6 位数字邀请码",
+ "joinClassCodeLabel": "邀请码",
+ "joinClassCodePlaceholder": "请输入 6 位数字邀请码",
+ "joinClassCodeHint": "邀请码由 6 位数字组成,可向任课教师索取。",
+ "joinClassSubmit": "加入",
+ "joinClassSubmitting": "加入中...",
+ "joinClassCancel": "取消",
+ "joinClassSuccess": "加入班级成功",
+ "joinClassFailed": "加入失败:{message}",
+ "joinClassCodeRequired": "请输入邀请码",
+ "joinClassCodeInvalid": "邀请码必须是 6 位数字",
+ "statusActive": "活跃",
+ "statusInactive": "已结课",
+ "cardTeacher": "任课教师",
+ "cardSchool": "学校",
+ "cardHeadTeacher": "班主任",
+ "cardGrade": "年级",
+ "sendEmail": "发送邮件",
+ "colRoom": "教室",
+ "colGrade": "年级"
},
"detail": {
"title": "课程详情",
@@ -2033,6 +4456,18 @@
"sectionSchedule": "本班课表",
"viewFullSchedule": "查看完整课表",
"viewHomework": "查看作业",
+ "colWeekday": "星期",
+ "colPeriod": "节次",
+ "colSubject": "科目",
+ "colTime": "时间",
+ "colCourse": "课程",
+ "weekdayMon": "周一",
+ "weekdayTue": "周二",
+ "weekdayWed": "周三",
+ "weekdayThu": "周四",
+ "weekdayFri": "周五",
+ "weekdaySat": "周六",
+ "weekdaySun": "周日",
"notFound": "未找到课程"
}
},
@@ -2041,11 +4476,24 @@
"title": "课程计划",
"description": "查看课程计划列表",
"total": "共 {count} 条",
+ "searchPlaceholder": "搜索计划名称/学科/班级/教师...",
+ "filterByStatus": "按状态筛选",
+ "statusAll": "全部状态",
+ "statusActive": "进行中",
+ "statusCompleted": "已完成",
+ "statusPlanning": "计划中",
+ "statusPaused": "已暂停",
"colTitle": "计划名称",
"colSubject": "学科",
"colGrade": "年级",
"colStatus": "状态",
"colActions": "操作",
+ "colClass": "班级",
+ "colTeacher": "教师",
+ "colSemester": "学期",
+ "colCreated": "创建时间",
+ "fieldProgress": "完成进度",
+ "progressHours": "{completed} / {total} 学时({percent}%)",
"viewDetail": "查看详情 →",
"emptyTitle": "暂无课程计划",
"mswNotice": "学生课程计划列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
@@ -2053,13 +4501,49 @@
"detail": {
"title": "课程计划详情",
"backToList": "返回列表",
+ "notFound": "未找到课程计划",
+ "mswNotice": "详情查询契约待补齐(@contract-pending)。",
+ "headerTitle": "课程计划详情",
+ "badgeClass": "班级",
+ "badgeSubject": "学科",
+ "badgeStatus": "状态",
+ "badgeSemester": "学期",
+ "badgeNoClass": "未分班",
+ "badgeUnknownSubject": "未知学科",
+ "fieldTeacher": "教师",
+ "fieldUnassigned": "未分配",
+ "fieldCreatedAt": "创建于 {date}",
+ "fieldStartDate": "开始于 {date}",
+ "fieldEndDate": "结束于 {date}",
+ "fieldSemester": "第 {semester} 学期",
"sectionBasic": "基本信息",
"fieldTitle": "标题",
"fieldSubject": "学科",
"fieldGrade": "年级",
"fieldDescription": "描述",
"sectionTextbooks": "关联教材",
- "notFound": "未找到课程计划"
+ "sectionProgress": "完成进度",
+ "progressLabel": "完成进度",
+ "progressHours": "{completed} / {total} 学时({percent}%)",
+ "progressItems": "{completed} / {total} 周计划已完成",
+ "sectionSyllabus": "教学大纲",
+ "sectionObjectives": "教学目标",
+ "emptyText": "暂无",
+ "sectionWeekPlans": "周计划",
+ "emptyWeekPlans": "暂无周计划",
+ "colWeek": "周次",
+ "colTopic": "主题",
+ "colHours": "学时",
+ "colChapter": "章节",
+ "colStatus": "状态",
+ "statusPlanned": "已计划",
+ "statusInProgress": "进行中",
+ "statusCompleted": "已完成",
+ "statusSkipped": "已跳过",
+ "statusUnknown": "未知",
+ "exportCsv": "导出 CSV",
+ "toastExported": "导出成功",
+ "toastExportFailed": "导出失败"
}
},
"lessonPlans": {
@@ -2073,15 +4557,23 @@
"colUpdatedAt": "更新时间",
"colActions": "操作",
"viewDetail": "查看教案 →",
+ "subjectFilter": "按学科筛选",
+ "allSubjects": "全部学科",
"emptyTitle": "暂无教案",
"mswNotice": "学生教案列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
},
"view": {
"title": "教案查看",
"backToList": "返回列表",
+ "sectionBasic": "基本信息",
"sectionContent": "教案内容",
+ "fieldSubject": "学科",
+ "fieldGrade": "年级",
+ "fieldTextbook": "教材",
+ "fieldChapter": "章节",
"notFound": "未找到教案或教案未发布",
- "outOfScope": "该教案不在你的年级范围内"
+ "outOfScope": "该教案不在你的年级范围内",
+ "notPublished": "该教案尚未发布,无法查看"
}
},
"textbooks": {
@@ -2100,14 +4592,43 @@
"colActions": "操作",
"viewChapters": "查看章节 →",
"emptyTitle": "暂无教材",
- "mswNotice": "学生教材列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "emptyFilteredTitle": "未找到匹配的教材",
+ "emptyFilteredDescription": "请尝试调整筛选条件或清除筛选后重试。",
+ "clearFilters": "清除筛选",
+ "gradeNotSetTitle": "未设置学生年级",
+ "gradeNotSetDescription": "尚未关联活跃班级,无法按年级筛选教材,请联系老师或管理员补全班级信息。",
+ "mswNotice": "学生教材列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "subjects": {
+ "chinese": "语文",
+ "math": "数学",
+ "english": "英语",
+ "physics": "物理",
+ "chemistry": "化学",
+ "biology": "生物"
+ },
+ "grades": {
+ "grade1": "一年级",
+ "grade2": "二年级",
+ "grade3": "三年级",
+ "grade7": "七年级",
+ "grade8": "八年级",
+ "grade9": "九年级"
+ }
},
"chapters": {
"title": "教材阅读器",
"backToList": "返回教材列表",
"sectionChapters": "章节列表",
"sectionContent": "阅读区",
- "notFound": "未找到教材或教材年级不匹配"
+ "notFound": "未找到教材或教材年级不匹配",
+ "mswNotice": "章节内容契约待补齐,当前通过 MSW 兜底。",
+ "subjectBadge": "学科",
+ "gradeBadge": "年级",
+ "gradeMismatchWarning": "教材年级({grade})与你的年级不匹配,仅作参考阅读",
+ "prevChapter": "上一章",
+ "nextChapter": "下一章",
+ "emptyChapters": "暂无章节内容",
+ "chapterLabel": "第 {n} 章"
}
},
"errorBook": {
@@ -2130,6 +4651,7 @@
"sourceFilter": "按来源筛选",
"allSources": "全部来源",
"dueOnlyFilter": "仅看待复习",
+ "resetFilters": "重置",
"total": "共 {count} 条",
"colQuestion": "题目",
"colSubject": "学科",
@@ -2140,8 +4662,44 @@
"colActions": "操作",
"markMastered": "标记已掌握",
"emptyTitle": "暂无错题",
+ "emptyDescription": "调整筛选条件或录入第一道错题",
"markSuccess": "已标记为已掌握",
- "markError": "标记失败"
+ "markError": "标记失败",
+ "fieldTotalDesc": "全部错题记录",
+ "fieldNewDesc": "尚未开始复习",
+ "fieldLearningDesc": "复习中尚未掌握",
+ "fieldMasteredDesc": "掌握率 {rate}%",
+ "fieldToReviewDesc": "已到复习节点",
+ "sourceExam": "考试",
+ "sourceHomework": "作业",
+ "sourcePractice": "练习",
+ "sourceManual": "手动添加",
+ "statusArchived": "已归档",
+ "difficultyEasy": "简单",
+ "difficultyMedium": "中等",
+ "difficultyHard": "困难",
+ "viewCard": "卡片视图",
+ "viewTable": "表格视图",
+ "reviewCount": "复习次数",
+ "masteryLevel": "掌握度",
+ "nextReviewAt": "下次复习",
+ "overdue": "逾期",
+ "viewDetail": "查看详情",
+ "detailTitle": "错题详情",
+ "close": "关闭",
+ "correctAnswer": "正确答案",
+ "note": "笔记",
+ "editNote": "编辑笔记",
+ "createdAt": "创建时间",
+ "variantPractice": "变式练习",
+ "practiceStarted": "练习已开始",
+ "practiceStartError": "开始练习失败",
+ "noteEditPending": "笔记编辑功能待开放",
+ "aiAnalysisTitle": "AI 分析",
+ "aiErrorCategory": "错因分类",
+ "aiKnowledgePoint": "关联知识点",
+ "aiLearningPath": "学习路径建议",
+ "aiLearningPathDesc": "建议结合变式练习巩固薄弱知识点"
},
"learning": {
"title": "学习中心",
@@ -2157,7 +4715,12 @@
"cardErrorBook": "错题本",
"cardErrorBookDesc": "待复习错题",
"enter": "进入 →",
- "emptyTitle": "暂无学习资源"
+ "emptyTitle": "暂无学习资源",
+ "statCourses": "已报名 {count} 个班级",
+ "statHomework": "待交 {pending} · 即将到期 {dueSoon}",
+ "statTextbooks": "可用 {count} 本教材",
+ "noStudentTitle": "未找到学生身份",
+ "noStudentDesc": "请确认你已加入班级或联系管理员"
},
"learningPath": {
"title": "学习路径",
@@ -2192,7 +4755,28 @@
"colActions": "操作",
"viewDetail": "查看详情 →",
"emptyTitle": "暂无练习记录",
- "mswNotice": "学生练习查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "starterDescription": "选择知识点与题目数量开始练习",
+ "starterQuestionCount": "题目数量",
+ "starterCreating": "创建中...",
+ "starterSelectKnowledgePoint": "请至少选择一个知识点",
+ "starterCreated": "练习会话已创建",
+ "starterCreateFailed": "创建失败:{message}",
+ "mswNotice": "学生练习查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "knowledgePointLoading": "知识点加载中...",
+ "knowledgePointLoadFailed": "知识点加载失败:{message}",
+ "knowledgePointEmpty": "暂无可选知识点",
+ "subjectFilterAll": "全部学科",
+ "subjectFilterLabel": "学科筛选",
+ "selectedCount": "已选 {count} 个知识点",
+ "difficultyEasy": "简单",
+ "difficultyMedium": "中等",
+ "difficultyHard": "困难",
+ "subjectMath": "数学",
+ "subjectChinese": "语文",
+ "subjectEnglish": "英语",
+ "subjectPhysics": "物理",
+ "subjectChemistry": "化学",
+ "knowledgePointDescription": "描述"
},
"session": {
"title": "练习会话",
@@ -2225,6 +4809,37 @@
"enroll": "选课",
"drop": "退课",
"viewDetail": "查看详情 →",
+ "filterByStatus": "状态筛选",
+ "filterBySelectionMode": "选课模式筛选",
+ "statusAll": "全部状态",
+ "statusOpen": "开放中",
+ "statusClosed": "已关闭",
+ "statusInProgress": "进行中",
+ "statusCompleted": "已结束",
+ "modeAll": "全部模式",
+ "modeFcfs": "先到先得",
+ "modeLottery": "抽签",
+ "badgeStatusOpen": "开放中",
+ "badgeStatusClosed": "已关闭",
+ "badgeStatusInProgress": "进行中",
+ "badgeStatusCompleted": "已结束",
+ "badgeModeFcfs": "先到先得",
+ "badgeModeLottery": "抽签",
+ "fieldSubject": "学科",
+ "fieldSchedule": "上课时间",
+ "fieldCredits": "学分",
+ "fieldCapacity": "容量",
+ "fieldEnrolledCount": "报名情况",
+ "fieldCategory": "分类",
+ "capacityFull": "已满",
+ "enrolledCount": "{enrolled}/{capacity}",
+ "dropDialogTitle": "确认退课",
+ "dropDialogDescription": "确认要退选 {courseName} 吗?退课后无法恢复。",
+ "dropDialogReasonLabel": "退课原因(可选)",
+ "dropDialogReasonPlaceholder": "请填写退课原因...",
+ "dropDialogCancel": "取消",
+ "dropDialogConfirm": "确认退课",
+ "dropDialogLoading": "处理中...",
"emptyMySelections": "暂无已选课程",
"emptyAvailable": "暂无可选课程",
"enrollSuccess": "选课成功",
@@ -2237,14 +4852,30 @@
"title": "选课详情",
"backToList": "返回选课列表",
"sectionBasic": "课程信息",
+ "sectionDescription": "课程描述",
+ "sectionSchedule": "上课时间",
"fieldName": "名称",
"fieldTeacher": "教师",
"fieldCapacity": "容量",
"fieldEnrolled": "已选",
- "fieldSchedule": "上课时间",
"fieldCredits": "学分",
"fieldCategory": "分类",
- "notFound": "未找到课程"
+ "fieldSubject": "学科",
+ "fieldGrade": "年级",
+ "fieldClassroom": "教室",
+ "fieldSelectionMode": "选课模式",
+ "fieldStartDate": "开始日期",
+ "fieldEndDate": "结束日期",
+ "fieldSelectionStartAt": "选课开始时间",
+ "fieldSelectionEndAt": "选课结束时间",
+ "statusDraft": "草稿",
+ "statusOpen": "开放中",
+ "statusClosed": "已关闭",
+ "statusCancelled": "已取消",
+ "selectionModeFcfs": "先到先得",
+ "selectionModeLottery": "抽签",
+ "notFound": "未找到课程",
+ "mswNotice": "选课详情查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
}
},
"leave": {
@@ -2274,7 +4905,16 @@
"statusRejected": "已驳回",
"emptyTitle": "暂无请假记录",
"noActiveClass": "暂无活跃班级,无法提交请假申请",
- "mswNotice": "学生请假查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "backToDashboard": "返回学生主页",
+ "mswNotice": "学生请假查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "validation": {
+ "selectClass": "请选择班级",
+ "selectStartDate": "请选择开始日期",
+ "selectEndDate": "请选择结束日期",
+ "selectType": "请选择请假类型",
+ "fillReason": "请填写请假原因",
+ "endDateBeforeStart": "结束日期不能早于开始日期"
+ }
},
"aiTutor": {
"title": "AI 辅导",
@@ -2334,6 +4974,110 @@
"retry": "重试"
}
},
+ "shared": {
+ "profile": {
+ "title": "个人资料",
+ "description": "查看个人资料与角色专属概览",
+ "editProfile": "编辑资料",
+ "sectionPersonal": "个人信息",
+ "sectionAccount": "账户信息",
+ "sectionRoleOverview": "角色概览",
+ "fieldName": "姓名",
+ "fieldGender": "性别",
+ "fieldAge": "年龄",
+ "fieldPhone": "电话",
+ "fieldAddress": "地址",
+ "fieldEmail": "邮箱",
+ "fieldRole": "角色",
+ "fieldCreatedAt": "注册时间",
+ "fieldOnboardedAt": "入职时间",
+ "fieldClassName": "班级",
+ "fieldGrade": "年级",
+ "fieldHeadTeacher": "班主任",
+ "fieldAvgScore": "平均分",
+ "fieldClassRank": "班级排名",
+ "fieldClassCount": "班级数",
+ "fieldStudentCount": "学生数",
+ "fieldCourses": "教授课程",
+ "rankValue": "第 {rank} / {total} 名",
+ "genderMale": "男",
+ "genderFemale": "女",
+ "genderOther": "其他",
+ "roleAdmin": "管理员",
+ "roleTeacher": "教师",
+ "roleStudent": "学生",
+ "roleParent": "家长",
+ "noRoleOverview": "当前角色暂无专属概览信息",
+ "mswNotice": "用户资料查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ },
+ "messages": {
+ "detail": {
+ "title": "消息详情",
+ "backToList": "返回消息列表",
+ "reply": "回复",
+ "delete": "删除",
+ "deleteConfirm": "确认删除这条消息吗?",
+ "deleteSuccess": "已删除",
+ "deleteError": "删除失败",
+ "cancel": "取消",
+ "fieldFrom": "发件人",
+ "fieldTo": "收件人",
+ "fieldSubject": "主题",
+ "fieldDate": "日期",
+ "fieldStatus": "状态",
+ "fieldBody": "正文",
+ "fieldStarred": "星标",
+ "statusRead": "已读",
+ "statusUnread": "未读",
+ "starredYes": "是",
+ "starredNo": "否",
+ "notFound": "未找到消息",
+ "mswNotice": "消息详情查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ },
+ "compose": {
+ "title": "撰写消息",
+ "description": "填写收件人、主题与正文",
+ "backToList": "返回消息列表",
+ "fieldTo": "收件人",
+ "fieldSubject": "主题",
+ "fieldBody": "正文",
+ "toPlaceholder": "请选择收件人",
+ "subjectPlaceholder": "请输入主题",
+ "bodyPlaceholder": "请输入正文",
+ "send": "发送",
+ "sending": "发送中...",
+ "cancel": "取消",
+ "selectRecipient": "请选择收件人",
+ "subjectRequired": "主题不能为空",
+ "bodyRequired": "正文不能为空",
+ "sendSuccess": "发送成功",
+ "sendError": "发送失败,请重试",
+ "noRecipients": "暂无可用收件人",
+ "mswNotice": "收件人列表与发送消息契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ },
+ "groupCompose": {
+ "title": "群发消息",
+ "description": "选择多位收件人发送同一条消息",
+ "backToList": "返回消息列表",
+ "fieldTo": "收件人(多选)",
+ "fieldSubject": "主题",
+ "fieldBody": "正文",
+ "subjectPlaceholder": "请输入主题",
+ "bodyPlaceholder": "请输入正文",
+ "send": "发送",
+ "sending": "发送中...",
+ "cancel": "取消",
+ "selectAtLeastOne": "请至少选择一位收件人",
+ "subjectRequired": "主题不能为空",
+ "bodyRequired": "正文不能为空",
+ "sendSuccess": "群发成功({count} 人)",
+ "sendError": "群发失败,请重试",
+ "noRecipients": "暂无可用收件人",
+ "selectedCount": "已选 {count} 人",
+ "mswNotice": "收件人列表与发送消息契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ }
+ }
+ },
"admin": {
"users": {
"list": {
@@ -2346,10 +5090,16 @@
"colName": "姓名",
"colEmail": "邮箱",
"colRole": "角色",
+ "colPhone": "电话",
"colStatus": "状态",
"colCreatedAt": "创建时间",
+ "colUpdatedAt": "更新时间",
+ "colUserType": "用户类型",
"colActions": "操作",
"editRole": "修改角色",
+ "editUser": "编辑用户",
+ "userTypeInternal": "内部",
+ "userTypeExternal": "外部",
"activate": "启用",
"deactivate": "停用",
"emptyTitle": "暂无用户",
@@ -2383,7 +5133,7 @@
"required": "必填",
"optional": "可选",
"uploadButton": "选择文件上传",
- "uploadHint": "支持 .csv 文件,单次最多 500 条",
+ "uploadHint": "支持 .csv / .xlsx / .xls 文件,单次最多 500 条",
"noticeTitle": "注意事项",
"notice1": "邮箱格式必须正确",
"notice2": "角色只能是 teacher / student / parent / admin",
@@ -2395,7 +5145,26 @@
"resultTotal": "总行数",
"resultSuccess": "成功",
"resultFailed": "失败",
- "resultErrors": "错误明细"
+ "resultErrors": "错误明细",
+ "filesPreview": "文件预览(前 {count} 行)",
+ "importStatus": "导入中...",
+ "reselect": "重新选择",
+ "confirmImport": "确认导入"
+ },
+ "assignDialog": {
+ "title": "分配角色",
+ "description": "为 {name} 分配角色,保存后将立即生效。",
+ "listAriaLabel": "{name} 的可分配角色列表",
+ "noEnabledRoles": "暂无可分配的角色",
+ "system": "系统",
+ "locked": "锁定",
+ "disabledAssignedLabel": "以下角色已禁用但仍分配给该用户:",
+ "disabledLabel": "已禁用",
+ "save": "保存",
+ "saving": "保存中…",
+ "success": "角色已分配",
+ "adminWarningTitle": "管理员角色",
+ "adminWarning": "管理员拥有系统最高权限,分配后该用户将获得全部管理能力,请谨慎操作。"
},
"error": {
"title": "用户模块出错了",
@@ -2409,17 +5178,38 @@
"description": "管理角色与权限分配",
"newRole": "新建角色",
"createButton": "新建角色",
+ "totalBadge": "共 {count} 个角色",
"searchPlaceholder": "搜索角色名称/描述...",
+ "tableCaption": "角色列表,包含名称、描述、类型、状态、用户数、权限数、更新时间与操作",
"colName": "角色名称",
"colDescription": "描述",
"colIsLocked": "系统锁定",
+ "colType": "类型",
+ "colStatus": "状态",
+ "colUserCount": "用户数",
"colPermissions": "权限数",
+ "colValue": "角色值",
+ "colUpdatedAt": "更新时间",
"colActions": "操作",
"viewDetail": "查看详情",
"editRole": "编辑",
"editPermissions": "编辑权限",
"deleteRole": "删除",
+ "enableRole": "启用",
+ "disableRole": "停用",
+ "enabled": "已启用",
+ "disabled": "已停用",
+ "userCountValue": "{count} 个",
+ "typeSystem": "系统",
+ "typeCustom": "自定义",
+ "deleteConfirmTitle": "确认删除角色",
+ "deleteConfirmDescription": "确定要删除角色「{name}」吗?该角色当前关联 {count} 个用户,删除后这些用户将失去对应权限。此操作不可撤销。",
+ "confirmDelete": "确认删除",
+ "deleted": "角色已删除",
+ "enabledSuccess": "角色已启用",
+ "disabledSuccess": "角色已停用",
"lockedRole": "系统角色(不可删除)",
+ "lockedRoleToggleWarn": "系统锁定角色不支持启停切换",
"emptyTitle": "暂无角色",
"emptyDescription": "新建第一个角色以开始管理权限",
"emptyAction": "新建角色",
@@ -2429,16 +5219,19 @@
"detail": {
"title": "角色详情",
"edit": "编辑",
+ "editRole": "编辑角色",
"notFound": "未找到该角色",
"backToList": "返回角色列表",
"sectionBasic": "基本信息",
"sectionPermissions": "权限矩阵",
"lockedNotice": "此为系统内置角色,部分字段不可修改",
+ "adminLockedWarn": "admin 是系统最高权限角色,对其权限的修改可能影响系统整体可用性。请谨慎调整。",
"fieldName": "角色名称",
"fieldDescription": "描述",
"fieldIsLocked": "系统锁定",
"fieldPermissions": "权限列表",
- "noPermissions": "该角色暂未分配任何权限"
+ "noPermissions": "该角色暂未分配任何权限",
+ "sectionPermissionMatrix": "权限操作矩阵"
},
"form": {
"titleCreate": "新建角色",
@@ -2447,7 +5240,50 @@
"fieldDescription": "角色描述",
"fieldPermissions": "权限分配",
"submit": "保存",
- "cancel": "取消"
+ "cancel": "取消",
+ "namePatternTitle": "只允许小写字母、数字与下划线"
+ },
+ "createDialog": {
+ "titleCreate": "新建角色",
+ "titleEdit": "编辑角色",
+ "descriptionCreate": "创建一个新角色,可在创建后通过权限矩阵分配具体操作权限。",
+ "descriptionEdit": "修改角色名称与描述。权限点请在权限操作矩阵中调整。",
+ "fieldName": "角色名称",
+ "fieldNamePlaceholder": "如:班主任、年级组长",
+ "fieldDescription": "角色描述",
+ "fieldDescriptionPlaceholder": "简要描述该角色的职责范围(可选)",
+ "fieldValue": "角色值",
+ "fieldValuePlaceholder": "如:role:teacher",
+ "lockedNameNotice": "系统内置角色名称不可修改",
+ "cancel": "取消",
+ "submitting": "提交中…",
+ "submitCreate": "创建角色",
+ "submitEdit": "保存修改",
+ "successCreated": "角色创建成功",
+ "successUpdated": "角色已更新",
+ "errorNameRequired": "请填写角色名称",
+ "namePatternTitle": "只允许小写字母、数字与下划线",
+ "errorNamePattern": "角色名称只允许小写字母、数字与下划线"
+ },
+ "matrix": {
+ "title": "{roleName} · 权限操作矩阵",
+ "locked": "已锁定",
+ "permissionCount": "共 {count} 个权限点",
+ "loading": "正在加载权限矩阵...",
+ "empty": "该角色暂无可配置的权限点",
+ "colPermission": "权限点",
+ "actionRead": "查看",
+ "actionCreate": "新建",
+ "actionUpdate": "编辑",
+ "actionDelete": "删除",
+ "moduleLabel": "模块:{module}",
+ "save": "保存矩阵",
+ "saving": "保存中…",
+ "successSaved": "权限矩阵已保存",
+ "searchPlaceholder": "搜索权限点/模块...",
+ "collapse": "收起",
+ "expand": "展开",
+ "userImpactNotice": "该角色当前关联 {count} 个用户,调整权限将立即影响这些用户。"
},
"error": {
"title": "角色模块出错了",
@@ -2464,8 +5300,11 @@
"colPermission": "权限点",
"colResource": "资源",
"colAction": "动作",
+ "colValue": "权限值",
+ "colKey": "标识",
"colRoleCount": "关联角色数",
"groupResource": "按资源分组",
+ "countLabel": "共 {count} 项",
"emptyTitle": "暂无权限点",
"emptyDescription": "权限目录将由 IAM 服务同步后展示",
"mswNotice": "权限目录查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
@@ -2496,6 +5335,9 @@
"allStatuses": "全部状态",
"export": "导出 CSV",
"exportCsv": "导出 CSV",
+ "exportSuccess": "成功导出 {count} 条记录",
+ "exportEmpty": "没有可导出的数据",
+ "exportFailed": "导出失败:{message}",
"colTimestamp": "时间",
"colUser": "用户",
"colUserId": "用户 ID",
@@ -2507,7 +5349,10 @@
"colResourceId": "资源 ID",
"colIp": "IP",
"colDetails": "详情",
+ "colActions": "操作",
"total": "共 {count} 条",
+ "resetFilter": "重置",
+ "pageOf": "第 {page} / {totalPages} 页",
"emptyTitle": "暂无审计日志",
"emptyDescription": "调整筛选条件或更换时间范围后重试",
"mswNotice": "审计日志查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
@@ -2526,6 +5371,19 @@
"statsToday": "今日日志",
"statsErrors": "错误日志",
"statsUsers": "活跃用户",
+ "auditEventsToday": "今日审计事件",
+ "failedLoginsToday": "今日失败登录",
+ "dataChangesToday": "今日数据变更",
+ "totalAuditLogs": "审计日志总数",
+ "sectionError": "区块加载失败",
+ "sectionRetry": "重试",
+ "chartEmpty": "暂无数据",
+ "quickLinkAuditLogs": "审计日志",
+ "quickLinkAuditLogsDesc": "查看所有用户操作审计记录",
+ "quickLinkLoginLogs": "登录日志",
+ "quickLinkLoginLogsDesc": "查看登录/登出/注册记录",
+ "quickLinkDataChanges": "数据变更",
+ "quickLinkDataChangesDesc": "查看数据表变更记录与统计",
"trendTitle": "近 7 天趋势",
"trendLast7Days": "近 7 天",
"distributionTitle": "数据变更动作分布",
@@ -2543,6 +5401,9 @@
"filterAction": "按动作筛选",
"filterStatus": "按状态筛选",
"filterUser": "按用户筛选",
+ "filterDateRange": "时间范围",
+ "startDate": "开始日期",
+ "endDate": "结束日期",
"actionFilter": "按动作筛选",
"statusFilter": "按状态筛选",
"allActions": "全部动作",
@@ -2554,15 +5415,22 @@
"statusFailure": "失败",
"export": "导出 CSV",
"exportCsv": "导出 CSV",
+ "exportSuccess": "成功导出 {count} 条记录",
+ "exportEmpty": "没有可导出的数据",
+ "exportFailed": "导出失败:{message}",
"colTimestamp": "时间",
"colUser": "用户",
"colUserId": "用户 ID",
"colUserName": "用户名",
"colAction": "动作",
"colStatus": "状态",
+ "colErrorMessage": "失败原因",
"colIp": "IP",
"colUserAgent": "User Agent",
+ "colActions": "操作",
"total": "共 {count} 条",
+ "resetFilter": "重置",
+ "pageOf": "第 {page} / {totalPages} 页",
"emptyTitle": "暂无登录日志",
"emptyDescription": "调整筛选条件后重试",
"mswNotice": "登录日志查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
@@ -2574,6 +5442,9 @@
"filterTable": "按表筛选",
"filterAction": "按动作筛选",
"filterUser": "按用户筛选",
+ "filterDateRange": "时间范围",
+ "startDate": "开始日期",
+ "endDate": "结束日期",
"tableFilter": "按表筛选",
"actionFilter": "按动作筛选",
"allTables": "全部表",
@@ -2583,11 +5454,19 @@
"actionDelete": "删除",
"export": "导出 CSV",
"exportCsv": "导出 CSV",
+ "exportSuccess": "成功导出 {count} 条记录",
+ "exportEmpty": "没有可导出的数据",
+ "exportFailed": "导出失败:{message}",
"sectionStats": "变更统计",
"statsTitle": "变更统计",
"statsAction": "动作",
"statsCount": "次数",
"statsLastChange": "最近变更",
+ "topTables": "热门表 Top 8",
+ "expandDetail": "查看变更对比",
+ "collapseDetail": "收起变更对比",
+ "oldValue": "变更前",
+ "newValue": "变更后",
"colTimestamp": "时间",
"colTable": "表名",
"colRecordId": "记录 ID",
@@ -2596,11 +5475,51 @@
"colUserId": "用户 ID",
"colUserName": "用户名",
"colChanges": "变更内容",
+ "colActions": "操作",
"total": "共 {count} 条",
+ "resetFilter": "重置",
+ "pageOf": "第 {page} / {totalPages} 页",
"emptyTitle": "暂无数据变更日志",
"emptyDescription": "调整筛选条件后重试",
"mswNotice": "数据变更日志查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
},
+ "detail": {
+ "title": "审计日志详情",
+ "description": "查看审计日志详细信息",
+ "viewDetail": "查看详情",
+ "userId": "用户 ID",
+ "userName": "用户名",
+ "action": "动作",
+ "module": "模块",
+ "resourceId": "资源 ID",
+ "tableName": "表名",
+ "recordId": "记录 ID",
+ "changes": "变更内容",
+ "oldValue": "变更前",
+ "newValue": "变更后",
+ "errorMessage": "失败原因",
+ "ipAddress": "IP 地址",
+ "userAgent": "User Agent",
+ "status": "状态",
+ "details": "详情",
+ "createdAt": "创建时间"
+ },
+ "retention": {
+ "title": "审计日志保留期设置",
+ "description": "配置审计日志与登录日志的保留天数及自动清理",
+ "retentionDays": "审计日志保留天数",
+ "retentionDaysDescription": "范围 7-3650 天,默认 90 天",
+ "loginLogRetentionDays": "登录日志保留天数",
+ "loginLogRetentionDaysDescription": "范围 7-3650 天,默认 365 天",
+ "autoCleanupEnabled": "启用自动清理",
+ "autoCleanupEnabledDescription": "开启后系统将按保留天数自动清理过期日志",
+ "save": "保存配置",
+ "purge": "立即清理",
+ "saveSuccess": "保留期配置已保存",
+ "purgeConfirm": "确定要立即清理过期日志吗?此操作不可撤销。",
+ "purgeSuccess": "已清理:审计日志 {auditLogsDeleted} 条,登录日志 {loginLogsDeleted} 条,数据变更日志 {dataChangeLogsDeleted} 条",
+ "loadFailed": "加载保留期配置失败"
+ },
"error": {
"title": "审计日志模块出错了",
"unknown": "审计日志模块发生未知错误",
@@ -2627,6 +5546,10 @@
"copyCode": "复制",
"allStatuses": "全部状态",
"total": "共 {count} 条",
+ "statsTotal": "总邀请码数",
+ "statsUsed": "已使用数",
+ "statsUnused": "未使用数",
+ "statsExpired": "已过期数",
"statusActive": "有效",
"statusUnused": "未使用",
"statusUsed": "已用完",
@@ -2635,7 +5558,12 @@
"emptyTitle": "暂无邀请码",
"emptyDescription": "生成第一个邀请码以邀请用户加入",
"emptyAction": "生成邀请码",
- "mswNotice": "邀请码列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "邀请码列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "deleteSelected": "删除选中 ({count})",
+ "deleteSuccess": "已删除 {count} 个邀请码",
+ "deleteConfirmTitle": "确认批量删除",
+ "deleteConfirmDesc": "即将删除 {count} 个邀请码,此操作不可撤销。",
+ "confirmDelete": "确认删除"
},
"generateForm": {
"title": "生成邀请码",
@@ -2645,6 +5573,41 @@
"submit": "生成",
"cancel": "取消"
},
+ "generateDialog": {
+ "title": "批量生成邀请码",
+ "description": "一次生成多个邀请码,可用于批量邀请教师、学生或家长加入系统。",
+ "fieldBatchName": "批次名",
+ "fieldBatchNamePlaceholder": "如:2026秋季新教师批次(可选)",
+ "fieldBatchNameHint": "用于标识本次生成批次,便于后续管理",
+ "fieldCount": "生成数量",
+ "countHint": "单次最多生成 100 个",
+ "fieldRole": "角色",
+ "roles": {
+ "teacher": "教师",
+ "student": "学生",
+ "parent": "家长",
+ "admin": "管理员"
+ },
+ "fieldExpireDays": "有效期(天)",
+ "expireDaysOption": "{days} 天",
+ "fieldPurpose": "用途",
+ "fieldPurposePlaceholder": "如:新教师入职邀请(可选)",
+ "fieldPurposeHint": "说明本次生成邀请码的用途,不超过 500 字",
+ "cancel": "取消",
+ "submit": "生成",
+ "generating": "生成中…",
+ "success": "已成功生成 {count} 个邀请码",
+ "errorCountRange": "生成数量需在 1 至 100 之间",
+ "resultTitle": "生成结果",
+ "resultDescription": "请及时复制或下载生成的邀请码,关闭后无法再次查看明文。",
+ "statsSuccess": "成功生成",
+ "statsFailed": "失败数量",
+ "copyCode": "复制",
+ "copyAll": "全部复制",
+ "copyFailed": "复制失败",
+ "downloadCsv": "下载 CSV",
+ "done": "完成"
+ },
"error": {
"title": "邀请码模块出错了",
"unknown": "邀请码模块发生未知错误",
@@ -2773,7 +5736,8 @@
"fieldCurrentYear": "当前学年",
"fieldCurrentTerm": "当前学期",
"submit": "保存",
- "cancel": "取消"
+ "cancel": "取消",
+ "fieldCode": "学校代码"
},
"deleteConfirm": {
"title": "删除学校",
@@ -2785,7 +5749,9 @@
"title": "学校模块出错了",
"unknown": "学校模块发生未知错误",
"retry": "重试"
- }
+ },
+ "colCode": "学校代码",
+ "colUpdatedAt": "更新时间"
},
"classes": {
"title": "班级管理",
@@ -2815,6 +5781,111 @@
"title": "班级模块出错了",
"unknown": "班级模块发生未知错误",
"retry": "重试"
+ },
+ "manageSchedule": "管理课表",
+ "manageInvitation": "邀请码",
+ "schedule": {
+ "form": {
+ "titleCreate": "新增课表条目",
+ "titleEdit": "编辑课表条目",
+ "classLabel": "班级",
+ "fieldWeekday": "星期",
+ "fieldPeriod": "节次",
+ "fieldSubject": "学科",
+ "fieldTeacher": "教师",
+ "fieldClassroom": "教室",
+ "fieldStartTime": "开始时间",
+ "fieldEndTime": "结束时间",
+ "subjectPlaceholder": "如:数学",
+ "teacherPlaceholder": "如:张老师",
+ "classroomPlaceholder": "如:301 教室",
+ "periodN": "第 {n} 节",
+ "save": "保存",
+ "create": "新增",
+ "createSuccess": "课表条目创建成功",
+ "editSuccess": "课表条目更新成功",
+ "deleteTitle": "删除课表条目",
+ "deleteMessage": "确认删除 {weekday} 第 {period} 节 {subject}?",
+ "deleteConfirm": "删除",
+ "deleteSuccess": "课表条目删除成功"
+ },
+ "manager": {
+ "title": "课表管理",
+ "add": "新增条目",
+ "colPeriod": "节次",
+ "colSubject": "学科",
+ "colTeacher": "教师",
+ "colClassroom": "教室",
+ "colTime": "时间",
+ "colActions": "操作",
+ "empty": "暂无课表条目"
+ },
+ "weekday": {
+ "1": "周一",
+ "2": "周二",
+ "3": "周三",
+ "4": "周四",
+ "5": "周五",
+ "6": "周六",
+ "7": "周日"
+ }
+ },
+ "invitation": {
+ "title": "邀请码管理",
+ "classLabel": "班级",
+ "generate": "生成邀请码",
+ "generateWithCustom": "自定义邀请码",
+ "generateSuccess": "邀请码生成成功",
+ "defaultDuration": "留空表示不限制有效期",
+ "defaultMaxUses": "留空表示不限制使用次数",
+ "expiresInHours": "有效期(小时)",
+ "maxUsesLabel": "最大使用次数",
+ "customNote": "备注",
+ "customNotePlaceholder": "如:暑期班邀请码",
+ "copy": "复制",
+ "copied": "已复制到剪贴板",
+ "revoke": "撤销",
+ "revokeSuccess": "邀请码已撤销",
+ "revokeConfirm": "确认撤销此邀请码?撤销后无法恢复。",
+ "neverExpires": "永不过期",
+ "empty": "暂无邀请码",
+ "colCode": "邀请码",
+ "colStatus": "状态",
+ "colUsedCount": "使用次数",
+ "colExpiresAt": "过期时间",
+ "colNote": "备注",
+ "colActions": "操作",
+ "status": {
+ "active": "可用",
+ "used": "已用完",
+ "expired": "已过期",
+ "revoked": "已撤销"
+ }
+ },
+ "colHomeroomLabel": "班级标签",
+ "colRoom": "教室",
+ "colSubjectTeachers": "任课教师",
+ "colUpdatedAt": "更新时间",
+ "form": {
+ "titleCreate": "新建班级",
+ "titleEdit": "编辑班级",
+ "fieldName": "班级名称",
+ "fieldSchool": "所属学校",
+ "fieldGrade": "所属年级",
+ "fieldHeadTeacher": "班主任",
+ "fieldHomeroomLabel": "班级标签",
+ "fieldHomeroomLabelPlaceholder": "如:1班、2班",
+ "fieldRoom": "教室",
+ "fieldRoomPlaceholder": "如:301教室",
+ "fieldHomeroom": "班主任",
+ "submit": "保存",
+ "cancel": "取消"
+ },
+ "deleteConfirm": {
+ "title": "删除班级",
+ "message": "确定要删除班级 \"{name}\" 吗?此操作不可撤销。",
+ "confirm": "确认删除",
+ "cancel": "取消"
}
},
"departments": {
@@ -2843,7 +5914,8 @@
"fieldSchool": "所属学校",
"fieldHead": "负责人 ID(可选)",
"submit": "保存",
- "cancel": "取消"
+ "cancel": "取消",
+ "fieldDescription": "描述"
},
"deleteConfirm": {
"title": "删除部门",
@@ -2855,7 +5927,9 @@
"title": "部门模块出错了",
"unknown": "部门模块发生未知错误",
"retry": "重试"
- }
+ },
+ "colDescription": "描述",
+ "colUpdatedAt": "更新时间"
},
"academicYear": {
"title": "学年管理",
@@ -2888,7 +5962,8 @@
"fieldEndDate": "结束日期",
"fieldIsActive": "设为当前激活",
"submit": "保存",
- "cancel": "取消"
+ "cancel": "取消",
+ "fieldIsActiveHint": "激活后该学年将作为当前学年"
},
"deleteConfirm": {
"title": "删除学年",
@@ -2900,7 +5975,10 @@
"title": "学年模块出错了",
"unknown": "学年模块发生未知错误",
"retry": "重试"
- }
+ },
+ "activeYearCardTitle": "当前激活学年",
+ "activeYearCardDescription": "展示当前设为激活的学年信息",
+ "activeYearCardEmpty": "暂无激活学年"
},
"grades": {
"title": "年级管理",
@@ -2946,7 +6024,11 @@
"title": "年级模块出错了",
"unknown": "年级模块发生未知错误",
"retry": "重试"
- }
+ },
+ "insights": "年级洞察",
+ "gradeOverviewSection": "年级概览卡片",
+ "colTeachingHead": "教学主任",
+ "notSet": "未设置"
}
},
"announcements": {
@@ -2963,10 +6045,12 @@
"colAudience": "受众",
"colPinnedAt": "置顶时间",
"colPublishedAt": "发布时间",
+ "colReadCount": "阅读数",
"colAuthor": "作者",
"colActions": "操作",
"viewDetail": "查看",
"edit": "编辑",
+ "publish": "发布",
"archive": "归档",
"pin": "置顶",
"unpin": "取消置顶",
@@ -2976,7 +6060,8 @@
"emptyTitle": "暂无公告",
"emptyDescription": "调整筛选条件或新建第一条公告",
"emptyAction": "新建公告",
- "mswNotice": "公告列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "公告列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "pageIndicator": "第 {page} / {total} 页"
},
"detail": {
"title": "公告详情",
@@ -3003,7 +6088,34 @@
"delete": "删除",
"deleteConfirm": "确认删除该公告?此操作不可撤销。",
"fieldGrades": "年级",
- "fieldClasses": "班级"
+ "fieldClasses": "班级",
+ "fieldReadCount": "阅读数",
+ "deleteConfirmDesc": "此操作不可撤销,公告将被永久删除。"
+ },
+ "create": {
+ "title": "新建公告",
+ "description": "创建一条新的学校公告",
+ "triggerButton": "新建公告",
+ "fieldTitle": "标题",
+ "fieldTitlePlaceholder": "请输入公告标题",
+ "fieldContent": "内容",
+ "fieldContentPlaceholder": "请输入公告内容",
+ "fieldStatus": "状态",
+ "fieldAudience": "受众",
+ "fieldGrades": "关联年级",
+ "fieldGradesDescription": "选择公告关联的年级(可选)",
+ "statusOption_draft": "草稿",
+ "statusOption_published": "已发布",
+ "statusOption_archived": "已归档",
+ "audienceOption_all": "全校",
+ "audienceOption_teachers": "教师",
+ "audienceOption_students": "学生",
+ "audienceOption_parents": "家长",
+ "errorTitleRequired": "请填写标题",
+ "errorContentRequired": "请填写内容",
+ "submit": "创建",
+ "submitting": "创建中…",
+ "submitSuccess": "公告已创建"
},
"form": {
"titleCreate": "新建公告",
@@ -3029,8 +6141,18 @@
"fieldStatus": "状态",
"fieldAudience": "受众",
"fieldPinned": "置顶",
+ "fieldGrades": "关联年级",
+ "fieldGradesHint": "多个年级 ID 用英文逗号分隔(如:grade-1,grade-2)",
"errorTitleRequired": "请填写标题",
- "errorContentRequired": "请填写内容"
+ "errorContentRequired": "请填写内容",
+ "multiSelect": {
+ "placeholder": "请选择年级",
+ "selected": "已选 {count} 项",
+ "toggle": "切换下拉",
+ "loading": "加载中...",
+ "empty": "暂无年级数据",
+ "remove": "移除 {name}"
+ }
},
"error": {
"title": "公告模块出错了",
@@ -3048,6 +6170,13 @@
"statByType": "按类型分布",
"uploadButton": "上传文件",
"searchPlaceholder": "搜索文件名...",
+ "fileTypeFilter": "按文件类型筛选",
+ "allTypes": "全部类型",
+ "typeImage": "图片",
+ "typeDocument": "文档",
+ "typeVideo": "视频",
+ "typeAudio": "音频",
+ "typeOther": "其他",
"colName": "文件名",
"colSize": "大小",
"colMimeType": "类型",
@@ -3061,7 +6190,57 @@
"emptyTitle": "暂无文件",
"emptyDescription": "调整筛选条件或上传新文件",
"emptyAction": "上传文件",
- "mswNotice": "文件列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "文件列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "selectAll": "全选",
+ "selectRow": "选择此行",
+ "statTopType": "热门类型 Top 2",
+ "statTopTypeHint": "按文件数量排序"
+ },
+ "upload": {
+ "button": "上传文件",
+ "uploading": "上传中...",
+ "success": "上传成功",
+ "ariaLabel": "选择文件上传",
+ "dragDrop": "拖拽文件到此处或点击选择",
+ "dropHere": "释放鼠标以上传",
+ "progress": "上传进度 {percent}%",
+ "dragActive": "拖拽激活中",
+ "multiUpload": "已选 {count} 个文件",
+ "remaining": "剩余 {count} 个"
+ },
+ "batch": {
+ "selectAll": "全选",
+ "selectedCount": "已选 {count} 项",
+ "clearSelection": "清空选择",
+ "batchDelete": "批量删除",
+ "confirming": "处理中...",
+ "deleteSuccess": "成功删除 {count} 个文件",
+ "confirmTitle": "确认批量删除",
+ "confirmDescription": "即将删除 {count} 个文件,此操作不可撤销。",
+ "confirmCancel": "取消",
+ "confirmSubmit": "确认删除"
+ },
+ "preview": {
+ "trigger": "预览",
+ "title": "文件预览",
+ "download": "下载",
+ "zoomIn": "放大",
+ "zoomOut": "缩小",
+ "text": {
+ "title": "文本预览",
+ "hint": "点击下方按钮加载文本内容",
+ "load": "加载内容",
+ "loading": "加载中...",
+ "error": "加载失败:{message}"
+ },
+ "office": {
+ "title": "Office 文件暂不支持在线预览",
+ "hint": "请下载后查看"
+ },
+ "other": {
+ "title": "此文件类型暂不支持在线预览",
+ "hint": "请下载后查看"
+ }
},
"error": {
"title": "文件模块出错了",
@@ -3088,8 +6267,15 @@
"edit": "编辑",
"delete": "删除",
"testConnection": "测试连接",
+ "testing": "测试中...",
+ "testSuccess": "连接成功,延迟 {latency} ms",
+ "testFailed": "连接失败:{message}",
"active": "启用",
"inactive": "停用",
+ "colVisibility": "可见性",
+ "colIsDefault": "默认",
+ "defaultProvider": "默认",
+ "nonDefaultProvider": "非默认",
"emptyTitle": "暂无 AI Provider",
"emptyDescription": "新增第一个 AI Provider 以启用 AI 能力",
"emptyAction": "新建 Provider",
@@ -3111,17 +6297,52 @@
"colDate": "日期",
"mswNotice": "AI 配置查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
},
+ "deleteDialog": {
+ "title": "删除 AI Provider",
+ "warning": "此操作不可逆。删除后,Provider \"{name}\" 的所有配置将永久丢失。",
+ "confirmPrompt": "请输入 Provider 名称 \"{name}\" 以确认删除:",
+ "confirmInputPlaceholder": "输入 Provider 名称",
+ "confirmInputHint": "输入的名称必须与上方显示的完全一致。",
+ "cancel": "取消",
+ "confirm": "删除",
+ "deleting": "删除中...",
+ "success": "已删除 Provider \"{name}\""
+ },
"form": {
"titleCreate": "新建 AI Provider",
"titleEdit": "编辑 AI Provider",
+ "description": "配置 AI Provider 连接参数。带 * 的字段为必填。",
"fieldName": "名称",
+ "fieldNamePlaceholder": "如:默认 OpenAI",
"fieldType": "类型",
"fieldScope": "作用域",
+ "fieldScopePlaceholder": "如:global / school_",
"fieldModel": "模型",
+ "fieldModelPlaceholder": "如:gpt-4o-mini",
"fieldApiBase": "API Base",
- "fieldIsActive": "启用",
+ "fieldApiBasePlaceholder": "如:https://api.openai.com/v1(可选)",
+ "fieldApiKey": "API Key",
+ "fieldApiKeyPlaceholder": "输入 API Key",
+ "fieldApiKeyPlaceholderEdit": "留空表示不修改原有 Key",
+ "fieldApiKeyHint": "编辑模式下留空将保留原有 Key,不会清空。",
+ "fieldIsActive": "启用此 Provider",
"submit": "保存",
- "cancel": "取消"
+ "saving": "保存中...",
+ "cancel": "取消",
+ "errorNameRequired": "请填写名称",
+ "errorModelRequired": "请填写模型名称",
+ "errorApiBaseInvalid": "API Base 必须为合法的 http(s) URL",
+ "createSuccess": "AI Provider 已创建",
+ "updateSuccess": "AI Provider 已更新",
+ "fieldVisibility": "可见性",
+ "fieldIsDefault": "设为默认 Provider",
+ "visibilityPrivate": "仅自己可见",
+ "visibilityShared": "组织共享",
+ "visibilityPublic": "全员可见",
+ "testConnection": "测试连接",
+ "testing": "测试中...",
+ "testSuccess": "连接成功,延迟 {latency} ms",
+ "testFailed": "连接失败:{message}"
},
"error": {
"title": "AI 配置模块出错了",
@@ -3387,6 +6608,10 @@
"colCreatedAt": "创建时间",
"colUpdatedAt": "更新时间",
"colActions": "操作",
+ "colSemester": "学期",
+ "colProgress": "进度",
+ "progressHours": "{completed}/{total} 课时",
+ "progressPercent": "{percent}%",
"viewDetail": "查看",
"edit": "编辑",
"emptyTitle": "暂无课程计划",
@@ -3416,7 +6641,77 @@
"fieldUpdatedAt": "更新时间",
"emptySchedule": "暂无教学进度数据",
"emptyGoals": "暂无教学目标",
- "emptyResources": "暂无教学资源"
+ "emptyResources": "暂无教学资源",
+ "weekPlansHint": "共 {count} 条周计划",
+ "addWeekPlan": "新建周计划",
+ "emptyWeekPlans": "暂无周计划数据",
+ "emptyWeekPlansCta": ",点击右上角新建第一条周计划",
+ "colWeek": "周次",
+ "colTopic": "主题",
+ "colHours": "课时",
+ "colChapter": "教材章节",
+ "colStatus": "状态",
+ "colActions": "操作",
+ "statusCompleted": "已完成",
+ "statusPending": "待完成",
+ "notesLabel": "备注:{notes}",
+ "moveUpAria": "上移第 {week} 周",
+ "moveDownAria": "下移第 {week} 周",
+ "editItem": "编辑",
+ "reorderSuccess": "排序已更新",
+ "delete": "删除",
+ "deleteTitle": "删除课程计划",
+ "deleteDescription": "确定要删除此课程计划吗?此操作不可撤销。",
+ "deleteConfirm": "确认删除",
+ "deleteCancel": "取消",
+ "deleteSuccess": "课程计划已删除",
+ "deleteFailed": "删除失败",
+ "fieldSemester": "学期",
+ "fieldSyllabus": "教学大纲",
+ "fieldObjectives": "教学目标",
+ "fieldTotalHours": "总课时",
+ "fieldWeeklyHours": "周课时",
+ "fieldStartDate": "开始日期",
+ "fieldEndDate": "结束日期",
+ "emptySyllabus": "暂无教学大纲",
+ "emptyObjectives": "暂无教学目标",
+ "linkTextbooks": "查看教材",
+ "linkHomework": "查看作业",
+ "bulkSelectAll": "全选",
+ "bulkMarkComplete": "批量标记完成",
+ "bulkMarkIncomplete": "批量取消完成",
+ "bulkSelected": "已选 {count} 项",
+ "bulkClear": "清空选择",
+ "bulkSuccess": "已批量更新 {count} 项",
+ "exportCsv": "导出 CSV",
+ "exportSuccess": "导出成功",
+ "exportFailed": "导出失败"
+ },
+ "itemEditor": {
+ "createTitle": "新建周计划",
+ "editTitle": "编辑周计划",
+ "week": "周次",
+ "hours": "课时",
+ "topic": "主题",
+ "topicPlaceholder": "请输入本周主题",
+ "content": "内容",
+ "contentPlaceholder": "请输入本周教学内容",
+ "chapter": "教材章节",
+ "chapterPlaceholder": "如:第3章",
+ "completedAt": "完成日期",
+ "notes": "备注",
+ "notesPlaceholder": "可选备注信息",
+ "cancel": "取消",
+ "save": "保存",
+ "saving": "保存中...",
+ "delete": "删除",
+ "markComplete": "标记完成",
+ "markIncomplete": "取消完成",
+ "errorTopicRequired": "请填写主题",
+ "createSuccess": "周计划已创建",
+ "updateSuccess": "周计划已更新",
+ "deleteSuccess": "周计划已删除",
+ "toggleSuccess": "完成状态已更新"
},
"create": {
"title": "新建课程计划",
@@ -3443,7 +6738,11 @@
"errorGradeRequired": "请选择年级",
"errorClassRequired": "请选择班级",
"errorSubjectRequired": "请选择科目",
- "contractPending": "课程计划创建契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "contractPending": "课程计划创建契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "fromTemplate": "从模板创建",
+ "fromTemplateHint": "选择模板可快速填充表单字段",
+ "openTemplatePicker": "选择模板",
+ "templateApplied": "已应用模板内容"
},
"edit": {
"title": "编辑课程计划",
@@ -3465,12 +6764,59 @@
"errorClassRequired": "请选择班级",
"errorSubjectRequired": "请选择科目",
"notFound": "未找到该课程计划",
- "backToDetail": "返回详情"
+ "backToDetail": "返回详情",
+ "sectionSchedule": "教学进度",
+ "scheduleNotice": "进度排序为本地编辑,保存时暂不同步到后端",
+ "scheduleEmpty": "暂无教学进度数据",
+ "colWeek": "周次",
+ "colTopic": "主题",
+ "colHours": "课时",
+ "colActions": "操作",
+ "fieldSemester": "学期",
+ "fieldSyllabus": "教学大纲",
+ "fieldObjectives": "教学目标",
+ "fieldTotalHours": "总课时",
+ "fieldWeeklyHours": "周课时",
+ "fieldStartDate": "开始日期",
+ "fieldEndDate": "结束日期"
},
"error": {
"title": "课程计划模块出错了",
"unknown": "课程计划模块发生未知错误",
"retry": "重试"
+ },
+ "sortableWeekRow": {
+ "notes": "备注:{notes}",
+ "moveUpAria": "上移第 {week} 周",
+ "moveDownAria": "下移第 {week} 周"
+ },
+ "templates": {
+ "title": "从模板创建课程计划",
+ "description": "选择一个模板快速填充表单字段",
+ "searchPlaceholder": "搜索模板...",
+ "loading": "加载模板中...",
+ "empty": "暂无可用模板",
+ "cancel": "取消",
+ "confirm": "应用模板",
+ "errorNoSelection": "请先选择一个模板",
+ "errorApply": "应用模板失败"
+ },
+ "export": {
+ "button": "导出 CSV",
+ "filename": "课程计划",
+ "colName": "名称",
+ "colClass": "班级",
+ "colSubject": "科目",
+ "colTeacher": "教师",
+ "colAcademicYear": "学年",
+ "colStatus": "状态",
+ "colCreatedAt": "创建时间",
+ "statusDraft": "草稿",
+ "statusPublished": "已发布",
+ "statusArchived": "已归档",
+ "success": "导出成功",
+ "error": "导出失败:{message}",
+ "errorEmpty": "没有可导出的数据"
}
},
"curriculumMap": {
@@ -3497,6 +6843,14 @@
"heatmapStandards": "标准",
"heatmapGrades": "年级",
"heatmapCoverage": "覆盖率",
+ "heatmapLinkedTotalHint": "单元格显示覆盖率与已关联/总课时数",
+ "heatmapCellTooltip": "{standard} / {grade}:{rate}({linked}/{total})",
+ "legendTitle": "图例",
+ "legendHigh": "高(≥80%)",
+ "legendMedium": "中(50-80%)",
+ "legendLow": "低(20-50%)",
+ "legendCritical": "极低(<20%)",
+ "legendNone": "无覆盖",
"emptyHeatmap": "暂无覆盖数据",
"emptyTitle": "暂无覆盖数据",
"emptyDescription": "暂无标准覆盖数据,请稍后再试或检查契约状态",
@@ -3534,7 +6888,40 @@
"emptyTitle": "暂无选修课",
"emptyDescription": "调整筛选条件或新建第一条选修课",
"emptyAction": "新建选修课",
- "mswNotice": "选修课列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "选修课列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "statTotalCourses": "课程总数",
+ "statTotalCapacity": "总容量",
+ "statTotalEnrolled": "已选人数",
+ "statTotalDraft": "草稿数",
+ "statTotalOpen": "报名中数",
+ "statsTitle": "选修课概览",
+ "colClassroom": "教室",
+ "colSchedule": "时间",
+ "colCredit": "学分",
+ "colSelectionMode": "选课模式",
+ "openSelection": "开放报名",
+ "closeSelection": "关闭报名",
+ "runLottery": "抽签",
+ "delete": "删除",
+ "deleteSuccess": "选修课已删除",
+ "deleteFailed": "删除失败",
+ "openSuccess": "已开放报名",
+ "closeSuccess": "已关闭报名",
+ "lotterySuccess": "抽签已完成",
+ "lotteryFailed": "抽签失败",
+ "confirmOpenTitle": "确认开放报名",
+ "confirmOpenDescription": "开放后学生可以开始选课,是否继续?",
+ "confirmCloseTitle": "确认关闭报名",
+ "confirmCloseDescription": "关闭后学生将无法选课,是否继续?",
+ "confirmLotteryTitle": "确认抽签",
+ "confirmLotteryDescription": "将对所有报名学生进行抽签分配,此操作不可撤销,是否继续?",
+ "confirmDeleteTitle": "删除选修课",
+ "confirmDeleteDescription": "确定要删除此选修课吗?此操作不可撤销。",
+ "confirmCancel": "取消",
+ "confirmSubmit": "确认",
+ "confirming": "处理中...",
+ "selectionModeFcfs": "先到先得",
+ "selectionModeLottery": "抽签"
},
"create": {
"title": "新建选修课",
@@ -3555,7 +6942,16 @@
"errorNameRequired": "名称不能为空",
"errorSubjectRequired": "请选择科目",
"errorCapacityInvalid": "容量必须为大于 0 的整数",
- "contractPending": "选修课创建契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "contractPending": "选修课创建契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "fieldCredit": "学分",
+ "fieldClassroom": "教室",
+ "fieldSchedule": "上课时间",
+ "fieldSelectionMode": "选课模式",
+ "fieldSelectionStartAt": "选课开始时间",
+ "fieldSelectionEndAt": "选课结束时间",
+ "fieldDropDeadline": "退课截止时间",
+ "selectionModeFcfs": "先到先得",
+ "selectionModeLottery": "抽签"
},
"detail": {
"title": "选修课详情",
@@ -3579,7 +6975,32 @@
"enrollmentStudentName": "学生姓名",
"enrollmentStudentNo": "学号",
"enrollmentEnrolledAt": "选课时间",
- "emptyEnrollment": "暂无学生选课"
+ "emptyEnrollment": "暂无学生选课",
+ "fieldClassroom": "教室",
+ "fieldSchedule": "上课时间",
+ "fieldCredit": "学分",
+ "fieldSelectionMode": "选课模式",
+ "fieldSelectionStart": "选课开始时间",
+ "fieldSelectionEnd": "选课结束时间",
+ "fieldDropDeadline": "退课截止时间",
+ "fieldPriority": "优先级",
+ "selectionModeFcfs": "先到先得",
+ "selectionModeLottery": "抽签",
+ "enrollmentPriority": "优先级",
+ "delete": "删除",
+ "deleteTitle": "删除选修课",
+ "deleteDescription": "确定要删除此选修课吗?此操作不可撤销。",
+ "deleteConfirm": "确认删除",
+ "deleteCancel": "取消",
+ "deleteSuccess": "选修课已删除",
+ "deleteFailed": "删除失败",
+ "openSelection": "开放报名",
+ "closeSelection": "关闭报名",
+ "runLottery": "抽签",
+ "openSuccess": "已开放报名",
+ "closeSuccess": "已关闭报名",
+ "lotterySuccess": "抽签已完成",
+ "lotteryFailed": "抽签失败"
},
"edit": {
"title": "编辑选修课",
@@ -3601,7 +7022,16 @@
"errorSubjectRequired": "请选择科目",
"errorCapacityInvalid": "容量必须为大于 0 的整数",
"notFound": "未找到该选修课",
- "backToDetail": "返回详情"
+ "backToDetail": "返回详情",
+ "fieldCredit": "学分",
+ "fieldClassroom": "教室",
+ "fieldSchedule": "上课时间",
+ "fieldSelectionMode": "选课模式",
+ "fieldSelectionStartAt": "选课开始时间",
+ "fieldSelectionEndAt": "选课结束时间",
+ "fieldDropDeadline": "退课截止时间",
+ "selectionModeFcfs": "先到先得",
+ "selectionModeLottery": "抽签"
},
"error": {
"title": "选修课模块出错了",
@@ -3691,7 +7121,79 @@
"emptyTitle": "暂无题目",
"emptyDescription": "调整筛选条件后重试,或新建第一条题目。",
"emptyAction": "新建题目",
- "mswNotice": "题库列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "题库列表查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "selectAll": "全选当前页",
+ "selectRow": "选择此行"
+ },
+ "detailDialog": {
+ "title": "题目详情",
+ "close": "关闭",
+ "retry": "重试",
+ "type": "题型",
+ "difficulty": "难度",
+ "status": "状态",
+ "subject": "学科",
+ "textbook": "教材",
+ "knowledgePoint": "知识点",
+ "content": "题目内容",
+ "noContent": "暂无题目内容",
+ "answer": "正确答案",
+ "explanation": "解析",
+ "source": "来源",
+ "createdBy": "创建者",
+ "createdAt": "创建时间",
+ "updatedAt": "更新时间",
+ "noData": "暂无详情数据"
+ },
+ "createDialog": {
+ "title": "新建题目",
+ "description": "填写题目内容、答案、解析等信息,提交后即可在题库中查看。",
+ "fieldType": "题型",
+ "fieldContent": "题干",
+ "fieldAnswer": "答案",
+ "fieldExplanation": "解析",
+ "fieldDifficulty": "难度",
+ "fieldKnowledgePoint": "知识点 ID",
+ "fieldKnowledgePointPlaceholder": "如 kp-001",
+ "fieldSource": "来源",
+ "fieldSourcePlaceholder": "如 人教版必修一(可选)",
+ "types": {
+ "single_choice": "单选题",
+ "multiple_choice": "多选题",
+ "fill_blank": "填空题",
+ "short_answer": "简答题",
+ "essay": "论述题",
+ "true_false": "判断题"
+ },
+ "difficultyEasy": "简单",
+ "difficultyMedium": "中等",
+ "difficultyHard": "困难",
+ "cancel": "取消",
+ "submit": "提交",
+ "submitting": "提交中…",
+ "success": "题目创建成功",
+ "errorContentRequired": "请填写题干内容",
+ "errorAnswerRequired": "请填写答案",
+ "errorKnowledgePointRequired": "请填写知识点 ID"
+ },
+ "importExport": {
+ "import": "导入",
+ "export": "导出",
+ "importSuccess": "已成功导入 {imported} 条,跳过 {skipped} 条",
+ "exportEmpty": "当前筛选条件下没有可导出的题目",
+ "exportSuccess": "已导出 {count} 条题目",
+ "exportFailed": "导出失败,请重试"
+ },
+ "batch": {
+ "selectedCount": "已选 {count} 项",
+ "clearSelection": "清空选择",
+ "batchDelete": "批量删除",
+ "confirmTitle": "确认批量删除",
+ "confirmDescription": "即将删除 {count} 条题目,此操作不可撤销,是否继续?",
+ "confirmCancel": "取消",
+ "confirmSubmit": "确认删除",
+ "confirming": "删除中…",
+ "deleteSuccess": "已删除 {deleted} 条,失败 {failed} 条"
},
"error": {
"title": "题库模块出错了",
@@ -3711,6 +7213,7 @@
"statusPublished": "已发布",
"statusArchived": "已归档",
"statusSubmitted": "已提交",
+ "statsTitle": "教案统计概览",
"statsTotal": "教案总数",
"statsPublished": "已发布",
"statsDraft": "草稿",
@@ -3753,6 +7256,15 @@
"emptyMaterials": "暂无教学材料",
"contractPending": "资源字段契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
},
+ "delete": {
+ "button": "删除",
+ "title": "删除教案",
+ "description": "确定要删除此教案吗?删除后将标记为已归档,不再在前台展示。",
+ "confirm": "确认删除",
+ "cancel": "取消",
+ "success": "教案已删除",
+ "error": "删除教案失败,请稍后重试"
+ },
"error": {
"title": "教案模块出错了",
"unknown": "教案模块发生未知错误",
@@ -3788,7 +7300,30 @@
"topWrongQuestionsErrorCount": "错误次数",
"emptyTitle": "暂无错题数据",
"emptyDescription": "暂未加载到错题本统计数据,请稍后重试或调整筛选条件。",
- "mswNotice": "错题本分析查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
+ "mswNotice": "错题本分析查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。",
+ "topWrongQuestionsActions": "操作",
+ "viewDetail": "查看详情",
+ "classDistributionTitle": "班级分布",
+ "classDistributionClass": "班级",
+ "classDistributionCount": "错误次数",
+ "exportCsv": "导出 CSV",
+ "exporting": "导出中...",
+ "exportSuccess": "已导出 {count} 条错题"
+ },
+ "detailDialog": {
+ "title": "错题详情",
+ "close": "关闭",
+ "retry": "重试",
+ "studentName": "学生姓名",
+ "className": "班级",
+ "errorCount": "错误次数",
+ "question": "题目内容",
+ "noContent": "暂无题目内容",
+ "correctAnswer": "正确答案",
+ "analysis": "解析",
+ "lastErrorTime": "最近错误时间",
+ "knowledgePoint": "知识点",
+ "noData": "暂无详情数据"
},
"error": {
"title": "错题本分析模块出错了",
@@ -3874,9 +7409,11 @@
"list": {
"title": "考勤管理",
"description": "全校考勤聚合分析",
+ "gradeFilter": "按年级筛选",
"classFilter": "按班级筛选",
"statusFilter": "按状态筛选",
"dateFilter": "按日期筛选",
+ "allGrades": "全部年级",
"allClasses": "全部班级",
"allStatuses": "全部状态",
"statusPresent": "出勤",
@@ -3890,6 +7427,17 @@
"statsEarlyLeaveRate": "早退率",
"statsAbnormalRate": "异常率",
"statsAvgCorrelation": "平均相关系数",
+ "recordsTitle": "考勤记录",
+ "recordsDescription": "按筛选条件展示考勤明细",
+ "recordsTotal": "共 {count} 条记录",
+ "recordsEmptyTitle": "暂无考勤记录",
+ "recordsEmptyDescription": "暂未加载到考勤记录,请稍后重试或调整筛选条件。",
+ "colStudent": "学生",
+ "colClass": "班级",
+ "colDate": "日期",
+ "colStatus": "状态",
+ "colNote": "备注",
+ "colRecorder": "记录人",
"classComparisonTitle": "班级对比",
"classComparisonClass": "班级",
"classComparisonRate": "出勤率",
@@ -3900,6 +7448,56 @@
"emptyDescription": "暂未加载到考勤统计数据,请稍后重试或调整筛选条件。",
"mswNotice": "考勤统计查询契约待补齐,请确认 NEXT_PUBLIC_MSW=1 已启用。"
},
+ "classComparison": {
+ "title": "班级出勤率对比",
+ "description": "各班级出勤率横向对比与排名",
+ "updatedAt": "更新于 {time}",
+ "errorNotification": "加载班级对比数据失败",
+ "errorTitle": "加载失败",
+ "errorDescription": "班级对比数据加载失败,请稍后重试。",
+ "emptyTitle": "暂无班级对比数据",
+ "emptyDescription": "暂未加载到班级对比数据,请稍后重试。",
+ "seriesRate": "出勤率",
+ "colRank": "排名",
+ "colClass": "班级",
+ "colTotal": "总人数",
+ "colPresent": "出勤人数",
+ "colRate": "出勤率",
+ "colBadge": "等级",
+ "badgeHigh": "优秀",
+ "badgeMid": "一般",
+ "badgeLow": "偏低"
+ },
+ "gradeCorrelation": {
+ "title": "考勤-成绩关联分析",
+ "description": "班级出勤率与平均成绩的关联性分析",
+ "errorNotification": "加载考勤-成绩关联数据失败",
+ "errorTitle": "加载失败",
+ "errorDescription": "考勤-成绩关联数据加载失败,请稍后重试。",
+ "emptyTitle": "暂无关联数据",
+ "emptyDescription": "暂未加载到考勤-成绩关联数据,请稍后重试。",
+ "summaryAvgCorrelation": "平均相关系数",
+ "summaryAvgCorrelationDesc": "出勤率与成绩的线性相关程度",
+ "summaryStrong": "强相关班级",
+ "summaryMedium": "中等相关班级",
+ "summaryWeak": "弱相关班级",
+ "scatterTitle": "出勤率-成绩散点图",
+ "xAxisLabel": "出勤率",
+ "yAxisLabel": "平均成绩",
+ "scatterSeries": "班级",
+ "legendStrong": "强相关 (>=0.7)",
+ "legendMedium": "中等相关 (0.4-0.7)",
+ "legendWeak": "弱相关 (<0.4)",
+ "detailsTitle": "班级明细",
+ "colClass": "班级",
+ "colAttendanceRate": "出勤率",
+ "colAvgScore": "平均成绩",
+ "colCorrelation": "相关系数",
+ "colTier": "关联等级",
+ "badgeStrong": "强相关",
+ "badgeMedium": "中等",
+ "badgeWeak": "弱相关"
+ },
"error": {
"title": "考勤管理模块出错了",
"unknown": "考勤管理模块发生未知错误",
diff --git a/apps/portal-shell/src/mocks/graphql-data.ts b/apps/portal-shell/src/mocks/graphql-data.ts
index 7eafcf1..40aea20 100644
--- a/apps/portal-shell/src/mocks/graphql-data.ts
+++ b/apps/portal-shell/src/mocks/graphql-data.ts
@@ -151,6 +151,8 @@ const mockUsers = {
name: "张老师",
email: "zhang@edu.cn",
role: "teacher",
+ roles: ["teacher"],
+ phone: "13800000001",
status: "active",
createdAt: "2026-06-01T00:00:00Z",
},
@@ -159,6 +161,8 @@ const mockUsers = {
name: "李老师",
email: "li@edu.cn",
role: "teacher",
+ roles: ["teacher", "parent"],
+ phone: "13800000002",
status: "active",
createdAt: "2026-06-15T00:00:00Z",
},
@@ -167,6 +171,8 @@ const mockUsers = {
name: "管理员",
email: "admin@edu.cn",
role: "admin",
+ roles: ["admin"],
+ phone: "13800000003",
status: "active",
createdAt: "2026-05-01T00:00:00Z",
},
@@ -175,6 +181,8 @@ const mockUsers = {
name: "王同学",
email: "wang@edu.cn",
role: "student",
+ roles: ["student"],
+ phone: "13800000004",
status: "active",
createdAt: "2026-07-01T00:00:00Z",
},
@@ -183,6 +191,8 @@ const mockUsers = {
name: "赵家长",
email: "zhao@edu.cn",
role: "parent",
+ roles: ["parent"],
+ phone: "13800000005",
status: "suspended",
createdAt: "2026-07-10T00:00:00Z",
},
@@ -243,6 +253,8 @@ const mockSchools = [
email: "admin@no1.edu.cn",
currentAcademicYear: "2026-2027",
currentTerm: "第一学期",
+ code: "SCH-001",
+ updatedAt: "2026-07-15T10:30:00Z",
},
{
id: "sch-002",
@@ -252,6 +264,8 @@ const mockSchools = [
email: "admin@no2.edu.cn",
currentAcademicYear: "2026-2027",
currentTerm: "第一学期",
+ code: "SCH-002",
+ updatedAt: "2026-07-14T08:20:00Z",
},
];
@@ -264,6 +278,8 @@ const mockDepartments = [
headId: "usr-001",
headName: "张老师",
memberCount: 12,
+ description: "负责全校数学课程教学与教研活动",
+ updatedAt: "2026-07-10T14:00:00Z",
},
{
id: "dept-002",
@@ -272,6 +288,8 @@ const mockDepartments = [
headId: "usr-002",
headName: "李老师",
memberCount: 10,
+ description: "负责全校语文课程教学与教研活动",
+ updatedAt: "2026-07-09T09:30:00Z",
},
{
id: "dept-003",
@@ -280,6 +298,8 @@ const mockDepartments = [
headId: "usr-006",
headName: "陈老师",
memberCount: 8,
+ description: "负责全校英语课程教学与教研活动",
+ updatedAt: "2026-07-08T16:00:00Z",
},
];
@@ -314,6 +334,10 @@ const mockAdminGrades = [
headStaffName: "王主任",
classCount: 8,
studentCount: 320,
+ order: 10,
+ teachingHead: "staff-004",
+ teachingHeadName: "孙教学主任",
+ updatedAt: "2026-07-12T11:00:00Z",
},
{
id: "grade-11",
@@ -324,6 +348,10 @@ const mockAdminGrades = [
headStaffName: "刘主任",
classCount: 8,
studentCount: 315,
+ order: 11,
+ teachingHead: "staff-005",
+ teachingHeadName: "周教学主任",
+ updatedAt: "2026-07-11T15:30:00Z",
},
{
id: "grade-12",
@@ -334,6 +362,10 @@ const mockAdminGrades = [
headStaffName: "赵主任",
classCount: 6,
studentCount: 240,
+ order: 12,
+ teachingHead: "",
+ teachingHeadName: "",
+ updatedAt: "2026-07-10T09:00:00Z",
},
];
@@ -350,6 +382,12 @@ const mockAdminClasses = [
headTeacherName: "张老师",
studentCount: 38,
subjectCount: 9,
+ homeroomLabel: "1班",
+ room: "301",
+ homeroom: "usr-001",
+ homeroomName: "张老师",
+ subjectTeachers: "张老师、李老师、陈老师",
+ updatedAt: "2026-07-12T10:00:00Z",
},
{
id: "cls-002",
@@ -362,6 +400,12 @@ const mockAdminClasses = [
headTeacherName: "李老师",
studentCount: 40,
subjectCount: 9,
+ homeroomLabel: "2班",
+ room: "302",
+ homeroom: "usr-002",
+ homeroomName: "李老师",
+ subjectTeachers: "李老师、王老师、刘老师",
+ updatedAt: "2026-07-12T10:30:00Z",
},
{
id: "cls-003",
@@ -374,6 +418,12 @@ const mockAdminClasses = [
headTeacherName: "陈老师",
studentCount: 42,
subjectCount: 9,
+ homeroomLabel: "1班",
+ room: "201",
+ homeroom: "usr-006",
+ homeroomName: "陈老师",
+ subjectTeachers: "陈老师、赵老师、孙老师",
+ updatedAt: "2026-07-11T16:00:00Z",
},
];
@@ -404,6 +454,7 @@ const mockAdminAnnouncements = {
createdAt: "2026-07-19T10:00:00Z",
updatedAt: "2026-07-20T08:00:00Z",
authorName: "王主任",
+ readCount: 128,
},
{
id: "ann-002",
@@ -416,6 +467,7 @@ const mockAdminAnnouncements = {
createdAt: "2026-07-18T15:30:00Z",
updatedAt: "2026-07-18T15:30:00Z",
authorName: "刘主任",
+ readCount: 0,
},
{
id: "ann-003",
@@ -428,11 +480,72 @@ const mockAdminAnnouncements = {
createdAt: "2026-07-04T14:00:00Z",
updatedAt: "2026-07-18T10:00:00Z",
authorName: "赵主任",
+ readCount: 256,
},
],
total: 3,
};
+// mockInvitationCodes: 邀请码列表
+const mockInvitationCodes = [
+ {
+ id: "inv-001",
+ code: "EDU-TEACHER-001",
+ role: "teacher",
+ roleName: "教师",
+ classId: null,
+ className: null,
+ email: null,
+ batchId: "batch-001",
+ status: "unused",
+ usedCount: 0,
+ maxUses: 1,
+ usedBy: null,
+ usedByName: null,
+ usedAt: null,
+ expiresAt: "2026-08-31T23:59:59Z",
+ createdAt: "2026-07-20T10:00:00Z",
+ createdBy: "管理员",
+ },
+ {
+ id: "inv-002",
+ code: "EDU-STUDENT-002",
+ role: "student",
+ roleName: "学生",
+ classId: "cls-001",
+ className: "高一(1)班",
+ email: null,
+ batchId: "batch-001",
+ status: "used",
+ usedCount: 1,
+ maxUses: 1,
+ usedBy: "usr-101",
+ usedByName: "张学生",
+ usedAt: "2026-07-21T14:30:00Z",
+ expiresAt: "2026-08-31T23:59:59Z",
+ createdAt: "2026-07-20T10:00:00Z",
+ createdBy: "管理员",
+ },
+ {
+ id: "inv-003",
+ code: "EDU-TEACHER-003",
+ role: "teacher",
+ roleName: "教师",
+ classId: null,
+ className: null,
+ email: "teacher@example.com",
+ batchId: "batch-002",
+ status: "unused",
+ usedCount: 0,
+ maxUses: 1,
+ usedBy: null,
+ usedByName: null,
+ usedAt: null,
+ expiresAt: "2026-09-15T23:59:59Z",
+ createdAt: "2026-07-22T09:00:00Z",
+ createdBy: "管理员",
+ },
+];
// mockFileAttachments:文件附件列表
const mockFileAttachments = {
items: [
@@ -491,11 +604,11 @@ const mockFileAttachments = {
const mockAiProviders = [
{
id: "aip-001",
- name: "豆包大模型",
- type: "doubao",
+ name: "智谱 GLM",
+ type: "zhipu",
scope: "school",
- private: false,
- public: true,
+ visibility: "school",
+ isDefault: true,
ownerId: "sch-001",
isActive: true,
model: "doubao-pro-4k",
@@ -505,11 +618,11 @@ const mockAiProviders = [
},
{
id: "aip-002",
- name: "DeepSeek",
- type: "deepseek",
+ name: "OpenAI GPT",
+ type: "openai",
scope: "personal",
- private: true,
- public: false,
+ visibility: "private",
+ isDefault: false,
ownerId: "usr-001",
isActive: true,
model: "deepseek-chat",
@@ -519,11 +632,11 @@ const mockAiProviders = [
},
{
id: "aip-003",
- name: "通义千问",
- type: "qwen",
+ name: "Google Gemini",
+ type: "gemini",
scope: "global",
- private: false,
- public: true,
+ visibility: "public",
+ isDefault: false,
ownerId: "system",
isActive: false,
model: "qwen-max",
@@ -766,10 +879,26 @@ const mockAdminCoursePlans = {
id: "cp-001",
name: "高三数学复习计划",
gradeId: "grade-12",
+ classId: "cls-001",
+ className: "高三(1)班",
subjectId: "sub-math",
+ subjectName: "数学",
+ teacherId: "usr-001",
+ teacherName: "张老师",
+ academicYearId: "ay-2026",
+ academicYearName: "2026-2027 学年",
semester: "2026-fall",
status: "PUBLISHED",
description: "覆盖函数、导数、概率统计三大模块",
+ totalHours: 120,
+ completedHours: 48,
+ weeklyHours: 6,
+ startDate: "2026-09-01",
+ endDate: "2027-01-15",
+ syllabus: "函数与导数、三角函数、数列、立体几何、解析几何、概率统计",
+ objectives: "掌握高考数学核心考点,能综合应用解题",
+ textbooksHref: "/shell/admin/textbooks?subject=sub-math",
+ homeworkHref: "/shell/admin/homework?classId=cls-001",
createdAt: "2026-07-01T00:00:00Z",
updatedAt: "2026-07-15T00:00:00Z",
},
@@ -777,10 +906,26 @@ const mockAdminCoursePlans = {
id: "cp-002",
name: "高二数学教学计划",
gradeId: "grade-11",
+ classId: "cls-002",
+ className: "高二(1)班",
subjectId: "sub-math",
+ subjectName: "数学",
+ teacherId: "usr-002",
+ teacherName: "李老师",
+ academicYearId: "ay-2026",
+ academicYearName: "2026-2027 学年",
semester: "2026-fall",
status: "DRAFT",
description: "立体几何与解析几何",
+ totalHours: 96,
+ completedHours: 0,
+ weeklyHours: 5,
+ startDate: "2026-09-01",
+ endDate: "2027-01-15",
+ syllabus: "立体几何、解析几何、概率初步",
+ objectives: "建立空间想象力与代数推理能力",
+ textbooksHref: "/shell/admin/textbooks?subject=sub-math",
+ homeworkHref: "/shell/admin/homework?classId=cls-002",
createdAt: "2026-07-05T00:00:00Z",
updatedAt: "2026-07-18T00:00:00Z",
},
@@ -788,10 +933,26 @@ const mockAdminCoursePlans = {
id: "cp-003",
name: "高三语文教学计划",
gradeId: "grade-12",
+ classId: "cls-003",
+ className: "高三(2)班",
subjectId: "sub-chinese",
+ subjectName: "语文",
+ teacherId: "usr-003",
+ teacherName: "王老师",
+ academicYearId: "ay-2026",
+ academicYearName: "2026-2027 学年",
semester: "2026-fall",
status: "PUBLISHED",
description: "古诗文与现代文阅读",
+ totalHours: 108,
+ completedHours: 36,
+ weeklyHours: 5,
+ startDate: "2026-09-01",
+ endDate: "2027-01-15",
+ syllabus: "古诗文鉴赏、现代文阅读、写作训练、语言文字应用",
+ objectives: "提升文本解读能力与写作表达能力",
+ textbooksHref: "/shell/admin/textbooks?subject=sub-chinese",
+ homeworkHref: "/shell/admin/homework?classId=cls-003",
createdAt: "2026-06-20T00:00:00Z",
updatedAt: "2026-07-10T00:00:00Z",
},
@@ -799,10 +960,26 @@ const mockAdminCoursePlans = {
id: "cp-004",
name: "高二英语教学计划",
gradeId: "grade-11",
+ classId: "cls-004",
+ className: "高二(2)班",
subjectId: "sub-english",
+ subjectName: "英语",
+ teacherId: "usr-006",
+ teacherName: "陈老师",
+ academicYearId: "ay-2026",
+ academicYearName: "2026-2027 学年",
semester: "2026-fall",
status: "PUBLISHED",
description: "听说读写综合训练",
+ totalHours: 100,
+ completedHours: 24,
+ weeklyHours: 5,
+ startDate: "2026-09-01",
+ endDate: "2027-01-15",
+ syllabus: "听力训练、口语表达、阅读理解、写作技巧、语法专题",
+ objectives: "全面提升英语综合应用能力",
+ textbooksHref: "/shell/admin/textbooks?subject=sub-english",
+ homeworkHref: "/shell/admin/homework?classId=cls-004",
createdAt: "2026-06-25T00:00:00Z",
updatedAt: "2026-07-12T00:00:00Z",
},
@@ -810,6 +987,133 @@ const mockAdminCoursePlans = {
total: 4,
};
+// mockAdminCoursePlanItems:管理端课程计划周计划项(in-memory store,可被 mutation 修改)
+const mockAdminCoursePlanItems: Record<
+ string,
+ Array<{
+ id: string;
+ planId: string;
+ week: number;
+ topic: string;
+ content: string | null;
+ hours: number;
+ textbookChapter: string | null;
+ notes: string | null;
+ isCompleted: boolean;
+ completedAt: string | null;
+ createdAt: string;
+ updatedAt: string;
+ }>
+> = {
+ "cp-001": [
+ {
+ id: "cpi-001-1",
+ planId: "cp-001",
+ week: 1,
+ topic: "集合与函数概念",
+ content: "集合的表示、函数的定义域与值域",
+ hours: 4,
+ textbookChapter: "第1章",
+ notes: "建议结合实例引入",
+ isCompleted: true,
+ completedAt: "2026-07-05T00:00:00Z",
+ createdAt: "2026-07-01T00:00:00Z",
+ updatedAt: "2026-07-05T00:00:00Z",
+ },
+ {
+ id: "cpi-001-2",
+ planId: "cp-001",
+ week: 2,
+ topic: "函数的基本性质",
+ content: "单调性、奇偶性、周期性",
+ hours: 4,
+ textbookChapter: "第2章",
+ notes: null,
+ isCompleted: true,
+ completedAt: "2026-07-12T00:00:00Z",
+ createdAt: "2026-07-01T00:00:00Z",
+ updatedAt: "2026-07-12T00:00:00Z",
+ },
+ {
+ id: "cpi-001-3",
+ planId: "cp-001",
+ week: 3,
+ topic: "指数函数与对数函数",
+ content: "指数运算、对数运算、图像与性质",
+ hours: 6,
+ textbookChapter: "第3章",
+ notes: "重点突破图像变换",
+ isCompleted: false,
+ completedAt: null,
+ createdAt: "2026-07-01T00:00:00Z",
+ updatedAt: "2026-07-15T00:00:00Z",
+ },
+ ],
+};
+
+function getCoursePlanItems(planId: string): Array<{
+ id: string;
+ planId: string;
+ week: number;
+ topic: string;
+ content: string | null;
+ hours: number;
+ textbookChapter: string | null;
+ notes: string | null;
+ isCompleted: boolean;
+ completedAt: string | null;
+ createdAt: string;
+ updatedAt: string;
+}> {
+ if (!mockAdminCoursePlanItems[planId]) {
+ mockAdminCoursePlanItems[planId] = [
+ {
+ id: planId + "-cpi-1",
+ planId,
+ week: 1,
+ topic: "模块一 导入",
+ content: null,
+ hours: 2,
+ textbookChapter: null,
+ notes: null,
+ isCompleted: false,
+ completedAt: null,
+ createdAt: "2026-07-22T00:00:00Z",
+ updatedAt: "2026-07-22T00:00:00Z",
+ },
+ {
+ id: planId + "-cpi-2",
+ planId,
+ week: 2,
+ topic: "模块二 基础",
+ content: null,
+ hours: 3,
+ textbookChapter: null,
+ notes: null,
+ isCompleted: false,
+ completedAt: null,
+ createdAt: "2026-07-22T00:00:00Z",
+ updatedAt: "2026-07-22T00:00:00Z",
+ },
+ {
+ id: planId + "-cpi-3",
+ planId,
+ week: 3,
+ topic: "模块三 进阶",
+ content: null,
+ hours: 3,
+ textbookChapter: null,
+ notes: null,
+ isCompleted: false,
+ completedAt: null,
+ createdAt: "2026-07-22T00:00:00Z",
+ updatedAt: "2026-07-22T00:00:00Z",
+ },
+ ];
+ }
+ return [...mockAdminCoursePlanItems[planId]].sort((a, b) => a.week - b.week);
+}
+
// mockAdminLessonPlans:管理端教案列表
const mockAdminLessonPlans = {
items: [
@@ -866,12 +1170,26 @@ const mockAdminElectives = {
description: "通过游戏化方式学习数学",
capacity: 30,
enrolledCount: 25,
+ selectedCount: 25,
semester: "2026-fall",
gradeLevel: "grade-10",
+ gradeId: "grade-10",
+ gradeName: "高一",
subject: "数学",
+ subjectId: "sub-math",
+ subjectName: "数学",
teacherId: "usr-001",
teacherName: "张老师",
- status: "PUBLISHED",
+ classroom: "A-301",
+ schedule: "周三下午 7-8 节",
+ credit: 2,
+ selectionMode: "FIRST_COME",
+ selectionStartAt: "2026-09-05T00:00:00Z",
+ selectionEndAt: "2026-09-15T23:59:59Z",
+ dropDeadline: "2026-09-20T23:59:59Z",
+ startDate: "2026-09-22",
+ endDate: "2027-01-15",
+ status: "OPEN",
createdAt: "2026-06-15T00:00:00Z",
updatedAt: "2026-07-10T00:00:00Z",
},
@@ -881,12 +1199,26 @@ const mockAdminElectives = {
description: "沉浸式英语口语训练",
capacity: 20,
enrolledCount: 18,
+ selectedCount: 18,
semester: "2026-fall",
gradeLevel: "grade-10",
+ gradeId: "grade-10",
+ gradeName: "高一",
subject: "英语",
+ subjectId: "sub-english",
+ subjectName: "英语",
teacherId: "usr-006",
teacherName: "陈老师",
- status: "PUBLISHED",
+ classroom: "B-205",
+ schedule: "周四下午 7-8 节",
+ credit: 2,
+ selectionMode: "FIRST_COME",
+ selectionStartAt: "2026-09-05T00:00:00Z",
+ selectionEndAt: "2026-09-15T23:59:59Z",
+ dropDeadline: "2026-09-20T23:59:59Z",
+ startDate: "2026-09-22",
+ endDate: "2027-01-15",
+ status: "FULL",
createdAt: "2026-06-18T00:00:00Z",
updatedAt: "2026-07-12T00:00:00Z",
},
@@ -896,11 +1228,25 @@ const mockAdminElectives = {
description: "动手实验理解物理原理",
capacity: 25,
enrolledCount: 0,
+ selectedCount: 0,
semester: "2026-fall",
gradeLevel: "grade-11",
+ gradeId: "grade-11",
+ gradeName: "高二",
subject: "物理",
+ subjectId: "sub-physics",
+ subjectName: "物理",
teacherId: "usr-008",
teacherName: "吴老师",
+ classroom: "实验楼 C-101",
+ schedule: "周二下午 7-8 节",
+ credit: 3,
+ selectionMode: "LOTTERY",
+ selectionStartAt: "2026-09-05T00:00:00Z",
+ selectionEndAt: "2026-09-15T23:59:59Z",
+ dropDeadline: "2026-09-20T23:59:59Z",
+ startDate: "2026-09-22",
+ endDate: "2027-01-15",
status: "DRAFT",
createdAt: "2026-07-01T00:00:00Z",
updatedAt: "2026-07-15T00:00:00Z",
@@ -1049,7 +1395,8 @@ const mockSchedulingRules = [
},
];
-// mockAdminAttendanceRecords:管理端考勤记录列表
+// mockAdminAttendanceRecords:管理端考勤记录列表(含 gradeId 字段以支持年级筛选,
+// 字段名对齐 AdminAttendanceRecord 类型:note/recordedBy,便于分页与筛选演示)
const mockAdminAttendanceRecords = {
items: [
{
@@ -1058,10 +1405,11 @@ const mockAdminAttendanceRecords = {
studentName: "张明",
classId: "cls-001",
className: "高三(1)班",
+ gradeId: "grade-12",
date: "2026-07-22",
status: "present",
- remark: "",
- recordedAt: "2026-07-22T08:10:00Z",
+ note: "",
+ recordedBy: "张老师",
},
{
id: "att-002",
@@ -1069,10 +1417,11 @@ const mockAdminAttendanceRecords = {
studentName: "李华",
classId: "cls-001",
className: "高三(1)班",
+ gradeId: "grade-12",
date: "2026-07-22",
status: "late",
- remark: "迟到 10 分钟",
- recordedAt: "2026-07-22T08:20:00Z",
+ note: "迟到 10 分钟",
+ recordedBy: "张老师",
},
{
id: "att-003",
@@ -1080,10 +1429,11 @@ const mockAdminAttendanceRecords = {
studentName: "王芳",
classId: "cls-001",
className: "高三(1)班",
+ gradeId: "grade-12",
date: "2026-07-22",
status: "absent",
- remark: "病假",
- recordedAt: "2026-07-22T08:00:00Z",
+ note: "病假",
+ recordedBy: "张老师",
},
{
id: "att-004",
@@ -1091,10 +1441,11 @@ const mockAdminAttendanceRecords = {
studentName: "赵六",
classId: "cls-002",
className: "高三(2)班",
+ gradeId: "grade-12",
date: "2026-07-22",
- status: "early_leave",
- remark: "家中有事早退",
- recordedAt: "2026-07-22T15:30:00Z",
+ status: "leave",
+ note: "事假",
+ recordedBy: "李老师",
},
{
id: "att-005",
@@ -1102,13 +1453,134 @@ const mockAdminAttendanceRecords = {
studentName: "钱七",
classId: "cls-003",
className: "高二(1)班",
+ gradeId: "grade-11",
date: "2026-07-22",
status: "present",
- remark: "",
- recordedAt: "2026-07-22T08:05:00Z",
+ note: "",
+ recordedBy: "王老师",
+ },
+ {
+ id: "att-006",
+ studentId: "stu-006",
+ studentName: "孙八",
+ classId: "cls-003",
+ className: "高二(1)班",
+ gradeId: "grade-11",
+ date: "2026-07-22",
+ status: "late",
+ note: "迟到 5 分钟",
+ recordedBy: "王老师",
+ },
+ {
+ id: "att-007",
+ studentId: "stu-007",
+ studentName: "周九",
+ classId: "cls-004",
+ className: "高二(2)班",
+ gradeId: "grade-11",
+ date: "2026-07-22",
+ status: "absent",
+ note: "旷课",
+ recordedBy: "陈老师",
+ },
+ {
+ id: "att-008",
+ studentId: "stu-008",
+ studentName: "吴十",
+ classId: "cls-005",
+ className: "高一(1)班",
+ gradeId: "grade-10",
+ date: "2026-07-22",
+ status: "present",
+ note: "",
+ recordedBy: "刘老师",
+ },
+ {
+ id: "att-009",
+ studentId: "stu-009",
+ studentName: "郑一",
+ classId: "cls-005",
+ className: "高一(1)班",
+ gradeId: "grade-10",
+ date: "2026-07-22",
+ status: "leave",
+ note: "事假",
+ recordedBy: "刘老师",
+ },
+ {
+ id: "att-010",
+ studentId: "stu-010",
+ studentName: "王二",
+ classId: "cls-001",
+ className: "高三(1)班",
+ gradeId: "grade-12",
+ date: "2026-07-23",
+ status: "present",
+ note: "",
+ recordedBy: "张老师",
+ },
+ {
+ id: "att-011",
+ studentId: "stu-011",
+ studentName: "冯三",
+ classId: "cls-002",
+ className: "高三(2)班",
+ gradeId: "grade-12",
+ date: "2026-07-23",
+ status: "late",
+ note: "迟到 8 分钟",
+ recordedBy: "李老师",
+ },
+ {
+ id: "att-012",
+ studentId: "stu-012",
+ studentName: "陈四",
+ classId: "cls-003",
+ className: "高二(1)班",
+ gradeId: "grade-11",
+ date: "2026-07-23",
+ status: "present",
+ note: "",
+ recordedBy: "王老师",
+ },
+ {
+ id: "att-013",
+ studentId: "stu-013",
+ studentName: "褚五",
+ classId: "cls-004",
+ className: "高二(2)班",
+ gradeId: "grade-11",
+ date: "2026-07-23",
+ status: "absent",
+ note: "病假",
+ recordedBy: "陈老师",
+ },
+ {
+ id: "att-014",
+ studentId: "stu-014",
+ studentName: "卫六",
+ classId: "cls-005",
+ className: "高一(1)班",
+ gradeId: "grade-10",
+ date: "2026-07-23",
+ status: "present",
+ note: "",
+ recordedBy: "刘老师",
+ },
+ {
+ id: "att-015",
+ studentId: "stu-015",
+ studentName: "蒋七",
+ classId: "cls-001",
+ className: "高三(1)班",
+ gradeId: "grade-12",
+ date: "2026-07-23",
+ status: "leave",
+ note: "事假",
+ recordedBy: "张老师",
},
],
- total: 5,
+ total: 15,
};
// mockLoginLogs:登录日志(分页)
@@ -1123,6 +1595,7 @@ const mockLoginLogs = {
ip: "192.168.1.10",
userAgent: "Mozilla/5.0 (Windows NT 10.0)",
timestamp: "2026-07-22T08:00:00Z",
+ errorMessage: null,
},
{
id: "log-002",
@@ -1133,6 +1606,7 @@ const mockLoginLogs = {
ip: "192.168.1.11",
userAgent: "Mozilla/5.0 (Macintosh)",
timestamp: "2026-07-22T08:15:00Z",
+ errorMessage: "密码错误,剩余 3 次尝试机会",
},
{
id: "log-003",
@@ -1143,6 +1617,7 @@ const mockLoginLogs = {
ip: "10.0.0.1",
userAgent: "Mozilla/5.0 (X11; Linux)",
timestamp: "2026-07-22T09:00:00Z",
+ errorMessage: null,
},
{
id: "log-004",
@@ -1153,6 +1628,7 @@ const mockLoginLogs = {
ip: "192.168.1.10",
userAgent: "Mozilla/5.0 (Windows NT 10.0)",
timestamp: "2026-07-22T17:30:00Z",
+ errorMessage: null,
},
{
id: "log-005",
@@ -1163,6 +1639,7 @@ const mockLoginLogs = {
ip: "192.168.1.20",
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS)",
timestamp: "2026-07-22T10:00:00Z",
+ errorMessage: null,
},
],
total: 5,
@@ -1180,6 +1657,8 @@ const mockDataChangeLogs = {
userName: "管理员",
changes: { status: "active", role: "teacher" },
timestamp: "2026-07-22T10:00:00Z",
+ oldValue: JSON.stringify({ status: "inactive", role: "student" }),
+ newValue: JSON.stringify({ status: "active", role: "teacher" }),
},
{
id: "dcl-002",
@@ -1190,6 +1669,8 @@ const mockDataChangeLogs = {
userName: "管理员",
changes: { name: "teacher", permissions: ["perm-003"] },
timestamp: "2026-07-22T11:00:00Z",
+ oldValue: null,
+ newValue: JSON.stringify({ name: "teacher", permissions: ["perm-003"] }),
},
{
id: "dcl-003",
@@ -1200,6 +1681,12 @@ const mockDataChangeLogs = {
userName: "管理员",
changes: null,
timestamp: "2026-07-22T14:00:00Z",
+ oldValue: JSON.stringify({
+ id: "cls-003",
+ name: "高三(2)班",
+ grade: "12",
+ }),
+ newValue: null,
},
{
id: "dcl-004",
@@ -1210,6 +1697,14 @@ const mockDataChangeLogs = {
userName: "张老师",
changes: { headStaffId: "staff-003", headStaffName: "赵主任" },
timestamp: "2026-07-22T15:30:00Z",
+ oldValue: JSON.stringify({
+ headStaffId: "staff-001",
+ headStaffName: "钱主任",
+ }),
+ newValue: JSON.stringify({
+ headStaffId: "staff-003",
+ headStaffName: "赵主任",
+ }),
},
{
id: "dcl-005",
@@ -1220,6 +1715,11 @@ const mockDataChangeLogs = {
userName: "王主任",
changes: { title: "2026 年秋季开学通知", status: "published" },
timestamp: "2026-07-20T08:00:00Z",
+ oldValue: null,
+ newValue: JSON.stringify({
+ title: "2026 年秋季开学通知",
+ status: "published",
+ }),
},
],
total: 5,
@@ -4713,34 +5213,55 @@ const mockStudentPracticeSession = {
},
};
-const mockStudentLeave = {
- studentLeave: {
- items: [
- {
- id: "sl-001",
- startDate: "2026-07-21",
- endDate: "2026-07-21",
- type: "personal",
- reason: "家中有事",
- status: "approved",
- },
- {
- id: "sl-002",
- startDate: "2026-06-15",
- endDate: "2026-06-16",
- type: "sick",
- reason: "感冒发烧",
- status: "approved",
- },
- {
- id: "sl-003",
- startDate: "2026-07-28",
- endDate: "2026-07-28",
- type: "personal",
- reason: "参加竞赛",
- status: "pending",
- },
- ],
+const mockStudentLeaveItems = [
+ {
+ id: "sl-001",
+ startDate: "2026-07-21",
+ endDate: "2026-07-21",
+ type: "personal",
+ reason: "家中有事",
+ status: "approved",
+ },
+ {
+ id: "sl-002",
+ startDate: "2026-06-15",
+ endDate: "2026-06-16",
+ type: "sick",
+ reason: "感冒发烧",
+ status: "approved",
+ },
+ {
+ id: "sl-003",
+ startDate: "2026-07-28",
+ endDate: "2026-07-28",
+ type: "personal",
+ reason: "参加竞赛",
+ status: "pending",
+ },
+];
+
+const mockUserProfile = {
+ userProfile: {
+ id: "dev-user-001",
+ name: "张老师",
+ email: "teacher@edu.example.com",
+ phone: "13812345678",
+ avatar: "",
+ role: "teacher",
+ gender: "male",
+ age: 35,
+ address: "北京市海淀区中关村大街 1 号",
+ createdAt: "2024-09-01T08:00:00Z",
+ onboardedAt: "2024-09-02T10:30:00Z",
+ student: null,
+ teacher: {
+ courses: [
+ { id: "course-001", name: "高中数学", subject: "数学" },
+ { id: "course-002", name: "高中物理", subject: "物理" },
+ ],
+ classCount: 3,
+ studentCount: 120,
+ },
},
};
@@ -5020,6 +5541,19 @@ export interface GraphQLRequestBody {
variables?: Record;
}
+/**
+ * CSV 字段转义纯函数(RFC 4180 简化版)。
+ * 含逗号/引号/换行的字段用双引号包裹,内部双引号转义为两个双引号。
+ */
+function escapeCsvField(value: string | null | undefined): string {
+ if (value == null) return "";
+ const str = String(value);
+ if (/[",\n\r]/.test(str)) {
+ return `"${str.replace(/"/g, '""')}"`;
+ }
+ return str;
+}
+
/**
* 根据 operationName + variables 返回 mock GraphQL 响应。
*
@@ -5083,6 +5617,26 @@ export function graphqlResponse(
};
case "UpdateUserRole":
return { data: { updateUserRole: { id: "usr-001", role: "admin" } } };
+ case "DeleteUser":
+ return {
+ data: {
+ deleteUser: {
+ id: (variables as { id?: string }).id ?? "usr-001",
+ success: true,
+ },
+ },
+ };
+ case "AssignUserRoles":
+ return {
+ data: {
+ assignUserRoles: {
+ userId: (variables as { userId?: string }).userId ?? "usr-001",
+ roleNames: (variables as { roleNames?: string[] }).roleNames ?? [
+ "teacher",
+ ],
+ },
+ },
+ };
case "GetRoles":
return {
data: {
@@ -5090,6 +5644,11 @@ export function graphqlResponse(
{
id: "role-001",
name: "admin",
+ description: "系统管理员(锁定)",
+ isLocked: true,
+ isEnabled: true,
+ userCount: 2,
+ updatedAt: "2026-07-20T08:00:00Z",
permissions: [
{
id: "perm-001",
@@ -5108,6 +5667,11 @@ export function graphqlResponse(
{
id: "role-002",
name: "teacher",
+ description: "教师角色",
+ isLocked: false,
+ isEnabled: true,
+ userCount: 18,
+ updatedAt: "2026-07-18T15:30:00Z",
permissions: [
{
id: "perm-003",
@@ -5117,6 +5681,33 @@ export function graphqlResponse(
},
],
},
+ {
+ id: "role-003",
+ name: "student",
+ description: "学生角色",
+ isLocked: false,
+ isEnabled: true,
+ userCount: 245,
+ updatedAt: "2026-07-15T10:00:00Z",
+ permissions: [
+ {
+ id: "perm-004",
+ name: "course.read",
+ resource: "course",
+ action: "read",
+ },
+ ],
+ },
+ {
+ id: "role-004",
+ name: "parent",
+ description: "家长角色(已停用)",
+ isLocked: false,
+ isEnabled: false,
+ userCount: 0,
+ updatedAt: "2026-07-10T09:00:00Z",
+ permissions: [],
+ },
],
},
};
@@ -5207,12 +5798,12 @@ export function graphqlResponse(
}
// UpdateQuestion($id, $input):mutation 兜底
case "UpdateQuestion": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateQuestion: { id } } };
}
// DeleteQuestion($id):mutation 兜底
case "DeleteQuestion": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteQuestion: { id } } };
}
@@ -5276,7 +5867,7 @@ export function graphqlResponse(
}
// UpdateTextbook($id, $input):mutation 兜底
case "UpdateTextbook": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateTextbook: { id } } };
}
// ── Legacy widget 兜底(P2 迁移后重命名,保留旧 widget 契约)──
@@ -5612,6 +6203,67 @@ export function graphqlResponse(
return { data: { recordGrade: { gradeId } } };
}
+ // ── 学生作答流程(@contract-pending 全 MSW)──
+ // StartHomeworkSubmission:开始作答 mutation 兜底
+ case "StartHomeworkSubmission": {
+ const input = (variables?.input ?? {}) as Record;
+ const assignmentId = (input.assignmentId as string) ?? "hw-unknown";
+ const submissionId = `sub-${assignmentId}-${Date.now()}`;
+ return { data: { startHomeworkSubmission: { submissionId } } };
+ }
+
+ // SaveHomeworkAnswer:保存单题答案 mutation 兜底
+ case "SaveHomeworkAnswer": {
+ const input = (variables?.input ?? {}) as Record;
+ const submissionId = (input.submissionId as string) ?? "sub-001";
+ const questionId = (input.questionId as string) ?? "q-001";
+ return {
+ data: { saveHomeworkAnswer: { submissionId, questionId } },
+ };
+ }
+
+ // SubmitHomework:提交作业 mutation 兜底
+ case "SubmitHomework": {
+ const input = (variables?.input ?? {}) as Record;
+ const submissionId = (input.submissionId as string) ?? "sub-001";
+ return {
+ data: {
+ submitHomework: { submissionId, totalScore: null },
+ },
+ };
+ }
+
+ // GetScans:查询某次提交的扫描图
+ case "GetScans": {
+ const submissionId =
+ (variables?.submissionId as string | undefined) ?? "";
+ void submissionId;
+ return { data: { scans: [] } };
+ }
+
+ // DeleteScan:删除扫描图 mutation 兜底
+ case "DeleteScan": {
+ const input = (variables?.input ?? {}) as Record;
+ const fileId = (input.fileId as string) ?? "file-unknown";
+ return { data: { deleteScan: { fileId, success: true } } };
+ }
+
+ // BatchAutoGrade:批量自动批改 mutation 兜底
+ case "BatchAutoGrade": {
+ const input = (variables?.input ?? {}) as Record;
+ const submissionIds = (input.submissionIds as string[]) ?? [];
+ const processedCount = submissionIds.length;
+ return {
+ data: {
+ batchAutoGrade: {
+ processedCount,
+ successCount: processedCount,
+ failedCount: 0,
+ },
+ },
+ };
+ }
+
// ── Grades 域(教师域 P2 迁移,@contract-pending)──
// GetGrade($id):按 id 单查(真实 schema 可用,MSW 也兜底)
case "GetGrade": {
@@ -6109,6 +6761,68 @@ export function graphqlResponse(
const eleId = (input.id as string) ?? "ele-001";
return { data: { updateElective: { id: eleId } } };
}
+ // DeleteElective($id)
+ case "DeleteElective": {
+ const eleId = (variables?.id as string) ?? "";
+ const idx = mockAdminElectives.items.findIndex((e) => e.id === eleId);
+ if (idx >= 0) mockAdminElectives.items.splice(idx, 1);
+ mockAdminElectives.total = mockAdminElectives.items.length;
+ return { data: { deleteElective: { id: eleId, success: true } } };
+ }
+ // OpenElectiveSelection($id)
+ case "OpenElectiveSelection": {
+ const eleId = (variables?.id as string) ?? "";
+ const item = mockAdminElectives.items.find((e) => e.id === eleId);
+ if (item) (item as Record).status = "OPEN";
+ return { data: { openElectiveSelection: { id: eleId, success: true } } };
+ }
+ // CloseElectiveSelection($id)
+ case "CloseElectiveSelection": {
+ const eleId = (variables?.id as string) ?? "";
+ const item = mockAdminElectives.items.find((e) => e.id === eleId);
+ if (item) (item as Record).status = "CLOSED";
+ return { data: { closeElectiveSelection: { id: eleId, success: true } } };
+ }
+ // RunElectiveLottery($id)
+ case "RunElectiveLottery": {
+ const eleId = (variables?.id as string) ?? "";
+ return { data: { runElectiveLottery: { id: eleId, success: true } } };
+ }
+ // DeleteCoursePlan($id)
+ case "DeleteCoursePlan": {
+ const cpId = (variables?.id as string) ?? "";
+ const idx = mockAdminCoursePlans.items.findIndex((e) => e.id === cpId);
+ if (idx >= 0) mockAdminCoursePlans.items.splice(idx, 1);
+ mockAdminCoursePlans.total = mockAdminCoursePlans.items.length;
+ return { data: { deleteCoursePlan: { id: cpId, success: true } } };
+ }
+ // BulkToggleCoursePlanItems
+ case "BulkToggleCoursePlanItems": {
+ const planId = (variables?.planId as string) ?? "";
+ const itemIds = (variables?.itemIds as string[]) ?? [];
+ const isCompleted = (variables?.isCompleted as boolean) ?? false;
+ const arr = mockAdminCoursePlanItems[planId] ?? [];
+ const now = new Date().toISOString();
+ for (const it of arr) {
+ if (itemIds.includes(it.id)) {
+ it.isCompleted = isCompleted;
+ it.completedAt = isCompleted ? now : null;
+ it.updatedAt = now;
+ }
+ }
+ return {
+ data: {
+ bulkToggleCoursePlanItems: arr
+ .filter((it) => itemIds.includes(it.id))
+ .map((it) => ({
+ id: it.id,
+ isCompleted: it.isCompleted,
+ completedAt: it.completedAt,
+ updatedAt: it.updatedAt,
+ })),
+ },
+ };
+ }
// ── Error Book 域(教师域 B2 迁移,✅ 真实查询 MSW 兜底)──
// schema: errorBookItems: [ErrorBookList!]!(无参数)
@@ -6695,7 +7409,7 @@ export function graphqlResponse(
}
// MarkNotificationRead($id):标记单条已读 mutation 兜底
case "MarkNotificationRead": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
const target = mockNotifications.find((n) => n.id === id);
if (target) target.isRead = true;
return {
@@ -6815,14 +7529,20 @@ export function graphqlResponse(
return { data: { createRole: { id: "role-new", name: "newrole" } } };
// UpdateRole($id, $input):mutation 兜底
case "UpdateRole": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateRole: { id, name: "updated" } } };
}
// DeleteRole($id):mutation 兜底
case "DeleteRole": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteRole: { id } } };
}
+ // ToggleRoleEnabled($id, $isEnabled):mutation 兜底
+ case "ToggleRoleEnabled": {
+ const _id = (variables?.id as string) ?? "";
+ const isEnabled = (variables?.isEnabled as boolean) ?? true;
+ return { data: { toggleRoleEnabled: { id, isEnabled } } };
+ }
// GetPermissionRoleCounts:权限-角色数聚合
case "GetPermissionRoleCounts":
@@ -6877,6 +7597,9 @@ export function graphqlResponse(
totalToday: 23,
totalErrors: 5,
totalUsers: 48,
+ auditEventsToday: 23,
+ failedLoginsToday: 3,
+ dataChangesToday: 12,
},
},
};
@@ -6973,15 +7696,265 @@ export function graphqlResponse(
},
};
+ // GetAuditRetentionConfig:审计日志保留期配置(@contract-pending MSW 兜底)
+ case "GetAuditRetentionConfig":
+ return {
+ data: {
+ auditRetentionConfig: {
+ retentionDays: 90,
+ loginLogRetentionDays: 365,
+ autoCleanupEnabled: false,
+ },
+ },
+ };
+
+ // SaveAuditRetentionConfig:保存保留期配置(返回传入 input)
+ case "SaveAuditRetentionConfig": {
+ const input = (variables?.input as {
+ retentionDays: number;
+ loginLogRetentionDays: number;
+ autoCleanupEnabled: boolean;
+ }) ?? {
+ retentionDays: 90,
+ loginLogRetentionDays: 365,
+ autoCleanupEnabled: false,
+ };
+ return { data: { saveAuditRetentionConfig: input } };
+ }
+
+ // PurgeExpiredAuditLogs:手动清理过期日志
+ case "PurgeExpiredAuditLogs":
+ return {
+ data: {
+ purgeExpiredAuditLogs: {
+ auditLogsDeleted: 5,
+ loginLogsDeleted: 2,
+ dataChangeLogsDeleted: 1,
+ },
+ },
+ };
+
+ // ── 课程计划周计划项 CRUD(@contract-pending,MSW 兜底)──
+
+ // CreateCoursePlanItem:创建周计划项
+ case "CreateCoursePlanItem": {
+ const input = (variables?.input ?? {}) as {
+ planId: string;
+ week: number;
+ topic: string;
+ content?: string | null;
+ hours?: number;
+ textbookChapter?: string | null;
+ notes?: string | null;
+ completedAt?: string | null;
+ };
+ const planId = input.planId || "cp-unknown";
+ const now = new Date().toISOString();
+ const newItem = {
+ id: `cpi-${planId}-${Date.now()}`,
+ planId,
+ week: input.week ?? 1,
+ topic: input.topic ?? "",
+ content: input.content ?? null,
+ hours: input.hours ?? 2,
+ textbookChapter: input.textbookChapter ?? null,
+ notes: input.notes ?? null,
+ isCompleted: Boolean(input.completedAt),
+ completedAt: input.completedAt ?? null,
+ createdAt: now,
+ updatedAt: now,
+ };
+ if (!mockAdminCoursePlanItems[planId]) {
+ getCoursePlanItems(planId);
+ }
+ const arr = mockAdminCoursePlanItems[planId];
+ if (arr) {
+ arr.push(newItem);
+ }
+ return { data: { createCoursePlanItem: newItem } };
+ }
+
+ // UpdateCoursePlanItem:更新周计划项
+ case "UpdateCoursePlanItem": {
+ const _id = (variables?.id as string) ?? "";
+ const input = (variables?.input ?? {}) as {
+ week?: number;
+ topic?: string;
+ content?: string | null;
+ hours?: number;
+ textbookChapter?: string | null;
+ notes?: string | null;
+ completedAt?: string | null;
+ };
+ for (const planId of Object.keys(mockAdminCoursePlanItems)) {
+ const arr = mockAdminCoursePlanItems[planId];
+ if (!arr) continue;
+ const idx = arr.findIndex((it) => it.id === id);
+ if (idx >= 0) {
+ const existing = arr[idx];
+ if (!existing) continue;
+ const updated = {
+ id: existing.id,
+ planId: existing.planId,
+ week: input.week ?? existing.week,
+ topic: input.topic ?? existing.topic,
+ content: input.content ?? existing.content,
+ hours: input.hours ?? existing.hours,
+ textbookChapter: input.textbookChapter ?? existing.textbookChapter,
+ notes: input.notes ?? existing.notes,
+ isCompleted: existing.isCompleted,
+ completedAt: input.completedAt ?? existing.completedAt,
+ createdAt: existing.createdAt,
+ updatedAt: new Date().toISOString(),
+ };
+ arr[idx] = updated;
+ return { data: { updateCoursePlanItem: updated } };
+ }
+ }
+ return {
+ data: {
+ updateCoursePlanItem: {
+ id,
+ planId: "",
+ week: input.week ?? 0,
+ topic: input.topic ?? "",
+ content: input.content ?? null,
+ hours: input.hours ?? 0,
+ textbookChapter: input.textbookChapter ?? null,
+ notes: input.notes ?? null,
+ isCompleted: false,
+ completedAt: input.completedAt ?? null,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ },
+ },
+ };
+ }
+
+ // DeleteCoursePlanItem:删除周计划项
+ case "DeleteCoursePlanItem": {
+ const _id = (variables?.id as string) ?? "";
+ for (const planId of Object.keys(mockAdminCoursePlanItems)) {
+ const arr = mockAdminCoursePlanItems[planId];
+ if (!arr) continue;
+ const idx = arr.findIndex((it) => it.id === id);
+ if (idx >= 0) {
+ arr.splice(idx, 1);
+ break;
+ }
+ }
+ return { data: { deleteCoursePlanItem: { id, success: true } } };
+ }
+
+ // ToggleCoursePlanItemCompleted:切换周计划项完成状态
+ case "ToggleCoursePlanItemCompleted": {
+ const _id = (variables?.id as string) ?? "";
+ const isCompleted = (variables?.isCompleted as boolean) ?? false;
+ const now = new Date().toISOString();
+ for (const planId of Object.keys(mockAdminCoursePlanItems)) {
+ const arr = mockAdminCoursePlanItems[planId];
+ if (!arr) continue;
+ const idx = arr.findIndex((it) => it.id === id);
+ if (idx >= 0) {
+ const existing = arr[idx];
+ if (!existing) continue;
+ const updated = {
+ id: existing.id,
+ planId: existing.planId,
+ week: existing.week,
+ topic: existing.topic,
+ content: existing.content,
+ hours: existing.hours,
+ textbookChapter: existing.textbookChapter,
+ notes: existing.notes,
+ isCompleted,
+ completedAt: isCompleted ? now : null,
+ createdAt: existing.createdAt,
+ updatedAt: now,
+ };
+ arr[idx] = updated;
+ return {
+ data: {
+ toggleCoursePlanItemCompleted: {
+ id,
+ isCompleted,
+ completedAt: updated.completedAt,
+ updatedAt: now,
+ },
+ },
+ };
+ }
+ }
+ return {
+ data: {
+ toggleCoursePlanItemCompleted: {
+ id,
+ isCompleted,
+ completedAt: isCompleted ? now : null,
+ updatedAt: now,
+ },
+ },
+ };
+ }
+
+ // ReorderCoursePlanItems:批量重排序周计划项
+ case "ReorderCoursePlanItems": {
+ const planId = (variables?.planId as string) ?? "";
+ const items = (variables?.items ?? []) as Array<{
+ id: string;
+ week: number;
+ }>;
+ const now = new Date().toISOString();
+ const arr = mockAdminCoursePlanItems[planId];
+ if (arr) {
+ for (const reorderItem of items) {
+ const idx = arr.findIndex((it) => it.id === reorderItem.id);
+ if (idx >= 0) {
+ const existing = arr[idx];
+ if (!existing) continue;
+ arr[idx] = {
+ id: existing.id,
+ planId: existing.planId,
+ week: reorderItem.week,
+ topic: existing.topic,
+ content: existing.content,
+ hours: existing.hours,
+ textbookChapter: existing.textbookChapter,
+ notes: existing.notes,
+ isCompleted: existing.isCompleted,
+ completedAt: existing.completedAt,
+ createdAt: existing.createdAt,
+ updatedAt: now,
+ };
+ }
+ }
+ return {
+ data: {
+ reorderCoursePlanItems: arr.map((it) => ({
+ id: it.id,
+ week: it.week,
+ updatedAt: it.updatedAt,
+ })),
+ },
+ };
+ }
+ return { data: { reorderCoursePlanItems: [] } };
+ }
+
// GetSchools:学校列表
case "GetSchools":
return { data: { schools: mockSchools } };
// CreateSchool($input):mutation 兜底
case "CreateSchool":
return { data: { createSchool: { id: "sch-new", name: "新学校" } } };
+ // AdminUpdateSchool($id, $input):mutation 兜底(admin-p5 useAdminUpdateSchool)
+ case "AdminUpdateSchool": {
+ const _id = (variables?.id as string) ?? "";
+ return { data: { updateSchool: { id, name: "updated" } } };
+ }
// DeleteSchool($id):mutation 兜底
case "DeleteSchool": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteSchool: { id } } };
}
@@ -6995,12 +7968,12 @@ export function graphqlResponse(
};
// UpdateDepartment($id, $input):mutation 兜底
case "UpdateDepartment": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateDepartment: { id, name: "updated" } } };
}
// DeleteDepartment($id):mutation 兜底
case "DeleteDepartment": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteDepartment: { id } } };
}
@@ -7014,12 +7987,12 @@ export function graphqlResponse(
};
// UpdateAcademicYear($id, $input):mutation 兜底
case "UpdateAcademicYear": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateAcademicYear: { id, name: "updated" } } };
}
// DeleteAcademicYear($id):mutation 兜底
case "DeleteAcademicYear": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteAcademicYear: { id } } };
}
@@ -7058,20 +8031,138 @@ export function graphqlResponse(
],
},
};
+ // AdminCreateGrade($input):mutation 兜底(admin-p5 useAdminCreateGrade)
+ case "AdminCreateGrade":
+ return { data: { createGrade: { id: "grade-new", name: "新年级" } } };
// UpdateGrade($id, $input):mutation 兜底
case "UpdateGrade": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateGrade: { id, name: "updated" } } };
}
// DeleteGrade($id):mutation 兜底
case "DeleteGrade": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteGrade: { id } } };
}
+ // GetSchoolWideGradeSummary($gradeId):全校年级成绩洞察(@contract-pending)
+ // 用于 /shell/admin/school/grades/insights 年级洞察页
+ case "GetSchoolWideGradeSummary": {
+ const gId = (variables?.gradeId as string | undefined) ?? "";
+ const grades = mockAdminGrades
+ .filter((g) => !gId || g.id === gId)
+ .map((g, idx) => {
+ const baseScore = 70 + ((idx * 7) % 20);
+ return {
+ gradeId: g.id,
+ gradeName: g.name,
+ averageScore: baseScore + 5.4,
+ passRate: 0.86 + (idx % 3) * 0.03,
+ excellenceRate: 0.22 + (idx % 2) * 0.05,
+ participantCount: g.studentCount,
+ classRankings: mockAdminClasses
+ .filter((c) => c.gradeId === g.id)
+ .map((c, ci) => ({
+ classId: c.id,
+ className: c.name,
+ averageScore: baseScore + 3 + ci * 1.2,
+ passRate: 0.82 + ci * 0.04,
+ studentCount: c.studentCount,
+ rank: ci + 1,
+ delta: ci === 0 ? 1.8 : ci === 1 ? -0.6 : 0.4,
+ prevAvg: baseScore + 3 + ci * 1.2 - 1.2,
+ overallAvg: baseScore + 5.4,
+ })),
+ };
+ });
+ const allRankings = grades.flatMap((g) => g.classRankings);
+ const overallAvg =
+ allRankings.length > 0
+ ? allRankings.reduce((s, c) => s + c.averageScore, 0) /
+ allRankings.length
+ : 0;
+ return {
+ data: {
+ schoolWideGradeSummary: {
+ overallStats: {
+ averageScore: overallAvg,
+ passRate: 0.87,
+ excellenceRate: 0.25,
+ totalParticipants: mockAdminGrades.reduce(
+ (s, g) => s + g.studentCount,
+ 0,
+ ),
+ classes: mockAdminClasses.length,
+ students: mockAdminGrades.reduce((s, g) => s + g.studentCount, 0),
+ overallAvg,
+ latestAvg: overallAvg - 0.8,
+ },
+ grades,
+ recentAssignments: [
+ {
+ assignmentId: "asg-001",
+ title: "月度数学测验",
+ subjectName: "数学",
+ gradeName: "高三",
+ averageScore: 82.5,
+ submitCount: 38,
+ totalStudents: 40,
+ status: "graded",
+ createdAt: "2026-07-20T09:00:00Z",
+ targeted: 40,
+ graded: 38,
+ median: 81.0,
+ },
+ {
+ assignmentId: "asg-002",
+ title: "语文阅读理解练习",
+ subjectName: "语文",
+ gradeName: "高二",
+ averageScore: 76.8,
+ submitCount: 41,
+ totalStudents: 42,
+ status: "graded",
+ createdAt: "2026-07-19T14:30:00Z",
+ targeted: 42,
+ graded: 41,
+ median: 77.5,
+ },
+ {
+ assignmentId: "asg-003",
+ title: "英语听力训练",
+ subjectName: "英语",
+ gradeName: "高一",
+ averageScore: 85.2,
+ submitCount: 35,
+ totalStudents: 38,
+ status: "submitted",
+ createdAt: "2026-07-18T10:00:00Z",
+ targeted: 38,
+ graded: 0,
+ median: 0,
+ },
+ ],
+ },
+ },
+ };
+ }
+
// GetAdminClasses:管理端班级列表
case "GetAdminClasses":
return { data: { adminClasses: mockAdminClasses } };
+ // CreateAdminClass($input):mutation 兜底(admin-p5 useCreateAdminClass)
+ case "CreateAdminClass":
+ return { data: { createAdminClass: { id: "cls-new", name: "新班级" } } };
+ // UpdateAdminClass($id, $input):mutation 兜底(admin-p5 useUpdateAdminClass)
+ case "UpdateAdminClass": {
+ const _id = (variables?.id as string) ?? "";
+ return { data: { updateAdminClass: { id, name: "updated" } } };
+ }
+ // DeleteAdminClass($id):mutation 兜底(admin-p5 useDeleteAdminClass)
+ case "DeleteAdminClass": {
+ const _id = (variables?.id as string) ?? "";
+ return { data: { deleteAdminClass: { id } } };
+ }
// GetTeacherOptions:教师下拉选项
case "GetTeacherOptions":
@@ -7114,6 +8205,7 @@ export function graphqlResponse(
createdAt: "2026-07-22T00:00:00Z",
updatedAt: "2026-07-22T00:00:00Z",
authorName: "管理员",
+ readCount: 0,
grades: [],
classes: [],
},
@@ -7127,27 +8219,101 @@ export function graphqlResponse(
};
// UpdateAnnouncement($id, $input):mutation 兜底
case "UpdateAnnouncement": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateAnnouncement: { id, title: "updated" } } };
}
// DeleteAnnouncement($id):mutation 兜底
case "DeleteAnnouncement": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteAnnouncement: { id } } };
}
// ArchiveAnnouncement($id):归档公告 mutation 兜底
case "ArchiveAnnouncement": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { archiveAnnouncement: { id, status: "archived" } } };
}
// PinAnnouncement($id):置顶公告 mutation 兜底
case "PinAnnouncement": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return {
data: { pinAnnouncement: { id, pinnedAt: "2026-07-22T10:00:00Z" } },
};
}
+ // PublishAnnouncement($id): 发布公告 mutation 兜底
+ case "PublishAnnouncement": {
+ const _id = (variables?.id as string) ?? "";
+ return {
+ data: {
+ publishAnnouncement: {
+ id,
+ status: "published",
+ publishedAt: new Date().toISOString(),
+ },
+ },
+ };
+ }
+ // GetInvitationCodes: 邀请码列表
+ case "GetInvitationCodes":
+ return { data: { invitationCodes: mockInvitationCodes } };
+ // CreateInvitationCode($input): mutation 兜底
+ case "CreateInvitationCode":
+ return {
+ data: {
+ createInvitationCode: {
+ id: "inv-new",
+ code:
+ "EDU-NEW-" + Math.random().toString(36).slice(2, 8).toUpperCase(),
+ role: "teacher",
+ roleName: "教师",
+ classId: null,
+ className: null,
+ email: null,
+ batchId: "batch-new",
+ status: "unused",
+ usedCount: 0,
+ maxUses: 1,
+ usedBy: null,
+ usedByName: null,
+ usedAt: null,
+ expiresAt: "2026-08-31T23:59:59Z",
+ createdAt: new Date().toISOString(),
+ createdBy: "管理员",
+ },
+ },
+ };
+ // RevokeInvitationCode($id): mutation 兜底
+ case "RevokeInvitationCode": {
+ const _id = (variables?.id as string) ?? "";
+ return { data: { revokeInvitationCode: { id, status: "revoked" } } };
+ }
+ // GenerateInvitationCodes($input): 批量生成邀请码 mutation 兜底
+ case "GenerateInvitationCodes": {
+ const input = (variables?.input ?? {}) as { count?: number };
+ const count = input.count ?? 1;
+ const generated = Array.from({ length: count }, (_, i) => ({
+ id: "inv-gen-" + (i + 1),
+ code: "EDU-GEN-" + Math.random().toString(36).slice(2, 8).toUpperCase(),
+ status: "unused",
+ maxUses: 1,
+ usedCount: 0,
+ expiresAt: "2026-08-31T23:59:59Z",
+ }));
+ return {
+ data: {
+ generateInvitationCodes: {
+ success: true,
+ generatedCount: count,
+ generated,
+ },
+ },
+ };
+ }
+ // DeleteInvitationCodes($ids): 批量删除邀请码 mutation 兜底
+ case "DeleteInvitationCodes": {
+ const ids = (variables?.ids as string[]) ?? [];
+ return { data: { deleteInvitationCodes: ids.length } };
+ }
// GetFileAttachments:文件附件分页列表
case "GetFileAttachments":
return { data: { fileAttachments: mockFileAttachments } };
@@ -7177,14 +8343,27 @@ export function graphqlResponse(
};
// UpdateAiProvider($id, $input):mutation 兜底
case "UpdateAiProvider": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateAiProvider: { id, name: "updated" } } };
}
// DeleteAiProvider($id):mutation 兜底
case "DeleteAiProvider": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { deleteAiProvider: { id } } };
}
+ // TestAiProvider($id): 测试 AI Provider 连通性 mutation 兜底
+ case "TestAiProvider": {
+ const _id = (variables?.id as string) ?? "";
+ return {
+ data: {
+ testAiProvider: {
+ ok: true,
+ latencyMs: 128,
+ message: "连接成功",
+ },
+ },
+ };
+ }
// GetAiUsageDashboard:AI 用量看板
case "GetAiUsageDashboard":
@@ -7237,7 +8416,7 @@ export function graphqlResponse(
return { data: { viewports: mockViewports } };
// UpdateViewport($id, $input):mutation 兜底
case "UpdateViewport": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { updateViewport: { id, name: "updated" } } };
}
@@ -7255,10 +8434,11 @@ export function graphqlResponse(
// GetAdminCoursePlans:管理端课程计划分页列表
case "GetAdminCoursePlans":
return { data: { adminCoursePlans: mockAdminCoursePlans } };
- // GetAdminCoursePlan($id):单条课程计划详情(含 content)
+ // GetAdminCoursePlan($id):单条课程计划详情(含 content + items 周计划)
case "GetAdminCoursePlan": {
const cpId = (variables?.id as string | undefined) ?? "";
const listMatch = mockAdminCoursePlans.items.find((c) => c.id === cpId);
+ const items = getCoursePlanItems(cpId);
return {
data: {
adminCoursePlan: listMatch
@@ -7268,12 +8448,7 @@ export function graphqlResponse(
"# " +
listMatch.name +
"\n\n本课程计划涵盖本学期主要教学模块,按周拆分进度。",
- objectives: "掌握核心知识点并能综合应用",
- weeklySchedule: [
- { week: 1, topic: "模块一 导入", hours: 2 },
- { week: 2, topic: "模块二 基础", hours: 3 },
- { week: 3, topic: "模块三 进阶", hours: 3 },
- ],
+ items,
}
: {
id: cpId,
@@ -7285,7 +8460,7 @@ export function graphqlResponse(
description: "",
content: "",
objectives: "",
- weeklySchedule: [],
+ items,
createdAt: "2026-07-22T00:00:00Z",
updatedAt: "2026-07-22T00:00:00Z",
},
@@ -7302,37 +8477,81 @@ export function graphqlResponse(
standardId: "std-001",
standardName: "集合",
gradeId: "grade-12",
- coverage: 0.85,
+ gradeName: "高三",
+ coverageRate: 0.85,
+ lessonPlanCount: 17,
+ total: 20,
+ linked: 17,
},
{
standardId: "std-002",
standardName: "函数",
gradeId: "grade-12",
- coverage: 0.92,
+ gradeName: "高三",
+ coverageRate: 0.92,
+ lessonPlanCount: 23,
+ total: 25,
+ linked: 23,
},
{
standardId: "std-003",
standardName: "导数",
gradeId: "grade-12",
- coverage: 0.78,
+ gradeName: "高三",
+ coverageRate: 0.78,
+ lessonPlanCount: 14,
+ total: 18,
+ linked: 14,
},
{
standardId: "std-004",
standardName: "概率统计",
gradeId: "grade-12",
- coverage: 0.65,
+ gradeName: "高三",
+ coverageRate: 0.65,
+ lessonPlanCount: 13,
+ total: 20,
+ linked: 13,
},
{
standardId: "std-005",
standardName: "立体几何",
gradeId: "grade-11",
- coverage: 0.88,
+ gradeName: "高二",
+ coverageRate: 0.88,
+ lessonPlanCount: 22,
+ total: 25,
+ linked: 22,
},
{
standardId: "std-006",
standardName: "解析几何",
gradeId: "grade-11",
- coverage: 0.72,
+ gradeName: "高二",
+ coverageRate: 0.72,
+ lessonPlanCount: 18,
+ total: 25,
+ linked: 18,
+ },
+ {
+ standardId: "std-001",
+ standardName: "集合",
+ gradeId: "grade-11",
+ gradeName: "高二",
+ coverageRate: 0.1,
+ lessonPlanCount: 2,
+ total: 20,
+ linked: 2,
+ },
+ {
+ standardId: "std-007",
+ standardName: "三角函数",
+ gradeId: "grade-10",
+ gradeName: "高一",
+ coverageRate: 0,
+ lessonPlanCount: 0,
+ total: 15,
+ linked: 0,
},
],
},
@@ -7352,6 +8571,29 @@ export function graphqlResponse(
},
};
+ // GetElectiveOverviewStats:选修课总览统计
+ case "GetElectiveOverviewStats":
+ return {
+ data: {
+ electiveOverviewStats: {
+ totalCourses: mockAdminElectives.items.length,
+ totalCapacity: mockAdminElectives.items.reduce(
+ (s, e) => s + (e.capacity ?? 0),
+ 0,
+ ),
+ totalEnrolled: mockAdminElectives.items.reduce(
+ (s, e) => s + (e.enrolledCount ?? 0),
+ 0,
+ ),
+ totalDraft: mockAdminElectives.items.filter(
+ (e) => e.status === "DRAFT",
+ ).length,
+ totalOpen: mockAdminElectives.items.filter(
+ (e) => e.status === "OPEN" || e.status === "PUBLISHED",
+ ).length,
+ },
+ },
+ };
// GetAdminElectives:管理端选修课分页列表
case "GetAdminElectives":
return { data: { adminElectives: mockAdminElectives } };
@@ -7369,19 +8611,25 @@ export function graphqlResponse(
studentId: "stu-001",
studentName: "张明",
selectedAt: "2026-07-10T10:00:00Z",
+ enrolledAt: "2026-07-10T10:00:00Z",
status: "confirmed",
+ priority: 1,
},
{
studentId: "stu-002",
studentName: "李华",
selectedAt: "2026-07-11T14:00:00Z",
+ enrolledAt: "2026-07-11T14:00:00Z",
status: "confirmed",
+ priority: 2,
},
{
studentId: "stu-003",
studentName: "王芳",
selectedAt: "2026-07-12T09:30:00Z",
+ enrolledAt: "2026-07-12T09:30:00Z",
status: "pending",
+ priority: 3,
},
],
}
@@ -7409,6 +8657,37 @@ export function graphqlResponse(
case "GetAdminQuestions":
return { data: { adminQuestions: mockAdminQuestions } };
+ // GetAdminQuestion($id):管理端题目单条详情(@contract-pending MSW 兜底)
+ // 列表项中缺少 answer/explanation/knowledgePointTitle/textbookTitle 等字段,
+ // 在此根据列表项 id 派生完整详情;找不到时返回兜底详情。
+ case "GetAdminQuestion": {
+ const qId = (variables?.id as string | undefined) ?? "";
+ const listMatch = mockAdminQuestions.items.find((q) => q.id === qId);
+ return {
+ data: {
+ adminQuestion: {
+ id: qId,
+ type: listMatch?.type ?? "single_choice",
+ content: listMatch?.content ?? "",
+ difficulty: listMatch?.difficulty ?? "medium",
+ subjectId: "sub-math",
+ subjectName: "数学",
+ textbookId: "tb-001",
+ textbookTitle: "高中数学必修一",
+ knowledgePointId: "kp-001",
+ knowledgePointTitle: "集合与函数概念",
+ status: listMatch?.status ?? "DRAFT",
+ answer: "1/2",
+ explanation: "sin(30°) = 1/2,是三角函数中的基础恒等式。",
+ source: listMatch?.source ?? null,
+ createdAt: listMatch?.createdAt ?? "2026-07-15T08:00:00Z",
+ updatedAt: listMatch?.createdAt ?? "2026-07-15T08:00:00Z",
+ createdBy: listMatch?.authorName ?? "张老师",
+ },
+ },
+ };
+ }
+
// GetAdminLessonPlans:管理端教案分页列表
case "GetAdminLessonPlans":
return { data: { adminLessonPlans: mockAdminLessonPlans } };
@@ -7484,6 +8763,43 @@ export function graphqlResponse(
errorRate: 0.22,
},
],
+ byClass: [
+ {
+ classId: "cls-001",
+ className: "高三(1)班",
+ errorCount: 1280,
+ questionCount: 510,
+ errorRate: 0.38,
+ },
+ {
+ classId: "cls-002",
+ className: "高三(2)班",
+ errorCount: 1120,
+ questionCount: 470,
+ errorRate: 0.35,
+ },
+ {
+ classId: "cls-003",
+ className: "高二(1)班",
+ errorCount: 980,
+ questionCount: 420,
+ errorRate: 0.31,
+ },
+ {
+ classId: "cls-004",
+ className: "高二(2)班",
+ errorCount: 860,
+ questionCount: 380,
+ errorRate: 0.28,
+ },
+ {
+ classId: "cls-005",
+ className: "高一(1)班",
+ errorCount: 720,
+ questionCount: 320,
+ errorRate: 0.25,
+ },
+ ],
topStudents: [
{
studentId: "stu-004",
@@ -7523,11 +8839,207 @@ export function graphqlResponse(
errorCount: 118,
errorRate: 0.68,
},
+ {
+ questionId: "q-012",
+ content: "求函数 y=log₂(x-1) 的定义域。",
+ errorCount: 112,
+ errorRate: 0.65,
+ },
+ {
+ questionId: "q-015",
+ content: "已知向量 a=(1,2),b=(3,1),求 a·b。",
+ errorCount: 108,
+ errorRate: 0.63,
+ },
+ {
+ questionId: "q-019",
+ content: "等差数列前 n 项和公式推导。",
+ errorCount: 102,
+ errorRate: 0.61,
+ },
+ {
+ questionId: "q-022",
+ content: "求极限 lim(x→0) sin(x)/x 的值。",
+ errorCount: 98,
+ errorRate: 0.59,
+ },
+ {
+ questionId: "q-025",
+ content: "已知复数 z=1+i,求 z² 的值。",
+ errorCount: 95,
+ errorRate: 0.57,
+ },
+ {
+ questionId: "q-028",
+ content: "二项式定理 (a+b)⁵ 展开式中 a³b² 的系数。",
+ errorCount: 92,
+ errorRate: 0.55,
+ },
+ {
+ questionId: "q-031",
+ content: "三角函数 sin²θ + cos²θ = ?",
+ errorCount: 88,
+ errorRate: 0.52,
+ },
+ {
+ questionId: "q-034",
+ content: "求抛物线 y²=4x 的焦点坐标。",
+ errorCount: 85,
+ errorRate: 0.5,
+ },
+ {
+ questionId: "q-037",
+ content: "已知椭圆方程 x²/9 + y²/4 = 1,求离心率。",
+ errorCount: 80,
+ errorRate: 0.48,
+ },
+ {
+ questionId: "q-040",
+ content: "排列组合:从 5 人中选 3 人参加活动的方案数。",
+ errorCount: 76,
+ errorRate: 0.45,
+ },
+ {
+ questionId: "q-043",
+ content: "导数应用:求函数 y=x³-3x+1 的极值点。",
+ errorCount: 72,
+ errorRate: 0.43,
+ },
+ {
+ questionId: "q-046",
+ content:
+ "概率题:袋中有 3 红 2 白球,任取 2 球均为红色的概率。",
+ errorCount: 68,
+ errorRate: 0.4,
+ },
+ {
+ questionId: "q-049",
+ content: "立体几何:求正方体对角线长度(边长为 a)。",
+ errorCount: 64,
+ errorRate: 0.38,
+ },
+ {
+ questionId: "q-052",
+ content: "三角恒等变换:cos(α-β) 的展开式。",
+ errorCount: 60,
+ errorRate: 0.36,
+ },
+ {
+ questionId: "q-055",
+ content: "数列:已知 a₁=1,aₙ₊₁=2aₙ+1,求通项公式。",
+ errorCount: 56,
+ errorRate: 0.34,
+ },
+ {
+ questionId: "q-058",
+ content: "不等式:解 |x-2| + |x+1| < 5。",
+ errorCount: 52,
+ errorRate: 0.31,
+ },
+ {
+ questionId: "q-061",
+ content: "圆锥曲线:双曲线 x²/4 - y²/9 = 1 的渐近线方程。",
+ errorCount: 48,
+ errorRate: 0.29,
+ },
+ {
+ questionId: "q-064",
+ content: "三角函数图像:y=sin(2x+π/3) 的周期与振幅。",
+ errorCount: 44,
+ errorRate: 0.27,
+ },
+ {
+ questionId: "q-067",
+ content: "对数运算:log₃81 + log₃9 的值。",
+ errorCount: 40,
+ errorRate: 0.24,
+ },
+ {
+ questionId: "q-070",
+ content: "向量运算:已知 |a|=3,|b|=4,a⊥b,求 |a+b|。",
+ errorCount: 36,
+ errorRate: 0.22,
+ },
+ {
+ questionId: "q-073",
+ content: "微积分:∫(2x+1)dx 的不定积分。",
+ errorCount: 32,
+ errorRate: 0.19,
+ },
+ {
+ questionId: "q-076",
+ content: "矩阵运算:2×2 矩阵的乘法示例。",
+ errorCount: 28,
+ errorRate: 0.17,
+ },
],
},
},
};
+ // ExportErrorBookCsv($filter):错题本 CSV 导出 mutation(@contract-pending MSW 兜底)
+ // 按当前 mock stats 数据构造 CSV 字符串,支持 subjectId/classId 过滤。
+ case "ExportErrorBookCsv": {
+ const filter =
+ (variables?.filter as
+ | {
+ subjectId?: string | null;
+ classId?: string | null;
+ }
+ | undefined) ?? {};
+ const statsResult = graphqlResponse("GetAdminErrorBookStats", {}) as {
+ data?: { adminErrorBookStats?: Record };
+ };
+ const stats = statsResult.data?.adminErrorBookStats;
+ const byClass = (
+ stats?.byClass as Array<{
+ classId: string;
+ className: string;
+ errorCount: number;
+ questionCount: number;
+ errorRate: number;
+ }>
+ ).filter((c) => !filter.classId || c.classId === filter.classId);
+ const bySubject = (
+ stats?.bySubject as Array<{
+ subjectId: string;
+ subjectName: string;
+ errorCount: number;
+ questionCount: number;
+ errorRate: number;
+ }>
+ ).filter((s) => !filter.subjectId || s.subjectId === filter.subjectId);
+ const header =
+ "class_id,class_name,subject_id,subject_name,error_count,question_count,error_rate";
+ const rows: string[] = [];
+ for (const cls of byClass) {
+ for (const subj of bySubject) {
+ rows.push(
+ [
+ cls.classId,
+ escapeCsvField(cls.className),
+ subj.subjectId,
+ escapeCsvField(subj.subjectName),
+ String(cls.errorCount),
+ String(subj.questionCount),
+ String(subj.errorRate),
+ ].join(","),
+ );
+ }
+ }
+ const csv = `${header}\n${rows.join("\n")}`;
+ return {
+ data: {
+ exportErrorBookCsv: {
+ success: true,
+ count: rows.length,
+ filename: `error-book-${new Date().toISOString().slice(0, 10)}.csv`,
+ csv,
+ },
+ },
+ };
+ }
+
// GetAdminScheduleChanges:管理端课表变更分页列表
case "GetAdminScheduleChanges":
return { data: { adminScheduleChanges: mockAdminScheduleChanges } };
@@ -7603,12 +9115,12 @@ export function graphqlResponse(
}
// ApproveScheduleChange($id):审批通过 mutation 兜底
case "ApproveScheduleChange": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { approveScheduleChange: { id, status: "approved" } } };
}
// RejectScheduleChange($id):审批驳回 mutation 兜底
case "RejectScheduleChange": {
- const id = (variables?.id as string) ?? "";
+ const _id = (variables?.id as string) ?? "";
return { data: { rejectScheduleChange: { id, status: "rejected" } } };
}
// AutoSchedule($classId):自动排课 mutation 兜底
@@ -7665,9 +9177,36 @@ export function graphqlResponse(
},
};
- // GetAdminAttendanceRecords:管理端考勤记录分页列表
- case "GetAdminAttendanceRecords":
- return { data: { adminAttendanceRecords: mockAdminAttendanceRecords } };
+ // GetAdminAttendanceRecords:管理端考勤记录分页列表(支持 classId/status/date/gradeId 过滤 + limit/offset 分页)
+ case "GetAdminAttendanceRecords": {
+ const filter =
+ (variables?.filter as
+ | {
+ classId?: string | null;
+ status?: string | null;
+ date?: string | null;
+ gradeId?: string | null;
+ }
+ | undefined) ?? {};
+ const limit = (variables?.limit as number | undefined) ?? 100;
+ const offset = (variables?.offset as number | undefined) ?? 0;
+ const filtered = mockAdminAttendanceRecords.items.filter((r) => {
+ if (filter.classId && r.classId !== filter.classId) return false;
+ if (filter.status && r.status !== filter.status) return false;
+ if (filter.date && r.date !== filter.date) return false;
+ if (filter.gradeId && r.gradeId !== filter.gradeId) return false;
+ return true;
+ });
+ const paged = filtered.slice(offset, offset + Math.max(0, limit));
+ return {
+ data: {
+ adminAttendanceRecords: {
+ items: paged,
+ total: filtered.length,
+ },
+ },
+ };
+ }
// GetAttendanceGradeCorrelation:考勤-成绩相关性
case "GetAttendanceGradeCorrelation":
@@ -7759,8 +9298,25 @@ export function graphqlResponse(
return { data: mockStudentPractice };
case "GetStudentPracticeSession":
return { data: mockStudentPracticeSession };
- case "GetStudentLeave":
- return { data: mockStudentLeave };
+ case "GetStudentLeave": {
+ const page = (variables?.page as number | undefined) ?? 1;
+ const pageSize = (variables?.pageSize as number | undefined) ?? 20;
+ const start = (page - 1) * pageSize;
+ const end = start + pageSize;
+ const pagedItems = mockStudentLeaveItems.slice(start, end);
+ return {
+ data: {
+ studentLeave: {
+ items: pagedItems,
+ total: mockStudentLeaveItems.length,
+ page,
+ pageSize,
+ },
+ },
+ };
+ }
+ case "GetUserProfile":
+ return { data: mockUserProfile };
case "GetStudentMessages":
return { data: mockStudentMessages };
case "GetStudentAnnouncements":
diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md
index e6c04d7..4fa3d09 100644
--- a/docs/troubleshooting/known-issues.md
+++ b/docs/troubleshooting/known-issues.md
@@ -767,3 +767,12 @@
| React 19 use() + Suspense jsdom 测试 | `use(promise)` 在 jsdom 中 promise resolve 后不自动触发重新渲染;需用 `await act(async () => { render(...); await Promise.resolve(); })` 包裹 render,并设置 `globalThis.IS_REACT_ACT_ENVIRONMENT = true`(setup 文件) |
| 前端错误上报生产端点 | api-gateway 在 `/api/v1/log` 直接处理(不代理到下游),slog 结构化 JSON 日志,64KB body 限制,返回 204;`useErrorReport` 按 `process.env.NODE_ENV` 切换:production→`/api/v1/log`,development→`/api/log`(Next.js API route mock) |
| vitest setup 文件配置 | `vitest.config.ts` 的 `setupFiles: ["src/__tests__/setup.ts"]` 注册 `@testing-library/jest-dom/vitest` matchers + 设置 `IS_REACT_ACT_ENVIRONMENT`;`declare global { var IS_REACT_ACT_ENVIRONMENT: boolean \| undefined }` 补类型签名 |
+| 管理域 GraphQL operation 命名冲突 | admin.graphql.ts 与 grades.graphql.ts/school-settings.graphql.ts 同名 mutation(CreateGrade/UpdateSchool 等)导致 codegen 重复声明;前缀化命名 `AdminCreateGrade`/`AdminUpdateSchool`/`AdminGetSchedulingRules` 区分 |
+| 管理域 invitation-codes 纯函数时间漂移 | 纯函数 `isInvitationExpired`/`getEffectiveStatus`/`isInvitationRevocable` 调用 `Date.now()` 会造成 SSR/CSR hydration mismatch;参数化注入 `now: number` 由 server page 传入 |
+| 管理域 StatusBadge 虚假推断状态 | `audit-logs` StatusBadge 用 `log.details ? "success" : ""` 推断 status 违反数据真实性原则;改用 schema 字段 `log.status`,未就绪时显示 "--" 占位符 |
+| 管理域未实现功能降级模式 | 未实现的 CRUD(如 questions 创建/导入导出、students/teachers/organization 详情)改为按钮 + `notify.info(t("mswNotice"))` 占位,避免死链;不创建实际路由 |
+| 管理域 @contract-pending 契约标注规则 | schema 未就绪字段在文件头注释 `@contract-pending: <工单>` + `§11.4 登记`;禁止在代码注释中声称 schema 已就绪而实际走 MSW 兜底(plugins 模块原标注造假已修正) |
+| 管理域 announcements grades 关联编辑 | AnnouncementInput.grades 字段已有 schema 定义;edit 表单用逗号分隔文本输入(简化版),避免下拉多选依赖 useGrades hook;提交时 `split(",").map(trim).filter(Boolean)` 解析 |
+| 管理域 audit-logs 分页 | ListPageShell pagination slot 支持 prev/next 按钮 + 当前页/总页数显示;URL 状态 `?page=N`,由 `updateQuery("page", ...)` 控制;总页数 `Math.ceil(total / pageSize)` |
+| 管理域 ai-settings 双权限注释 | AI_CHAT(普通用户访问 private provider)+ AI_CONFIGURE(管理员访问 public/他人 provider);admin 路由仅放行 ["admin"],管理员隐含 AI_CONFIGURE;普通用户 AI_CHAT 走 `/shell/ai-settings`(非 admin 域) |
+| 管理域 viewports DataScope 注释 | 视口配置属管理员全局视角(DataScope = "all"),不随班级/年级范围收窄;route-permissions.ts 仅放行 ["admin"];无需 ctx.dataScope 上下文 |
|