feat(ui): 工作台精修 + 拆分(P3-3..7)
- Workbench.tsx 937→467 行,抽出 AiToolbar/WorkbenchToolbar/MobileContextBar - P3-3 AI 工具条分组留白 + 眉题 + 主 CTA 凸显 - P3-4 专注写作模式(useFocusMode hook+vitest,隐藏侧栏加宽正文)+ 左双栏面色区分 - P3-5 右栏本章参考小标题去衬线(font-sans) + 空态引导(无标题保 h2→h3 大纲) - P3-6 底栏信息/操作/状态三区 + TOC 当前章高亮;TOC 状态点因无每章状态数据跳过 - P3-7 展示层流式跟随滚动(不触碰 SSE 逻辑,尊重 reduced-motion) - AppShell 加可选 hideNav(加法式,默认 false)
This commit is contained in:
209
apps/web/components/workbench/WorkbenchToolbar.tsx
Normal file
209
apps/web/components/workbench/WorkbenchToolbar.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { RefObject } from "react";
|
||||
import {
|
||||
BookMarked,
|
||||
ClipboardCheck,
|
||||
FileText,
|
||||
Maximize2,
|
||||
MessagesSquare,
|
||||
Minimize2,
|
||||
Square,
|
||||
} from "lucide-react";
|
||||
|
||||
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import type { useAutosave } from "@/lib/autosave/useAutosave";
|
||||
import { friendlyError } from "@/lib/errors/messages";
|
||||
import { buttonClass } from "@/lib/ui/variants";
|
||||
import { CONTEXT_DRAWER_ENABLED } from "./ContextDrawer";
|
||||
import { AI_CONVERSATION_ENABLED } from "./AiConversationDrawer";
|
||||
|
||||
// 写章失败提示:友好文案(不暴露 raw code)+ 可选「去设置」动作(F4)。
|
||||
function StreamErrorNote({
|
||||
streamError,
|
||||
}: {
|
||||
streamError: { code: string; message: string };
|
||||
}) {
|
||||
const friendly = friendlyError(streamError.code, streamError.message);
|
||||
return (
|
||||
<p className="mb-2 text-sm text-conflict">
|
||||
{friendly.text}
|
||||
{friendly.actionHref ? (
|
||||
<>
|
||||
{" "}
|
||||
<Link
|
||||
href={friendly.actionHref}
|
||||
className="underline hover:text-cinnabar"
|
||||
>
|
||||
{friendly.actionLabel}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// 专注写作开关(P3-4):图标按钮,进入=隐藏左侧图标导航 + 目录 TOC 加宽正文,退出还原。
|
||||
// aria-pressed 表状态、aria-label 表动作,键盘可达(复用共享 Button 焦点环)。
|
||||
function FocusToggle({
|
||||
focus,
|
||||
onToggle,
|
||||
}: {
|
||||
focus: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
onClick={onToggle}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
aria-pressed={focus}
|
||||
aria-label={focus ? "退出专注写作模式" : "进入专注写作模式(隐藏侧栏,加宽正文)"}
|
||||
title={focus ? "退出专注写作" : "专注写作"}
|
||||
>
|
||||
{focus ? (
|
||||
<Minimize2 className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<Maximize2 className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
专注
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// 底栏(P1-1 瘦身 + P3-6 分区):按「信息 | 操作 | 状态」三区排布。
|
||||
// 信息=字数/生成中;操作=速查/对话记录/审稿/专注(+流式时的「停」);状态=保存态(右侧)。
|
||||
interface ToolbarProps {
|
||||
projectId: string;
|
||||
chapterNo: number;
|
||||
wordCount: number;
|
||||
savedLabel: string | null;
|
||||
saveStatus: ReturnType<typeof useAutosave>["status"];
|
||||
streaming: boolean;
|
||||
streamError: { code: string; message: string } | null;
|
||||
// 流式里程碑播报句(完成/停止/失败);仅在 role=status 区出现,不逐 token 刷新。
|
||||
liveMessage: string;
|
||||
// 流式时的「停」:底栏 sticky,滚到长正文下方也够得着(中央条会滚出视野)。
|
||||
onStop: () => void;
|
||||
// 专注写作开关(P3-4)。
|
||||
focus: boolean;
|
||||
onToggleFocus: () => void;
|
||||
// 打开上下文速查抽屉(WFW-6)。
|
||||
onOpenContext: () => void;
|
||||
contextTriggerRef: RefObject<HTMLButtonElement | null>;
|
||||
// 打开对话记录抽屉(AC-3)。
|
||||
onOpenConversation: () => void;
|
||||
conversationTriggerRef: RefObject<HTMLButtonElement | null>;
|
||||
}
|
||||
|
||||
export function Toolbar({
|
||||
projectId,
|
||||
chapterNo,
|
||||
wordCount,
|
||||
savedLabel,
|
||||
saveStatus,
|
||||
streaming,
|
||||
streamError,
|
||||
liveMessage,
|
||||
onStop,
|
||||
focus,
|
||||
onToggleFocus,
|
||||
onOpenContext,
|
||||
contextTriggerRef,
|
||||
onOpenConversation,
|
||||
conversationTriggerRef,
|
||||
}: ToolbarProps) {
|
||||
return (
|
||||
<div className="sticky bottom-0 z-10 border-t border-line bg-panel/95 px-4 py-2 backdrop-blur sm:px-6 sm:py-3">
|
||||
{streamError ? <StreamErrorNote streamError={streamError} /> : null}
|
||||
<div className="grid gap-2 sm:flex sm:flex-wrap sm:items-center sm:gap-3">
|
||||
{/* 信息区:字数 / 生成中(含流式时的「停」)。 */}
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{streaming ? (
|
||||
<Button onClick={onStop} variant="danger" size="sm">
|
||||
<Square className="h-4 w-4" aria-hidden="true" />
|
||||
停
|
||||
</Button>
|
||||
) : null}
|
||||
{streaming ? (
|
||||
// ThinkingIndicator 自带 role=status,开始时播报一次「生成中」;
|
||||
// 易变字数 aria-hidden,仅供视觉,不逐 token 打扰屏读。
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<ThinkingIndicator
|
||||
label="生成中"
|
||||
className="text-xs text-cinnabar"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="font-mono text-xs text-cinnabar"
|
||||
>
|
||||
{wordCount} 字
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-mono text-xs text-ink-soft">
|
||||
字数 {wordCount}
|
||||
</span>
|
||||
)}
|
||||
{/* 终结句播报区:完成/停止/失败时写入,屏读只播报里程碑。 */}
|
||||
<span className="sr-only" role="status" aria-live="polite">
|
||||
{liveMessage}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 操作区:速查 / 对话记录 / 审稿 / 专注;与信息区以细分隔线拉开。 */}
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2 sm:border-l sm:border-line-soft sm:pl-3">
|
||||
{CONTEXT_DRAWER_ENABLED ? (
|
||||
<Button
|
||||
ref={contextTriggerRef}
|
||||
onClick={onOpenContext}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
title="打开上下文速查(设定库/大纲/伏笔/规则/文风)"
|
||||
>
|
||||
<BookMarked className="h-4 w-4" aria-hidden="true" />
|
||||
速查
|
||||
</Button>
|
||||
) : null}
|
||||
{AI_CONVERSATION_ENABLED ? (
|
||||
<Button
|
||||
ref={conversationTriggerRef}
|
||||
onClick={onOpenConversation}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
title="打开对话记录(润色/重写/续写/工具箱往复留痕的只读日志)"
|
||||
>
|
||||
<MessagesSquare className="h-4 w-4" aria-hidden="true" />
|
||||
对话记录
|
||||
</Button>
|
||||
) : null}
|
||||
<FocusToggle focus={focus} onToggle={onToggleFocus} />
|
||||
{/* 唯一强调的「审稿」= 写→审→验收的前进 CTA(导航去重后仅此一处强调,左栏另有入口)。 */}
|
||||
<Link
|
||||
href={`/projects/${projectId}/review?chapter=${chapterNo}`}
|
||||
className={buttonClass({ variant: "outline", size: "sm" })}
|
||||
>
|
||||
<ClipboardCheck className="h-4 w-4" aria-hidden="true" />
|
||||
审稿
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 状态区:保存态,右侧对齐。 */}
|
||||
<span
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="min-w-0 font-mono text-xs text-ink-soft sm:ml-auto"
|
||||
>
|
||||
<FileText className="mr-1 inline h-3.5 w-3.5" aria-hidden="true" />
|
||||
{saveStatus === "saving"
|
||||
? "保存中…"
|
||||
: saveStatus === "error"
|
||||
? "保存失败"
|
||||
: (savedLabel ?? "尚未保存")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user