Files
Edu/apps/portal-shell/src/widgets/teacher/textbook-manager/index.tsx
SpecialX 2910a90271 feat(portal-shell): extract domain API layer and migrate 31 widgets
Task 4-10 of portal-shell data abstraction plan (M1-M2).

Add 7 domain API modules under src/lib/api/ (parent/admin/teacher/
student/universal/sidebar/topbar), each exposing semantic hooks that
wrap useWidgetQuery/useWidgetMutation and return flattened domain
models. Widget code now imports from @/lib/api instead of inlining
gql literals.

- 31 widgets migrated (gql literal count in widgets: 0)
- 7 test files (85 cases, all passing)
- topbar.useNotifications renamed to useNotificationBell to avoid
  barrel export collision with universal.useNotifications
- typecheck + lint (0 errors) + test (85/85) verified
2026-07-17 13:07:24 +08:00

130 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* textbook-managerteacher / main
*
* 教材管理:按科目与年级筛选教材,点击教材查看章节列表。
* 通过 useWidgetQuery 查询 apollo-router → content 子图的 textbooks 数据。
*
* 关联portal-shell spec §5.6 统一 Hook
*/
import { useState } from "react";
import { useTextbooks } from "@/lib/api";
import { PluginSkeleton } from "@/shell/PluginLoader";
import type { PluginProps } from "@/lib/types";
export default function TextbookManager(
_props: PluginProps,
): React.ReactElement {
const [subjectInput, setSubjectInput] = useState("");
const [gradeInput, setGradeInput] = useState("");
const [appliedSubject, setAppliedSubject] = useState("");
const [appliedGrade, setAppliedGrade] = useState("");
const [selectedId, setSelectedId] = useState<string | null>(null);
const { data, loading } = useTextbooks({
subjectId: appliedSubject || undefined,
grade: appliedGrade || undefined,
});
if (loading && !data) {
return <PluginSkeleton variant="list" />;
}
const textbooks = data ?? [];
const selected = textbooks.find((t) => t.id === selectedId) ?? null;
const handleApply = (): void => {
setAppliedSubject(subjectInput.trim());
setAppliedGrade(gradeInput.trim());
setSelectedId(null);
};
return (
<section className="rounded-card border border-rule bg-surface p-md">
<h3 className="text-heading-3 text-ink"></h3>
<div className="mt-sm flex gap-md">
<label className="flex flex-col space-y-xs">
<span className="text-small text-ink-muted"> 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"
placeholder="可选,输入科目 ID"
/>
</label>
<label className="flex flex-col space-y-xs">
<span className="text-small text-ink-muted"></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"
placeholder="可选,如:三年级"
/>
</label>
<button
type="button"
onClick={handleApply}
className="self-end rounded-button bg-accent px-md py-xs text-small text-ink-onAccent"
>
</button>
</div>
<div className="mt-md flex gap-md">
<ul className="flex-1 space-y-sm">
{textbooks.length === 0 ? (
<li className="text-small text-ink-muted"></li>
) : (
textbooks.map((t) => (
<li key={t.id}>
<button
type="button"
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"
}
>
<p className="text-body text-ink">{t.title}</p>
<p className="text-small text-ink-muted">
{t.author} · {t.publisher}
</p>
<p className="text-tiny text-ink-muted">ISBN: {t.isbn}</p>
</button>
</li>
))
)}
</ul>
<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">
{selected.author} · {selected.publisher}
</p>
<h5 className="mt-md text-small text-ink"></h5>
<ol className="mt-xs space-y-xs">
{selected.chapters.map((c, i) => (
<li key={c.id} className="text-small text-ink">
{i + 1}. {c.title}
</li>
))}
</ol>
</div>
) : (
<div className="rounded-card border border-rule bg-surface p-md text-small text-ink-muted">
</div>
)}
</div>
</div>
</section>
);
}