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
130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* textbook-manager(teacher / 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>
|
||
);
|
||
}
|