feat(foreshadow): 写作台本章伏笔主动提醒(前端)

- 新增 chapterForeshadow 纯分类:按当前章分 可回收/待回收/已逾期(启用闲置的 isWindowApproaching)
- useChapterForeshadow hook(复用 GET /foreshadow,切章仅重分类不重拉)
- 右栏「本章参考」+ ContextDrawer 伏笔页新增「本章伏笔」实时清单,
  分色展示 + 跳看板链接 + 空态;标注『按已验收进度』
This commit is contained in:
Yaojia Wang
2026-07-12 17:53:00 +02:00
parent 9aa0cddeaf
commit b810a3fa3c
8 changed files with 507 additions and 4 deletions

View File

@@ -1,7 +1,9 @@
"use client";
import type { ReactNode } from "react";
import Link from "next/link";
import {
ArrowUpRight,
Minus,
Pin,
PinOff,
@@ -15,6 +17,9 @@ import {
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
import { Button } from "@/components/ui/Button";
import { StatusNote } from "@/components/ui/StatusNote";
import { ChapterForeshadowList } from "@/components/workbench/ChapterForeshadowList";
import { chapterForeshadowTotal } from "@/lib/foreshadow/chapterForeshadow";
import { useChapterForeshadow } from "@/lib/foreshadow/useChapterForeshadow";
import { buttonClass } from "@/lib/ui/variants";
import {
isPinned,
@@ -183,6 +188,8 @@ export function AssistantContent({ projectId, chapterNo }: ChapterAssistantProps
) : null}
</section>
<ChapterForeshadowSection projectId={projectId} chapterNo={chapterNo} />
<section className="mt-6 border-t border-line-soft pt-5">
<PanelLabel></PanelLabel>
<p className="mt-1.5 text-caption leading-6 text-ink-soft">
@@ -193,6 +200,55 @@ export function AssistantContent({ projectId, chapterNo }: ChapterAssistantProps
);
}
// 「本章伏笔」主动提醒:按当前章号把伏笔分可回收 / 待回收 / 已逾期三类,
// 让作者在写这章时就想起该回收哪条、哪条已逾期。只读,编辑走「伏笔看板」。
function ChapterForeshadowSection({ projectId, chapterNo }: ChapterAssistantProps) {
const { groups, status, reload } = useChapterForeshadow(projectId, chapterNo);
const boardHref = `/projects/${projectId}/foreshadow`;
const isEmpty = chapterForeshadowTotal(groups) === 0;
return (
<section className="mt-6 border-t border-line-soft pt-5">
<PanelLabel
action={<span className="text-2xs text-muted-soft"></span>}
>
</PanelLabel>
{status === "loading" ? (
<p className="mt-2 text-caption text-ink-soft"></p>
) : status === "error" ? (
<StatusNote variant="danger" className="mt-2 text-xs" title="伏笔暂不可用">
<button
type="button"
onClick={reload}
className={buttonClass({ variant: "outline", size: "sm", className: "mt-2" })}
>
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</StatusNote>
) : isEmpty ? (
<p className="mt-2 text-caption leading-6 text-ink-soft">
</p>
) : (
<div className="mt-3">
<ChapterForeshadowList groups={groups} />
</div>
)}
<Link
href={boardHref}
className="mt-3 inline-flex items-center gap-1 text-caption text-cinnabar hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cinnabar/35"
>
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
</section>
);
}
interface EntityRowProps {
entity: InjectionEntity;
pinned: boolean;