feat(table): 表格下载支持 CSV 和 Markdown 双格式下拉选择

重构下载功能,将单一下载按钮改为 DropdownMenu 下拉菜单,新增 CSV 导出和 escapeCsvCell 辅助函数,downloadTextFile 支持自定义 MIME 类型。
This commit is contained in:
肖应宇 2026-06-12 11:39:02 +08:00
parent 8e6c8c7424
commit 269408b66f

View File

@ -12,6 +12,12 @@ import {
MessageResponse, MessageResponse,
type MessageResponseProps, type MessageResponseProps,
} from "@/components/ai-elements/message"; } from "@/components/ai-elements/message";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { streamdownPlugins } from "@/core/streamdown"; import { streamdownPlugins } from "@/core/streamdown";
import { CopyButton } from "@/components/workspace/copy-button"; import { CopyButton } from "@/components/workspace/copy-button";
@ -58,9 +64,22 @@ function toMarkdownTable(data: TableData): string {
return [headerLine, dividerLine, ...rowLines].join("\n"); return [headerLine, dividerLine, ...rowLines].join("\n");
} }
function downloadMarkdownFile(content: string, filename: string) { function escapeCsvCell(cell: string): string {
const normalized = cell.replace(/\r\n/g, "\n");
if (!/["\n,]/.test(normalized)) return normalized;
return `"${normalized.replace(/"/g, '""')}"`;
}
function toCsvTable(data: TableData): string {
if (data.headers.length === 0) return "";
const headerLine = data.headers.map(escapeCsvCell).join(",");
const rowLines = data.rows.map((row) => row.map(escapeCsvCell).join(","));
return [headerLine, ...rowLines].join("\r\n");
}
function downloadTextFile(content: string, filename: string, mimeType: string) {
const blob = new Blob(["\uFEFF", content], { const blob = new Blob(["\uFEFF", content], {
type: "text/markdown;charset=utf-8", type: mimeType,
}); });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const anchor = document.createElement("a"); const anchor = document.createElement("a");
@ -99,13 +118,23 @@ export function MarkdownTable({
})(); })();
const handleDownload = useCallback(() => { const handleDownload = useCallback(() => {
const table = tableRef.current;
if (!table) return;
const data = parseTableData(table);
if (!data) return;
const csv = toCsvTable(data);
if (!csv) return;
downloadTextFile(csv, "table.csv", "text/csv;charset=utf-8");
}, []);
const handleDownloadMarkdown = useCallback(() => {
const table = tableRef.current; const table = tableRef.current;
if (!table) return; if (!table) return;
const data = parseTableData(table); const data = parseTableData(table);
if (!data) return; if (!data) return;
const markdown = toMarkdownTable(data); const markdown = toMarkdownTable(data);
if (!markdown) return; if (!markdown) return;
downloadMarkdownFile(markdown, "table.md"); downloadTextFile(markdown, "table.md", "text/markdown;charset=utf-8");
}, []); }, []);
return ( return (
@ -115,15 +144,24 @@ export function MarkdownTable({
> >
<div className="flex items-center justify-end gap-1"> <div className="flex items-center justify-end gap-1">
<CopyButton className="text-muted-foreground hover:bg-transparent hover:text-foreground cursor-pointer p-1 transition-all" clipboardData={clipboardData} /> <CopyButton className="text-muted-foreground hover:bg-transparent hover:text-foreground cursor-pointer p-1 transition-all" clipboardData={clipboardData} />
<DropdownMenu>
<Tooltip content={downloadLabel}> <Tooltip content={downloadLabel}>
<DropdownMenuTrigger asChild>
<button <button
className="h-[32px] w-[32px] text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all" className="h-[32px] w-[32px] text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all"
onClick={handleDownload}
type="button" type="button"
> >
<DownloadIcon size={16} /> <DownloadIcon size={16} />
</button> </button>
</DropdownMenuTrigger>
</Tooltip> </Tooltip>
<DropdownMenuContent align="end" className="min-w-[140px] p-1">
<DropdownMenuItem onSelect={handleDownload}>CSV</DropdownMenuItem>
<DropdownMenuItem onSelect={handleDownloadMarkdown}>
Markdown
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table <table