feat: 优化定时任务 Markdown 阅读页
This commit is contained in:
1 parent
0c663f5f7c
commit
b0db8cc3d8
4 files changed
+459
-24
No files matched your search
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { BookOpen, ListTree } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
import { createMarkdownAnchorResolver } from "@/core/utils/markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import "./wiki.css";
|
||||
|
||||
function stripMarkdownToText(markdown: string): string {
|
||||
return markdown
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/~~~[\s\S]*?~~~/g, " ")
|
||||
.replace(/!\[[^\]]*\]\([^)]+\)/g, " ")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
.replace(/^[>\-*+\d.)\s]+/gm, "")
|
||||
.replace(/[`*_~|]/g, "")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getMarkdownExcerpt(markdown: string): string {
|
||||
const text = stripMarkdownToText(markdown);
|
||||
if (!text) return "";
|
||||
return text.length > 150 ? `${text.slice(0, 150)}...` : text;
|
||||
}
|
||||
|
||||
function countReadableWords(markdown: string): number {
|
||||
const text = stripMarkdownToText(markdown);
|
||||
if (!text) return 0;
|
||||
const cjk = text.match(/[\u4e00-\u9fff]/g)?.length ?? 0;
|
||||
const words = text.replace(/[\u4e00-\u9fff]/g, " ").match(/[A-Za-z0-9]+/g)?.length ?? 0;
|
||||
return cjk + words;
|
||||
}
|
||||
|
||||
function selectorForId(id: string): string {
|
||||
const css = window.CSS;
|
||||
if (css?.escape) return `#${css.escape(id)}`;
|
||||
return `[id="${id.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
|
||||
}
|
||||
|
||||
export function ReportMarkdownView({
|
||||
content,
|
||||
title,
|
||||
subtitle,
|
||||
badgeLabel = "报告阅读",
|
||||
className,
|
||||
compactHeader = false,
|
||||
}: {
|
||||
content: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
badgeLabel?: string;
|
||||
className?: string;
|
||||
compactHeader?: boolean;
|
||||
}) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const anchorResolver = useMemo(() => createMarkdownAnchorResolver(content), [content]);
|
||||
const headings = useMemo(
|
||||
() => anchorResolver.headings.filter((item) => item.level >= 1 && item.level <= 3),
|
||||
[anchorResolver],
|
||||
);
|
||||
const [activeHeadingId, setActiveHeadingId] = useState<string | null>(null);
|
||||
const excerpt = useMemo(() => getMarkdownExcerpt(content), [content]);
|
||||
const sectionCount = headings.length;
|
||||
const wordCount = useMemo(() => countReadableWords(content), [content]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveHeadingId(headings[0]?.id ?? null);
|
||||
}, [headings]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
if (!root || headings.length === 0) return;
|
||||
|
||||
const targets = headings
|
||||
.map((item) => root.querySelector<HTMLElement>(selectorForId(item.id)))
|
||||
.filter((item): item is HTMLElement => !!item);
|
||||
if (targets.length === 0) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
|
||||
if (visible?.target.id) setActiveHeadingId(visible.target.id);
|
||||
},
|
||||
{
|
||||
root: root.closest("[data-radix-scroll-area-viewport]") ?? null,
|
||||
rootMargin: "-96px 0px -65% 0px",
|
||||
threshold: [0, 1],
|
||||
},
|
||||
);
|
||||
|
||||
targets.forEach((target) => observer.observe(target));
|
||||
return () => observer.disconnect();
|
||||
}, [headings, content]);
|
||||
|
||||
const scrollToHeading = useCallback((id: string) => {
|
||||
const root = rootRef.current;
|
||||
const target =
|
||||
root?.querySelector<HTMLElement>(selectorForId(id)) ?? document.getElementById(id);
|
||||
setActiveHeadingId(id);
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={cn("report-markdown-shell grid min-h-0 gap-5 lg:grid-cols-[minmax(0,1fr)_300px]", className)}
|
||||
>
|
||||
<article className="report-markdown-paper min-w-0 overflow-hidden">
|
||||
<header className={cn("report-markdown-header", compactHeader && "report-markdown-header-compact")}>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary" className="report-markdown-badge">
|
||||
<BookOpen className="size-3.5" />
|
||||
{badgeLabel}
|
||||
</Badge>
|
||||
{subtitle && (
|
||||
<span className="max-w-full truncate text-xs text-slate-500 dark:text-slate-400">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{title && <h1 className="report-markdown-title">{title}</h1>}
|
||||
{!compactHeader && excerpt && <p className="report-markdown-excerpt">{excerpt}</p>}
|
||||
<div className="report-markdown-metrics">
|
||||
<span>{sectionCount || 1} 个章节</span>
|
||||
<span>约 {wordCount} 字</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="wiki-markdown report-markdown-body">
|
||||
<MarkdownContent
|
||||
className="max-w-none"
|
||||
content={content}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside className="hidden min-w-0 lg:block">
|
||||
<div className="report-markdown-toc">
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||
<ListTree className="size-4" />
|
||||
内容目录
|
||||
</div>
|
||||
<span className="rounded-md bg-slate-100 px-1.5 py-0.5 text-xs text-slate-500 dark:bg-slate-800 dark:text-slate-400">
|
||||
{sectionCount || 0}
|
||||
</span>
|
||||
</div>
|
||||
{headings.length > 0 ? (
|
||||
<nav className="report-markdown-toc-list">
|
||||
{headings.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"report-markdown-toc-item",
|
||||
activeHeadingId === item.id && "report-markdown-toc-item-active",
|
||||
item.level === 2 && "report-markdown-toc-level-2",
|
||||
item.level === 3 && "report-markdown-toc-level-3",
|
||||
)}
|
||||
title={item.text}
|
||||
onClick={() => scrollToHeading(item.id)}
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed p-3 text-xs text-slate-500 dark:border-slate-800 dark:text-slate-400">
|
||||
暂无标题目录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -220,6 +220,231 @@
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.report-markdown-shell {
|
||||
align-items: start;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.report-markdown-paper {
|
||||
border: 1px solid rgba(203, 213, 225, 0.85);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(248, 250, 252, 0.92), rgba(255, 255, 255, 0) 220px),
|
||||
#ffffff;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||
0 18px 44px rgba(15, 23, 42, 0.07);
|
||||
}
|
||||
|
||||
.report-markdown-header {
|
||||
border-bottom: 1px solid #e8edf4;
|
||||
padding: 26px 34px 22px;
|
||||
}
|
||||
|
||||
.report-markdown-header-compact {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.report-markdown-badge {
|
||||
gap: 6px;
|
||||
border: 1px solid #dbe3ef;
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.report-markdown-title {
|
||||
margin: 14px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.report-markdown-excerpt {
|
||||
margin: 14px 0 0;
|
||||
max-width: 840px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.report-markdown-metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.report-markdown-metrics span {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.report-markdown-body {
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 42px 44px;
|
||||
}
|
||||
|
||||
.report-markdown-body h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.report-markdown-body h2 {
|
||||
position: relative;
|
||||
margin-top: 44px;
|
||||
border-bottom-color: #e5edf7;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.report-markdown-body h2::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 0.34em;
|
||||
width: 4px;
|
||||
height: 1.05em;
|
||||
border-radius: 999px;
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.report-markdown-body h3 {
|
||||
color: #1e293b;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.report-markdown-body p,
|
||||
.report-markdown-body li {
|
||||
font-size: 16px;
|
||||
line-height: 1.92;
|
||||
}
|
||||
|
||||
.report-markdown-body blockquote {
|
||||
border-left-color: #2563eb;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.report-markdown-body table {
|
||||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.report-markdown-toc {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(203, 213, 225, 0.82);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
padding: 14px;
|
||||
box-shadow: 0 12px 34px rgba(15, 23, 42, 0.06);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.report-markdown-toc-list {
|
||||
max-height: calc(100vh - 128px);
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.report-markdown-toc-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-left: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 7px 8px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
.report-markdown-toc-item:hover {
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.report-markdown-toc-item-active {
|
||||
border-left-color: #2563eb;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.report-markdown-toc-level-2 {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.report-markdown-toc-level-3 {
|
||||
padding-left: 30px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark .report-markdown-paper {
|
||||
border-color: #1f2937;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 23, 42, 0.9), rgba(2, 6, 23, 0) 220px),
|
||||
#020617;
|
||||
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.dark .report-markdown-header {
|
||||
border-bottom-color: #1e293b;
|
||||
}
|
||||
|
||||
.dark .report-markdown-badge,
|
||||
.dark .report-markdown-metrics span {
|
||||
border-color: #334155;
|
||||
background: #0f172a;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.dark .report-markdown-title {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.dark .report-markdown-excerpt,
|
||||
.dark .report-markdown-metrics {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark .report-markdown-body h3 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .report-markdown-body h2::before {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
.dark .report-markdown-body blockquote {
|
||||
background: #0f172a;
|
||||
border-left-color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark .report-markdown-toc {
|
||||
border-color: #1f2937;
|
||||
background: rgba(2, 6, 23, 0.88);
|
||||
}
|
||||
|
||||
.dark .report-markdown-toc-item:hover {
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.dark .report-markdown-toc-item-active {
|
||||
background: rgba(37, 99, 235, 0.16);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
/* Anchor target offset so headings clear the floating "目录" pill. */
|
||||
.wiki-markdown h1,
|
||||
.wiki-markdown h2,
|
||||
|
||||
@@ -24,6 +24,11 @@ import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -43,13 +48,13 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||
import { HtmlPreviewFrame } from "@/components/html-preview-frame";
|
||||
import { ReportMarkdownView } from "@/components/wiki/ReportMarkdownView";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { usePublicBackground } from "@/core/embed/background";
|
||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
import { createMarkdownAnchorResolver } from "@/core/utils/markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import "@/components/wiki/wiki.css";
|
||||
|
||||
type PublicTask = {
|
||||
task_id: string;
|
||||
@@ -875,13 +880,10 @@ function PublicFullscreenPreviewContent({ preview }: { preview: PublicFullscreen
|
||||
}
|
||||
if (preview.mode === "markdown") {
|
||||
return (
|
||||
<KnowledgeMarkdownView
|
||||
<ReportMarkdownView
|
||||
className="max-w-none"
|
||||
content={preview.content || "无正文内容"}
|
||||
title={preview.title}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -889,13 +891,10 @@ function PublicFullscreenPreviewContent({ preview }: { preview: PublicFullscreen
|
||||
const content = preview.file.content || "无文件内容";
|
||||
if (isMarkdownFile(preview.file)) {
|
||||
return (
|
||||
<KnowledgeMarkdownView
|
||||
<ReportMarkdownView
|
||||
className="max-w-none"
|
||||
content={content}
|
||||
title={preview.title}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -920,6 +919,7 @@ function publicFullscreenHref(taskId: string, runId: string, tab: ContentTab): s
|
||||
function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [fullscreenPreview, setFullscreenPreview] = useState<PublicFullscreenPreview | null>(null);
|
||||
const [runMetaOpen, setRunMetaOpen] = useState(false);
|
||||
|
||||
// HTML page run: render a sandboxed iframe preview instead of the MD tabs.
|
||||
const isHtmlRun =
|
||||
@@ -1159,7 +1159,29 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Collapsible open={runMetaOpen} onOpenChange={setRunMetaOpen} className="mt-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground hover:text-foreground h-7 px-2 text-xs"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("size-3.5 transition-transform", runMetaOpen && "rotate-90")}
|
||||
/>
|
||||
运行信息
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-2 grid gap-1 rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground sm:grid-cols-3">
|
||||
<div className="min-w-0 break-all">计划:{formatDate(run.scheduled_for)}</div>
|
||||
<div>开始:{formatDate(run.started_at)}</div>
|
||||
<div>完成:{formatDate(run.finished_at)}</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
<p className="hidden">
|
||||
开始:{formatDate(run.started_at)} · 完成:{formatDate(run.finished_at)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1311,7 +1333,7 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
||||
无内容。点击「编辑」开始添加。
|
||||
</div>
|
||||
) : (
|
||||
<KnowledgeMarkdownView
|
||||
<ReportMarkdownView
|
||||
className="max-w-none pr-2"
|
||||
content={activeSource}
|
||||
title={
|
||||
@@ -1319,10 +1341,6 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
||||
? stripExtension(activeTabMeta.fileName || "")
|
||||
: run.result?.title
|
||||
}
|
||||
subtitle={`执行时间:${formatDate(run.scheduled_for)}`}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@/components/workspace/workspace-container";
|
||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||
import { HtmlPreviewFrame } from "@/components/html-preview-frame";
|
||||
import { ReportMarkdownView } from "@/components/wiki/ReportMarkdownView";
|
||||
import {
|
||||
downloadScheduledTaskRun,
|
||||
downloadScheduledTaskRunFile,
|
||||
@@ -91,11 +92,12 @@ function RenderPreview({ file }: { file: ScheduledTaskResultFileContent }) {
|
||||
}
|
||||
if (isMarkdownFile(file)) {
|
||||
return (
|
||||
<MarkdownContent
|
||||
<ReportMarkdownView
|
||||
className="max-w-none"
|
||||
content={file.content}
|
||||
isLoading={false}
|
||||
rehypePlugins={[]}
|
||||
title={file.name.replace(/\.(md|markdown)$/i, "")}
|
||||
badgeLabel="结果文件"
|
||||
compactHeader
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -114,11 +116,11 @@ function FullscreenPreviewContent({ preview }: { preview: FullscreenPreview }) {
|
||||
}
|
||||
if (preview.mode === "markdown") {
|
||||
return (
|
||||
<MarkdownContent
|
||||
<ReportMarkdownView
|
||||
className="max-w-none"
|
||||
content={preview.content || "无正文内容"}
|
||||
isLoading={false}
|
||||
rehypePlugins={[]}
|
||||
title={preview.title}
|
||||
badgeLabel="正文预览"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -653,11 +655,12 @@ export default function ScheduledTaskRunDetailPage() {
|
||||
</Button>
|
||||
</div>
|
||||
{result.format === "markdown" || result.format === "mixed" ? (
|
||||
<MarkdownContent
|
||||
<ReportMarkdownView
|
||||
className="max-w-none"
|
||||
content={result.content || "无正文内容"}
|
||||
isLoading={false}
|
||||
rehypePlugins={[]}
|
||||
title={result.title || "正文预览"}
|
||||
subtitle={`计划时间:${formatDate(run.scheduled_for)}`}
|
||||
badgeLabel="任务报告"
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap break-words text-sm">
|
||||
|
||||
Reference in new issue
Block a user