fix: 表格的复制按钮被禁用的问题

This commit is contained in:
肖应宇 2026-04-20 10:24:01 +08:00
parent c52b505354
commit a62e65acfe
1 changed files with 110 additions and 4 deletions

View File

@ -1,14 +1,20 @@
"use client";
import { useMemo } from "react";
import type { AnchorHTMLAttributes } from "react";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useCallback, useMemo, useState, type MouseEvent } from "react";
import type {
AnchorHTMLAttributes,
ComponentPropsWithoutRef,
ReactNode,
} from "react";
import {
MessageResponse,
type MessageResponseProps,
} from "@/components/ai-elements/message";
import { useI18n } from "@/core/i18n/hooks";
import { streamdownPlugins } from "@/core/streamdown";
import { cn } from "@/lib/utils";
import { cn, copyToClipboard } from "@/lib/utils";
import { CitationLink } from "../citations/citation-link";
@ -25,6 +31,89 @@ export type MarkdownContentProps = {
components?: MessageResponseProps["components"];
};
type TableData = {
headers: string[];
rows: string[][];
};
function parseTableData(table: HTMLTableElement): TableData {
const headers = Array.from(table.querySelectorAll("thead th")).map((cell) =>
(cell.textContent ?? "").trim(),
);
const rows = Array.from(table.querySelectorAll("tbody tr")).map((row) =>
Array.from(row.querySelectorAll("td")).map(
(cell) => (cell.textContent ?? "").trim(),
),
);
return { headers, rows };
}
function toMarkdownTable(data: TableData): string {
if (data.headers.length === 0) return "";
const headerLine = `| ${data.headers.join(" | ")} |`;
const dividerLine = `| ${data.headers.map(() => "---").join(" | ")} |`;
const rowLines = data.rows.map((row) => `| ${row.join(" | ")} |`);
return [headerLine, dividerLine, ...rowLines].join("\n");
}
function MarkdownTable({
className,
children,
isLoading,
copyLabel,
...props
}: ComponentPropsWithoutRef<"table"> & {
isLoading: boolean;
copyLabel: string;
}) {
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(
async (event: MouseEvent<HTMLButtonElement>) => {
const wrapper = event.currentTarget.closest('[data-streamdown="table-wrapper"]');
const table = wrapper?.querySelector("table");
if (!(table instanceof HTMLTableElement)) return;
const markdown = toMarkdownTable(parseTableData(table));
if (!markdown) return;
try {
await copyToClipboard(markdown);
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
} catch {
// no-op
}
},
[],
);
return (
<div className="my-4 flex flex-col space-y-2" data-streamdown="table-wrapper">
<div className="flex items-center justify-end gap-1">
<button
className="cursor-pointer p-1 text-muted-foreground transition-all hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
disabled={isLoading}
onClick={handleCopy}
title={copyLabel}
type="button"
>
{copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
</button>
</div>
<div className="overflow-x-auto">
<table
className={cn("w-full border-collapse border border-border", className)}
data-streamdown="table"
{...props}
>
{children}
</table>
</div>
</div>
);
}
/** Renders markdown content. */
export function MarkdownContent({
content,
@ -34,6 +123,8 @@ export function MarkdownContent({
remarkPlugins = streamdownPlugins.remarkPlugins,
components: componentsFromProps,
}: MarkdownContentProps) {
const { t } = useI18n();
const components = useMemo(() => {
return {
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
@ -58,9 +149,23 @@ export function MarkdownContent({
/>
);
},
table: ({
children,
className,
...props
}: ComponentPropsWithoutRef<"table"> & { children?: ReactNode }) => (
<MarkdownTable
className={className}
copyLabel={t.clipboard.copyToClipboard}
isLoading={isLoading}
{...props}
>
{children}
</MarkdownTable>
),
...componentsFromProps,
};
}, [componentsFromProps]);
}, [componentsFromProps, isLoading, t.clipboard.copyToClipboard]);
if (!content) return null;
@ -68,6 +173,7 @@ export function MarkdownContent({
<MessageResponse
className={className}
isAnimating={isLoading}
controls={{ table: false }}
parseIncompleteMarkdown={!isLoading}
remarkPlugins={remarkPlugins}
rehypePlugins={rehypePlugins}