弹窗样式修改

This commit is contained in:
董宏凯 committed 2026-08-24 14:45:25 +08:00
1 parent 541bc3280f
commit cdb8aa5f81
1 file changed
+46 -134
@@ -9,7 +9,7 @@
* 与 标题 筛选;卡片上可 switch 启停、编辑、删除(二次确认),并显示
* 最后修改时间与创建用户名;新增时不填状态(服务端强制默认启用)。
* - 详情视图:点击卡片即「选择」该结构(内容回填到高级配置的报告结构
* custom_outline),弹窗切换为展示 标题 + 接口内容,右上角「返回」回列表。
* custom_outline),弹窗切换为展示 标题 + Markdown 大纲预览,右上角「返回」回列表。
*/
import { useEffect, useMemo, useState } from "react";
@@ -39,6 +39,8 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
import { reasoningPlugins } from "@/core/streamdown";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -102,16 +104,19 @@ function isSystemStructure(structure: ReportStructure): boolean {
return creator === "系统内置" || creator.toLowerCase() === "system";
}
type OutlinePreviewLine = {
label?: string;
content: string;
};
type OutlinePreviewSection = {
number: string;
title: string;
lines: OutlinePreviewLine[];
};
const outlineMarkdownClassName =
"!h-auto !w-full max-w-none min-w-0 text-sm leading-7 text-neutral-700 dark:text-slate-300 " +
"[&_h1]:mt-0 [&_h1]:mb-3 [&_h1]:text-xl [&_h1]:font-bold [&_h1]:tracking-tight [&_h1]:text-neutral-900 dark:[&_h1]:text-slate-100 " +
"[&_h2]:mt-6 [&_h2]:mb-2 [&_h2]:text-base [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-neutral-900 dark:[&_h2]:text-slate-100 " +
"[&_h3]:mt-4 [&_h3]:mb-1.5 [&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-neutral-900 dark:[&_h3]:text-slate-100 " +
"[&_h4]:mt-3 [&_h4]:mb-1 [&_h4]:text-sm [&_h4]:font-semibold " +
"[&_p]:my-2.5 [&_ul]:my-2.5 [&_ol]:my-2.5 [&_li]:my-1 " +
"[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 " +
"[&_strong]:font-semibold [&_strong]:text-neutral-900 dark:[&_strong]:text-slate-100 " +
"[&_blockquote]:my-3 [&_blockquote]:border-l-4 [&_blockquote]:border-rose-200 [&_blockquote]:pl-4 [&_blockquote]:text-neutral-500 dark:[&_blockquote]:border-rose-900/50 dark:[&_blockquote]:text-slate-400 " +
"[&_code]:rounded [&_code]:border [&_code]:bg-neutral-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs dark:[&_code]:border-slate-700 dark:[&_code]:bg-slate-800 " +
"[&_pre]:my-4 [&_pre]:max-w-full [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:bg-neutral-50 [&_pre]:p-4 dark:[&_pre]:border-slate-700 dark:[&_pre]:bg-slate-800 " +
"[&_pre_code]:border-0 [&_pre_code]:bg-transparent [&_pre_code]:p-0";
function cleanOutlineText(line: string): string {
return line
@@ -123,34 +128,6 @@ function cleanOutlineText(line: string): string {
.trim();
}
function extractMajorSection(text: string, fallbackNumber: number): { number: string; title: string } | null {
const decimalSubsection = /^\d+\.\d+/.test(text);
if (decimalSubsection) return null;
const arabic = text.match(/^(\d+)[..、]\s*(.+)$/);
if (arabic) return { number: arabic[1], title: arabic[2].trim() };
const chineseOrder = text.match(/^([一二三四五六七八九十]+)[..、]\s*(.+)$/);
if (chineseOrder) return { number: String(fallbackNumber), title: chineseOrder[2].trim() };
const chapter = text.match(/^第[一二三四五六七八九十百千万〇零\d]+[章节篇部][::\s]*(.+)$/);
if (chapter) return { number: String(fallbackNumber), title: chapter[1].trim() };
return null;
}
function parseOutlineLine(text: string): OutlinePreviewLine {
const withoutSubsectionNumber = text.replace(/^\d+(?:\.\d+)+\s*/, "").trim();
const colonIndex = withoutSubsectionNumber.search(/[::]/);
if (colonIndex > 0 && colonIndex <= 18) {
return {
label: withoutSubsectionNumber.slice(0, colonIndex).trim(),
content: withoutSubsectionNumber.slice(colonIndex + 1).trim(),
};
}
return { content: withoutSubsectionNumber };
}
function extractOutlineDocumentTitle(content: string, fallbackTitle: string): string {
for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
const raw = rawLine.trim();
@@ -160,7 +137,7 @@ function extractOutlineDocumentTitle(content: string, fallbackTitle: string): st
const text = cleanOutlineText(heading ? heading[2] : raw);
if (!text || text === "报告标题") continue;
if (/^(日期|类型|密级)\s*[::]/.test(text)) continue;
if (extractMajorSection(text, 1) || /^\d+\.\d+/.test(text)) break;
if (/^\d+([..、]|\.\d+)/.test(text) || /^[一二三四五六七八九十]+[..、]/.test(text)) break;
const wrapped = text.match(/^《(.+)》$/);
if (wrapped?.[1]) return wrapped[1].trim();
@@ -171,58 +148,19 @@ function extractOutlineDocumentTitle(content: string, fallbackTitle: string): st
return fallbackTitle;
}
function isLeadingDocumentTitle(text: string): boolean {
return /^《.+》$/.test(text) || /报告/.test(text);
}
function parseOutlinePreview(content: string, documentTitle: string, structureTitle: string): OutlinePreviewSection[] {
const sections: OutlinePreviewSection[] = [];
let current: OutlinePreviewSection | null = null;
let nextNumber = 1;
function countOutlineMajorHeadings(content: string, skipTitles: string[]): number {
const skip = new Set(skipTitles.filter(Boolean));
let h1 = 0;
let h2 = 0;
for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
const raw = rawLine.trim();
if (!raw) continue;
const heading = raw.match(/^(#{1,6})\s+(.+)$/);
const headingLevel = heading ? heading[1].length : null;
const text = cleanOutlineText(heading ? heading[2] : raw);
if (!text) continue;
if (text === documentTitle || text === structureTitle || text === "报告标题") continue;
if (/^(日期|类型|密级)\s*[::]/.test(text)) continue;
const major = extractMajorSection(text, nextNumber);
const isHeadingMajor = headingLevel !== null && headingLevel <= 2 && !/^\d+\.\d+/.test(text);
if (!current && sections.length === 0 && !major && isLeadingDocumentTitle(text)) continue;
if (major || isHeadingMajor) {
current = {
number: major?.number ?? String(nextNumber),
title: major?.title ?? text,
lines: [],
};
sections.push(current);
nextNumber += 1;
continue;
}
const line = parseOutlineLine(text);
if (!current) {
current = {
number: String(nextNumber),
title: line.label ?? line.content,
lines: line.label ? [{ content: line.content }] : [],
};
sections.push(current);
nextNumber += 1;
continue;
}
current.lines.push(line);
const heading = rawLine.trim().match(/^(#{1,2})\s+(.+)$/);
if (!heading) continue;
const text = cleanOutlineText(heading[2]);
if (!text || text === "报告标题" || skip.has(text)) continue;
if (heading[1].length === 1) h1 += 1;
else h2 += 1;
}
if (sections.length > 0) return sections;
const fallback = cleanOutlineText(content).slice(0, 600);
return [{ number: "1", title: documentTitle || structureTitle, lines: fallback ? [{ content: fallback }] : [] }];
return h1 > 0 ? h1 : h2;
}
// ── main dialog ──────────────────────────────────────────────────────────────
@@ -289,8 +227,8 @@ export function WorkflowConfigDialog({
() => (detail ? extractOutlineDocumentTitle(detail.content, detail.title) : ""),
[detail],
);
const detailOutlineSections = useMemo(
() => (detail ? parseOutlinePreview(detail.content, detailOutlineTitle, detail.title) : []),
const detailOutlineHeadingCount = useMemo(
() => (detail ? countOutlineMajorHeadings(detail.content, [detailOutlineTitle, detail.title]) : 0),
[detail, detailOutlineTitle],
);
@@ -734,50 +672,24 @@ export function WorkflowConfigDialog({
<h3 className="min-w-0 truncate text-base font-bold tracking-tight text-neutral-900 dark:text-slate-100">
{detailOutlineTitle}
</h3>
<span className="shrink-0 text-xs font-normal text-neutral-400 dark:text-slate-500">
共 {detailOutlineSections.length} 个主要章节
</span>
{detailOutlineHeadingCount > 0 && (
<span className="shrink-0 text-xs font-normal text-neutral-400 dark:text-slate-500">
共 {detailOutlineHeadingCount} 个主要章节
</span>
)}
</div>
<div className="space-y-6">
{detailOutlineSections.map((section) => (
<div
key={`${section.number}-${section.title}`}
className="group rounded-xl transition-all"
>
<div className="mb-2.5 flex items-baseline gap-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-neutral-100 text-xs font-bold text-neutral-800 dark:bg-slate-800 dark:text-slate-200">
{section.number}
</span>
<h4 className="text-sm font-bold tracking-tight text-neutral-900 dark:text-slate-100">
{section.title}
</h4>
</div>
{section.lines.length > 0 && (
<div className="space-y-2.5 pl-7">
{section.lines.map((line, index) => (
<div
key={`${section.number}-${index}`}
className="text-xs leading-relaxed text-neutral-700 dark:text-slate-300"
>
{line.label ? (
<>
<span className="font-semibold text-neutral-900 dark:text-slate-100">
{line.label}:
</span>
<span className="text-neutral-600 dark:text-slate-300">{line.content}</span>
</>
) : (
<span className="text-neutral-600 dark:text-slate-300">{line.content}</span>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
{detail.content.trim() ? (
<MarkdownContent
className={outlineMarkdownClassName}
content={detail.content}
isLoading={false}
remarkPlugins={reasoningPlugins.remarkPlugins}
rehypePlugins={reasoningPlugins.rehypePlugins}
/>
) : (
<p className="text-sm text-neutral-400 dark:text-slate-500">暂无大纲内容</p>
)}
</div>
</section>
</div>