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:
SpecialX
2026-07-17 16:49:00 +08:00
parent 9cedf0c437
commit f586a0b19e
46 changed files with 2415 additions and 559 deletions

View File

@@ -1,4 +1,4 @@
"use client";
"use client";
/**
* question-bankteacher / 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>