feat(table): 表格下载支持 CSV 和 Markdown 双格式下拉选择
重构下载功能,将单一下载按钮改为 DropdownMenu 下拉菜单,新增 CSV 导出和 escapeCsvCell 辅助函数,downloadTextFile 支持自定义 MIME 类型。
This commit is contained in:
parent
8e6c8c7424
commit
269408b66f
@ -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} />
|
||||||
<Tooltip content={downloadLabel}>
|
<DropdownMenu>
|
||||||
<button
|
<Tooltip content={downloadLabel}>
|
||||||
className="h-[32px] w-[32px] text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all"
|
<DropdownMenuTrigger asChild>
|
||||||
onClick={handleDownload}
|
<button
|
||||||
type="button"
|
className="h-[32px] w-[32px] text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all"
|
||||||
>
|
type="button"
|
||||||
<DownloadIcon size={16} />
|
>
|
||||||
</button>
|
<DownloadIcon size={16} />
|
||||||
</Tooltip>
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user