diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 2170895..8ce7d67 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -14,9 +14,11 @@ export default function RootLayout({ }: { children: React.ReactNode; }) { + // suppressHydrationWarning:浏览器扩展(如 Dark Reader/翻译插件)常在 html/body + // 注入 class/attr,水合前改 DOM → 仅这一层属性的水合告警,非本应用 bug。 return ( - - + + {children} diff --git a/apps/web/components/ThinkingIndicator.tsx b/apps/web/components/ThinkingIndicator.tsx new file mode 100644 index 0000000..d778d11 --- /dev/null +++ b/apps/web/components/ThinkingIndicator.tsx @@ -0,0 +1,32 @@ +interface ThinkingIndicatorProps { + // 状态文案(如「审稿进行中」「验收中」「回炉中」「生成中」)。 + label: string; + className?: string; +} + +// AI 推理中动画提示:三点波动(朱砂/继承色),让长等待「看得见在工作」。 +// motion-safe:尊重 prefers-reduced-motion,减动偏好下三点静止为「…」仍可读(a11y §10)。 +// role=status + aria-live:屏读播报状态变化。 +export function ThinkingIndicator({ + label, + className = "", +}: ThinkingIndicatorProps) { + return ( + + + ); +} diff --git a/apps/web/components/review/AcceptPanel.tsx b/apps/web/components/review/AcceptPanel.tsx index d6a75ae..ca361ea 100644 --- a/apps/web/components/review/AcceptPanel.tsx +++ b/apps/web/components/review/AcceptPanel.tsx @@ -2,6 +2,7 @@ import type { AcceptResponse } from "@/lib/api/types"; import { buildAcceptPreview } from "@/lib/review/accept-preview"; +import { ThinkingIndicator } from "@/components/ThinkingIndicator"; interface AcceptPanelProps { conflictCount: number; @@ -101,7 +102,11 @@ export function AcceptPanel({ aria-disabled={blocked || accepting} className="w-full rounded bg-cinnabar px-4 py-2 text-sm text-panel hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40" > - {accepting ? "验收中…" : "全部处理完 → 验收本章"} + {accepting ? ( + + ) : ( + "全部处理完 → 验收本章" + )} ); diff --git a/apps/web/components/review/ReviewReport.tsx b/apps/web/components/review/ReviewReport.tsx index 6e6fdf8..c71b603 100644 --- a/apps/web/components/review/ReviewReport.tsx +++ b/apps/web/components/review/ReviewReport.tsx @@ -30,6 +30,7 @@ import { useReviewStream } from "@/lib/review/useReviewStream"; import type { ReviewConflict, StyleDriftSegment } from "@/lib/review/sse"; import { normalizeStyleDrift } from "@/lib/style/style"; import { useToast } from "@/components/Toast"; +import { ThinkingIndicator } from "@/components/ThinkingIndicator"; import { AcceptPanel } from "./AcceptPanel"; import { AnnotatedText } from "./AnnotatedText"; import { ConflictCard } from "./ConflictCard"; @@ -318,6 +319,11 @@ export function ReviewReport({ + {/* B:讲清裁决保存模型——「采纳/裁决」仅本地暂存,验收时一并落库 */} +

+ 裁决先暂存在本地,点下方「验收本章」时与终稿一并保存落库(验收前刷新会丢失未保存的裁决)。 +

+ {groups.map((group) => (

@@ -429,8 +435,8 @@ function SectionStatusLine({ }: SectionStatusLineProps) { if (reviewing) { return ( -

- 审稿进行中… +

+

); } diff --git a/apps/web/components/style/RefineView.tsx b/apps/web/components/style/RefineView.tsx index a226696..4900627 100644 --- a/apps/web/components/style/RefineView.tsx +++ b/apps/web/components/style/RefineView.tsx @@ -3,6 +3,8 @@ import { useRefine } from "@/lib/style/useRefine"; import { useEffect } from "react"; +import { ThinkingIndicator } from "@/components/ThinkingIndicator"; + interface RefineViewProps { projectId: string; chapterNo: number; @@ -54,8 +56,8 @@ export function RefineView({ {segment.trim().length === 0 ? (

该段在终稿中为空,请先在终稿中保留该段正文。

) : refiner.status === "refining" ? ( -

- 回炉中… +

+

) : refiner.status === "error" ? (

回炉失败,请稍后重试。

diff --git a/apps/web/components/workbench/Editor.tsx b/apps/web/components/workbench/Editor.tsx index fedcd19..2a92fe7 100644 --- a/apps/web/components/workbench/Editor.tsx +++ b/apps/web/components/workbench/Editor.tsx @@ -1,5 +1,7 @@ "use client"; +import { useEffect, useRef } from "react"; + interface EditorProps { value: string; onChange: (value: string) => void; @@ -9,19 +11,32 @@ interface EditorProps { // 中栏正文编辑器:宋体、720px 居中、行高 1.9(UX §2.3 / §6.3)。 // 流式中显示朱砂光标提示(prefers-reduced-motion 下不闪烁,见 globals.css)。 export function Editor({ value, onChange, streaming }: EditorProps) { + const ref = useRef(null); + + // 自适应高度:textarea 高度=内容高度(overflow-hidden 不内部滚动),由外层写作区 + // 单一滚动条统管。修复正文出现嵌套滚动条 + 定高 60vh 不铺满展示区的怪象。 + // min-h-[60vh] 仍作空稿时的最小可写高度(内容更长时按内容增高、外层滚动)。 + useEffect(() => { + const el = ref.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }, [value]); + return (