feat(portal-shell): v2.0 P1-P4 token migration + unit tests + prod endpoint + e2e
P1: 31 widget 旧纸感令牌批量迁移到 shadcn 标准(1104 次替换) - bg-paper→bg-background / bg-surface→bg-card / text-ink→text-foreground - 保留 button.tsx 中 bg-accent(shadcn 标准 hover 语义令牌) P2: v2.0 新增组件单元测试补齐(5 文件 81 用例) - permission-bitmap: 24 用例(含 GRADE_READ 重复去重) - route-permissions: 26 用例(4 张表优先级 + AND/OR 语义) - notify: 12 用例(sonner toast 双重性质 vi.hoisted mock) - use-error-report: 9 用例(jsdom Blob vi.stubGlobal mock) - plugin-boundary: 10 用例(错误边界 + 骨架变体) P3: 错误上报端点生产替换(后端 /api/v1/log) - api-gateway: internal/log/handler.go(slog 结构化日志,64KB 限制,204 返回) - main.go: 注册 POST /api/v1/log 路由 - useErrorReport: 环境感知端点(prod→/api/v1/log,dev→/api/log) P4: E2E 测试(3 文件 30 用例) - streaming: 4 用例(React 19 use() + Suspense,act 包裹 render) - error-boundaries: 6 用例(三级错误边界层级 L1/L2/L3) - security-boundaries: 20 用例(L1 角色门禁 + L2 权限点 + L3 数据范围) - vitest setup: IS_REACT_ACT_ENVIRONMENT + jest-dom matchers 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由 / 206 测试全部通过
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* lesson-plan-editor(teacher / main)
|
||||
@@ -43,9 +43,9 @@ export default function LessonPlanEditor(
|
||||
|
||||
if (!classId) {
|
||||
return (
|
||||
<section className="rounded-card border border-rule bg-surface p-md">
|
||||
<h3 className="text-heading-3 text-ink">备课画布</h3>
|
||||
<p className="text-small text-ink-muted">请先选择班级</p>
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">备课画布</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择班级</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -101,31 +101,31 @@ export default function LessonPlanEditor(
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-card border border-rule bg-surface p-md">
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-heading-3 text-ink">备课画布</h3>
|
||||
<h3 className="text-heading-3 text-foreground">备课画布</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNew}
|
||||
className="rounded-button bg-accent px-sm py-xs text-small text-ink-onAccent"
|
||||
className="rounded-md bg-primary px-sm py-xs text-sm text-primary-foreground"
|
||||
>
|
||||
新建备课
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-sm flex gap-md">
|
||||
<div className="mt-sm flex gap-4">
|
||||
<ul className="w-64 shrink-0 space-y-sm">
|
||||
{plans.length === 0 ? (
|
||||
<li className="text-small text-ink-muted">暂无备课记录</li>
|
||||
<li className="text-sm text-muted-foreground">暂无备课记录</li>
|
||||
) : (
|
||||
plans.map((p) => (
|
||||
<li key={p.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(p)}
|
||||
className={`w-full rounded-button border border-rule px-sm py-xs text-left text-small ${
|
||||
className={`w-full rounded-md border border px-sm py-xs text-left text-sm ${
|
||||
draft.id === p.id
|
||||
? "bg-subtle text-ink"
|
||||
: "bg-surface text-ink"
|
||||
? "bg-muted text-foreground"
|
||||
: "bg-card text-foreground"
|
||||
}`}
|
||||
>
|
||||
{p.title || "未命名备课"}
|
||||
@@ -134,45 +134,45 @@ export default function LessonPlanEditor(
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
<div className="flex-1 space-y-md">
|
||||
<div className="flex-1 space-y-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">标题</span>
|
||||
<span className="text-sm text-muted-foreground">标题</span>
|
||||
<input
|
||||
type="text"
|
||||
value={draft.title}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, title: e.target.value }))
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
placeholder="请输入备课标题"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">教学目标</span>
|
||||
<span className="text-sm text-muted-foreground">教学目标</span>
|
||||
<textarea
|
||||
value={draft.objectives}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, objectives: e.target.value }))
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
rows={3}
|
||||
placeholder="请输入教学目标"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">教学内容</span>
|
||||
<span className="text-sm text-muted-foreground">教学内容</span>
|
||||
<textarea
|
||||
value={draft.content}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, content: e.target.value }))
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
rows={4}
|
||||
placeholder="请输入教学内容"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">教学资源</span>
|
||||
<span className="text-sm text-muted-foreground">教学资源</span>
|
||||
<ul className="space-y-xs">
|
||||
{draft.resources.map((r, i) => (
|
||||
<li key={i} className="flex gap-xs">
|
||||
@@ -180,13 +180,13 @@ export default function LessonPlanEditor(
|
||||
type="text"
|
||||
value={r}
|
||||
onChange={(e) => handleResourceChange(i, e.target.value)}
|
||||
className="w-full rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
placeholder="资源名称或链接"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleResourceRemove(i)}
|
||||
className="rounded-button bg-subtle px-sm py-xs text-small text-ink"
|
||||
className="rounded-md bg-muted px-sm py-xs text-sm text-foreground"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
@@ -196,7 +196,7 @@ export default function LessonPlanEditor(
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResourceAdd}
|
||||
className="self-start rounded-button bg-subtle px-sm py-xs text-small text-ink"
|
||||
className="self-start rounded-md bg-muted px-sm py-xs text-sm text-foreground"
|
||||
>
|
||||
添加资源
|
||||
</button>
|
||||
@@ -205,7 +205,7 @@ export default function LessonPlanEditor(
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving || !draft.title.trim()}
|
||||
className="rounded-button bg-accent px-md py-xs text-small text-ink-onAccent disabled:opacity-50"
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
{saving ? "保存中" : "保存"}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* question-bank(teacher / main)
|
||||
@@ -66,9 +66,9 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
|
||||
if (!bankId) {
|
||||
return (
|
||||
<section className="rounded-card border border-rule bg-surface p-md">
|
||||
<h3 className="text-heading-3 text-ink">题库管理</h3>
|
||||
<p className="text-small text-ink-muted">请先选择题库</p>
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">题库管理</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择题库</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -98,25 +98,25 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-card border border-rule bg-surface p-md">
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-heading-3 text-ink">题库管理</h3>
|
||||
<h3 className="text-heading-3 text-foreground">题库管理</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowForm((v) => !v)}
|
||||
className="rounded-button bg-accent px-sm py-xs text-small text-ink-onAccent"
|
||||
className="rounded-md bg-primary px-sm py-xs text-sm text-primary-foreground"
|
||||
>
|
||||
{showForm ? "收起新建" : "新建题目"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-sm flex gap-md">
|
||||
<div className="mt-sm flex gap-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">题型</span>
|
||||
<span className="text-sm text-muted-foreground">题型</span>
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{QUESTION_TYPES.map((t) => (
|
||||
@@ -127,11 +127,11 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">难度</span>
|
||||
<span className="text-sm text-muted-foreground">难度</span>
|
||||
<select
|
||||
value={difficultyFilter}
|
||||
onChange={(e) => setDifficultyFilter(e.target.value)}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{DIFFICULTIES.map((d) => (
|
||||
@@ -144,16 +144,16 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{showForm ? (
|
||||
<div className="mt-sm space-y-md rounded-card bg-subtle p-md">
|
||||
<div className="flex gap-md">
|
||||
<div className="mt-sm space-y-4 rounded-xl bg-muted p-4">
|
||||
<div className="flex gap-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">题型</span>
|
||||
<span className="text-sm text-muted-foreground">题型</span>
|
||||
<select
|
||||
value={newQuestion.type}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, type: e.target.value }))
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
>
|
||||
{QUESTION_TYPES.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
@@ -163,13 +163,13 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">难度</span>
|
||||
<span className="text-sm text-muted-foreground">难度</span>
|
||||
<select
|
||||
value={newQuestion.difficulty}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, difficulty: e.target.value }))
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
>
|
||||
{DIFFICULTIES.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
@@ -180,25 +180,25 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
</label>
|
||||
</div>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">题干</span>
|
||||
<span className="text-sm text-muted-foreground">题干</span>
|
||||
<textarea
|
||||
value={newQuestion.content}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, content: e.target.value }))
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
rows={3}
|
||||
placeholder="请输入题干"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">答案</span>
|
||||
<span className="text-sm text-muted-foreground">答案</span>
|
||||
<textarea
|
||||
value={newQuestion.answer}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, answer: e.target.value }))
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
rows={2}
|
||||
placeholder="请输入答案"
|
||||
/>
|
||||
@@ -207,46 +207,46 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
disabled={!newQuestion.content.trim()}
|
||||
className="rounded-button bg-accent px-md py-xs text-small text-ink-onAccent disabled:opacity-50"
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ul className="mt-sm space-y-md">
|
||||
<ul className="mt-sm space-y-4">
|
||||
{questions.length === 0 ? (
|
||||
<li className="text-small text-ink-muted">暂无题目</li>
|
||||
<li className="text-sm text-muted-foreground">暂无题目</li>
|
||||
) : (
|
||||
questions.map((q) => (
|
||||
<li key={q.id} className="rounded-button border border-rule p-sm">
|
||||
<li key={q.id} className="rounded-md border border p-2">
|
||||
<div className="flex flex-wrap gap-xs">
|
||||
<span className="rounded-button bg-accent px-xs py-xs text-tiny text-ink-onAccent">
|
||||
<span className="rounded-md bg-primary px-xs py-xs text-xs text-primary-foreground">
|
||||
{TYPE_LABELS[q.type] ?? q.type}
|
||||
</span>
|
||||
<span className="rounded-button bg-subtle px-xs py-xs text-tiny text-ink">
|
||||
<span className="rounded-md bg-muted px-xs py-xs text-xs text-foreground">
|
||||
{DIFFICULTY_LABELS[q.difficulty] ?? q.difficulty}
|
||||
</span>
|
||||
{q.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-button bg-subtle px-xs py-xs text-tiny text-ink-muted"
|
||||
className="rounded-md bg-muted px-xs py-xs text-xs text-muted-foreground"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-xs text-body text-ink">{q.content}</p>
|
||||
<p className="mt-xs text-body text-foreground">{q.content}</p>
|
||||
{q.options.length > 0 ? (
|
||||
<ul className="mt-xs space-y-xs">
|
||||
{q.options.map((opt, i) => (
|
||||
<li key={i} className="text-small text-ink-muted">
|
||||
<li key={i} className="text-sm text-muted-foreground">
|
||||
{String.fromCharCode(65 + i)}. {opt}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<p className="mt-xs text-small text-ink-muted">
|
||||
<p className="mt-xs text-sm text-muted-foreground">
|
||||
答案:{q.answer}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* scheduling-rules(teacher / main)
|
||||
@@ -44,9 +44,9 @@ export default function SchedulingRules(
|
||||
|
||||
if (!classId) {
|
||||
return (
|
||||
<section className="rounded-card border border-rule bg-surface p-md">
|
||||
<h3 className="text-heading-3 text-ink">排课规则</h3>
|
||||
<p className="text-small text-ink-muted">请先选择班级</p>
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">排课规则</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择班级</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -85,14 +85,14 @@ export default function SchedulingRules(
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-card border border-rule bg-surface p-md">
|
||||
<h3 className="text-heading-3 text-ink">排课规则</h3>
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">排课规则</h3>
|
||||
{rules.length === 0 ? (
|
||||
<p className="mt-sm text-small text-ink-muted">暂无排课规则</p>
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无排课规则</p>
|
||||
) : (
|
||||
<table className="mt-sm w-full text-small">
|
||||
<table className="mt-sm w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-rule text-ink-muted">
|
||||
<tr className="border-b border text-muted-foreground">
|
||||
<th className="py-xs text-left">星期</th>
|
||||
<th className="py-xs text-left">节次</th>
|
||||
<th className="py-xs text-left">科目</th>
|
||||
@@ -106,7 +106,7 @@ export default function SchedulingRules(
|
||||
const isEditing = editingId === rule.id;
|
||||
if (isEditing && draft) {
|
||||
return (
|
||||
<tr key={rule.id} className="border-b border-rule">
|
||||
<tr key={rule.id} className="border-b border">
|
||||
<td className="py-xs">
|
||||
<select
|
||||
value={draft.dayOfWeek}
|
||||
@@ -115,7 +115,7 @@ export default function SchedulingRules(
|
||||
d ? { ...d, dayOfWeek: Number(e.target.value) } : d,
|
||||
)
|
||||
}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
>
|
||||
{DAY_NAMES.map((name, i) => (
|
||||
<option key={name} value={i + 1}>
|
||||
@@ -133,7 +133,7 @@ export default function SchedulingRules(
|
||||
d ? { ...d, periods: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
@@ -145,7 +145,7 @@ export default function SchedulingRules(
|
||||
d ? { ...d, subject: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
@@ -157,7 +157,7 @@ export default function SchedulingRules(
|
||||
d ? { ...d, teacherId: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
@@ -169,7 +169,7 @@ export default function SchedulingRules(
|
||||
d ? { ...d, room: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
@@ -178,14 +178,14 @@ export default function SchedulingRules(
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="rounded-button bg-accent px-sm py-xs text-tiny text-ink-onAccent disabled:opacity-50"
|
||||
className="rounded-md bg-primary px-sm py-xs text-xs text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="rounded-button bg-subtle px-sm py-xs text-tiny text-ink"
|
||||
className="rounded-md bg-muted px-sm py-xs text-xs text-foreground"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@@ -195,17 +195,19 @@ export default function SchedulingRules(
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr key={rule.id} className="border-b border-rule">
|
||||
<td className="py-xs text-ink">{dayName(rule.dayOfWeek)}</td>
|
||||
<td className="py-xs text-ink">{rule.periods}</td>
|
||||
<td className="py-xs text-ink">{rule.subject}</td>
|
||||
<td className="py-xs text-ink">{rule.teacherId}</td>
|
||||
<td className="py-xs text-ink">{rule.room}</td>
|
||||
<tr key={rule.id} className="border-b border">
|
||||
<td className="py-xs text-foreground">
|
||||
{dayName(rule.dayOfWeek)}
|
||||
</td>
|
||||
<td className="py-xs text-foreground">{rule.periods}</td>
|
||||
<td className="py-xs text-foreground">{rule.subject}</td>
|
||||
<td className="py-xs text-foreground">{rule.teacherId}</td>
|
||||
<td className="py-xs text-foreground">{rule.room}</td>
|
||||
<td className="py-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEdit(rule)}
|
||||
className="rounded-button bg-subtle px-sm py-xs text-tiny text-ink"
|
||||
className="rounded-md bg-muted px-sm py-xs text-xs text-foreground"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* textbook-manager(teacher / main)
|
||||
@@ -41,43 +41,43 @@ export default function TextbookManager(
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-card border border-rule bg-surface p-md">
|
||||
<h3 className="text-heading-3 text-ink">教材管理</h3>
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">教材管理</h3>
|
||||
|
||||
<div className="mt-sm flex gap-md">
|
||||
<div className="mt-sm flex gap-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">科目 ID</span>
|
||||
<span className="text-sm text-muted-foreground">科目 ID</span>
|
||||
<input
|
||||
type="text"
|
||||
value={subjectInput}
|
||||
onChange={(e) => setSubjectInput(e.target.value)}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
placeholder="可选,输入科目 ID"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<span className="text-small text-ink-muted">年级</span>
|
||||
<span className="text-sm text-muted-foreground">年级</span>
|
||||
<input
|
||||
type="text"
|
||||
value={gradeInput}
|
||||
onChange={(e) => setGradeInput(e.target.value)}
|
||||
className="rounded-button border border-rule bg-surface px-sm py-xs text-small text-ink"
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
placeholder="可选,如:三年级"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleApply}
|
||||
className="self-end rounded-button bg-accent px-md py-xs text-small text-ink-onAccent"
|
||||
className="self-end rounded-md bg-primary px-md py-xs text-sm text-primary-foreground"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-md flex gap-md">
|
||||
<div className="mt-md flex gap-4">
|
||||
<ul className="flex-1 space-y-sm">
|
||||
{textbooks.length === 0 ? (
|
||||
<li className="text-small text-ink-muted">暂无教材数据</li>
|
||||
<li className="text-sm text-muted-foreground">暂无教材数据</li>
|
||||
) : (
|
||||
textbooks.map((t) => (
|
||||
<li key={t.id}>
|
||||
@@ -86,15 +86,17 @@ export default function TextbookManager(
|
||||
onClick={() => setSelectedId(t.id)}
|
||||
className={
|
||||
selectedId === t.id
|
||||
? "w-full rounded-button border border-rule bg-subtle p-sm text-left"
|
||||
: "w-full rounded-button border border-rule bg-surface p-sm text-left"
|
||||
? "w-full rounded-md border border bg-muted p-2 text-left"
|
||||
: "w-full rounded-md border border bg-card p-2 text-left"
|
||||
}
|
||||
>
|
||||
<p className="text-body text-ink">{t.title}</p>
|
||||
<p className="text-small text-ink-muted">
|
||||
<p className="text-body text-foreground">{t.title}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t.author} · {t.publisher}
|
||||
</p>
|
||||
<p className="text-tiny text-ink-muted">ISBN: {t.isbn}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
ISBN: {t.isbn}
|
||||
</p>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
@@ -103,22 +105,22 @@ export default function TextbookManager(
|
||||
|
||||
<div className="flex-1">
|
||||
{selected ? (
|
||||
<div className="rounded-card border border-rule bg-surface p-md">
|
||||
<h4 className="text-body text-ink">{selected.title}</h4>
|
||||
<p className="mt-xs text-small text-ink-muted">
|
||||
<div className="rounded-xl border border bg-card p-4">
|
||||
<h4 className="text-body text-foreground">{selected.title}</h4>
|
||||
<p className="mt-xs text-sm text-muted-foreground">
|
||||
{selected.author} · {selected.publisher}
|
||||
</p>
|
||||
<h5 className="mt-md text-small text-ink">章节列表</h5>
|
||||
<h5 className="mt-md text-sm text-foreground">章节列表</h5>
|
||||
<ol className="mt-xs space-y-xs">
|
||||
{selected.chapters.map((c, i) => (
|
||||
<li key={c.id} className="text-small text-ink">
|
||||
<li key={c.id} className="text-sm text-foreground">
|
||||
{i + 1}. {c.title}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-card border border-rule bg-surface p-md text-small text-ink-muted">
|
||||
<div className="rounded-xl border border bg-card p-4 text-sm text-muted-foreground">
|
||||
点击左侧教材查看章节
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user