Skip to content

Commit 8c6a3f1

Browse files
committed
feat: Add table copy functionality to Markdown component with styled copy button, enhancing user experience for table data management
1 parent e1ff0da commit 8c6a3f1

2 files changed

Lines changed: 153 additions & 2 deletions

File tree

src/renderer/src/components/common/markdown/Markdown.tsx

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import RehypeHighlight from 'rehype-highlight'
1111
import mermaid from 'mermaid'
1212

1313
import React from 'react'
14-
import { useRef, useState, RefObject, useEffect, useMemo } from 'react'
14+
import { useRef, useState, RefObject, useEffect, useMemo, ReactElement } from 'react'
1515

1616
import LoadingIcon from './three-dots.svg'
1717
import { useDebouncedCallback } from 'use-debounce'
@@ -62,6 +62,72 @@ export function Mermaid(props: { code: string }) {
6262
)
6363
}
6464

65+
export function TableWrapper(props: { children: ReactElement }) {
66+
const ref = useRef<HTMLDivElement>(null)
67+
const [isCopied, setIsCopied] = useState(false)
68+
const [tableMarkdown, setTableMarkdown] = useState('')
69+
70+
useEffect(() => {
71+
if (ref.current) {
72+
const table = ref.current.querySelector('table')
73+
if (table) {
74+
const markdown = convertTableToMarkdown(table)
75+
setTableMarkdown(markdown)
76+
}
77+
}
78+
}, [props.children])
79+
80+
const convertTableToMarkdown = (table: HTMLTableElement): string => {
81+
const rows: string[][] = []
82+
const thead = table.querySelector('thead')
83+
const tbody = table.querySelector('tbody')
84+
85+
if (thead) {
86+
const headerRow = thead.querySelector('tr')
87+
if (headerRow) {
88+
const headers = Array.from(headerRow.querySelectorAll('th')).map(
89+
(th) => th.textContent?.trim() || ''
90+
)
91+
rows.push(headers)
92+
rows.push(headers.map(() => '---'))
93+
}
94+
}
95+
96+
if (tbody) {
97+
const bodyRows = tbody.querySelectorAll('tr')
98+
bodyRows.forEach((tr) => {
99+
const cells = Array.from(tr.querySelectorAll('td')).map(
100+
(td) => td.textContent?.trim() || ''
101+
)
102+
rows.push(cells)
103+
})
104+
}
105+
106+
return rows.map((row) => '| ' + row.join(' | ') + ' |').join('\n')
107+
}
108+
109+
const handleCopy = async () => {
110+
if (tableMarkdown) {
111+
const success = await copyToClipboard(tableMarkdown)
112+
if (success) {
113+
setIsCopied(true)
114+
setTimeout(() => setIsCopied(false), 2000)
115+
}
116+
}
117+
}
118+
119+
return (
120+
<div className="table-wrapper" ref={ref}>
121+
<span
122+
className={`copy-table-button ${isCopied ? 'copied' : ''}`}
123+
onClick={handleCopy}
124+
title={isCopied ? '已复制!' : '复制表格'}
125+
></span>
126+
{props.children}
127+
</div>
128+
)
129+
}
130+
65131
export function PreCode(props: { children: any }) {
66132
const ref = useRef<HTMLPreElement>(null)
67133
const refText = ref.current?.innerText
@@ -164,7 +230,8 @@ function _MarkDownContent(props: { content: string }) {
164230
const isInternal = /^\/#/i.test(href)
165231
const target = isInternal ? '_self' : (aProps.target ?? '_blank')
166232
return <a {...aProps} target={target} />
167-
}
233+
},
234+
table: (tableProps) => <TableWrapper>{<table {...tableProps} />}</TableWrapper>
168235
}}
169236
>
170237
{escapedContent}

src/renderer/src/components/common/markdown/markdown.scss

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,66 @@
309309
overflow: auto;
310310
}
311311

312+
.markdown-body .table-wrapper {
313+
position: relative;
314+
margin-bottom: 16px;
315+
316+
.copy-table-button {
317+
position: absolute;
318+
top: 8px;
319+
right: 8px;
320+
width: 24px;
321+
height: 24px;
322+
background: rgba(0, 0, 0, 0.3);
323+
border: none;
324+
border-radius: 4px;
325+
cursor: pointer;
326+
opacity: 0;
327+
transition:
328+
opacity 0.2s ease,
329+
background-color 0.2s ease;
330+
z-index: 10;
331+
332+
/* 复制图标 */
333+
&::before {
334+
content: '';
335+
position: absolute;
336+
top: 50%;
337+
left: 50%;
338+
transform: translate(-50%, -50%);
339+
width: 14px;
340+
height: 14px;
341+
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z'/%3E%3C/svg%3E");
342+
background-repeat: no-repeat;
343+
background-position: center;
344+
background-size: contain;
345+
}
346+
347+
&:active {
348+
background: rgba(0, 0, 0, 0.6);
349+
}
350+
351+
/* 复制成功状态 */
352+
&.copied {
353+
background: rgba(34, 197, 94, 0.8);
354+
opacity: 1;
355+
356+
&::before {
357+
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z'/%3E%3C/svg%3E");
358+
}
359+
}
360+
}
361+
362+
&:hover .copy-table-button {
363+
opacity: 0.6;
364+
}
365+
366+
.copy-table-button:hover {
367+
background: rgba(0, 0, 0, 0.5);
368+
opacity: 1;
369+
}
370+
}
371+
312372
.markdown-body td,
313373
.markdown-body th {
314374
padding: 0;
@@ -1227,3 +1287,27 @@
12271287
}
12281288
}
12291289
}
1290+
1291+
.dark .markdown-body .table-wrapper .copy-table-button {
1292+
background: rgba(255, 255, 255, 0.15);
1293+
1294+
&::before {
1295+
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z'/%3E%3C/svg%3E");
1296+
}
1297+
1298+
&:hover {
1299+
background: rgba(255, 255, 255, 0.25);
1300+
}
1301+
1302+
&:active {
1303+
background: rgba(255, 255, 255, 0.35);
1304+
}
1305+
1306+
&.copied {
1307+
background: rgba(34, 197, 94, 0.8);
1308+
1309+
&::before {
1310+
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z'/%3E%3C/svg%3E");
1311+
}
1312+
}
1313+
}

0 commit comments

Comments
 (0)