|
|
@@ -85,7 +85,7 @@
|
|
|
<div v-else class="no-preview-text">CSV文件内容为空</div>
|
|
|
</div>
|
|
|
<div v-else-if="isMdFile(file.name)" class="markdown-preview">
|
|
|
- <div class="markdown-body" v-html="renderedMarkdown"></div>
|
|
|
+ <div class="markdown-body" v-html="renderedMarkdown" @click="handleTableAction"></div>
|
|
|
</div>
|
|
|
<div v-else-if="isTextFile(file.name)" class="text-preview">
|
|
|
<pre>{{ file.content || '文件内容加载中...' }}</pre>
|
|
|
@@ -118,6 +118,7 @@
|
|
|
getFileExtension,
|
|
|
getFileIconVar,
|
|
|
renderMarkdown,
|
|
|
+ wrapMarkdownCodeBlocks,
|
|
|
} from './utils';
|
|
|
import type { AttachedFile } from './types';
|
|
|
|
|
|
@@ -126,6 +127,13 @@
|
|
|
return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
|
|
|
};
|
|
|
|
|
|
+ const tableIcons = {
|
|
|
+ copy: getCssUrl('--img-chat-copy-icon'),
|
|
|
+ download: getCssUrl('--img-chat-download-icon-btn'),
|
|
|
+ preview: getCssUrl('--img-chat-preview-icon'),
|
|
|
+ copySuccess: getCssUrl('--img-chat-copy-success-icon'),
|
|
|
+ };
|
|
|
+
|
|
|
const props = withDefaults(
|
|
|
defineProps<{
|
|
|
file: AttachedFile;
|
|
|
@@ -139,6 +147,7 @@
|
|
|
const emit = defineEmits<{
|
|
|
(e: 'close'): void;
|
|
|
(e: 'save-success', content: string): void;
|
|
|
+ (e: 'open-markdown-tab', payload: { title: string; content: string }): void;
|
|
|
}>();
|
|
|
|
|
|
// 编辑模式
|
|
|
@@ -232,9 +241,77 @@
|
|
|
});
|
|
|
|
|
|
const renderedMarkdown = computed(() => {
|
|
|
- return renderMarkdown(props.file.content || '');
|
|
|
+ return wrapMarkdownCodeBlocks(renderMarkdown(props.file.content || ''), tableIcons);
|
|
|
});
|
|
|
|
|
|
+ // Markdown 代码块工具栏操作(复制/下载/预览)
|
|
|
+
|
|
|
+ const copyToClipboard = async (text: string) => {
|
|
|
+ try {
|
|
|
+ await navigator.clipboard.writeText(text);
|
|
|
+ } catch {
|
|
|
+ const ta = document.createElement('textarea');
|
|
|
+ ta.value = text;
|
|
|
+ ta.style.position = 'fixed';
|
|
|
+ ta.style.opacity = '0';
|
|
|
+ document.body.appendChild(ta);
|
|
|
+ ta.select();
|
|
|
+ document.execCommand('copy');
|
|
|
+ document.body.removeChild(ta);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const showCopySuccess = (btn: HTMLElement) => {
|
|
|
+ const img = btn.querySelector('img');
|
|
|
+ const successIcon = btn.dataset.iconSuccess;
|
|
|
+ const originalIcon = btn.dataset.icon;
|
|
|
+ if (img && successIcon) {
|
|
|
+ img.src = successIcon;
|
|
|
+ btn.title = '复制成功';
|
|
|
+ const wrapper = btn.closest('.markdown-code-wrapper')!;
|
|
|
+ const reset = () => {
|
|
|
+ img.src = originalIcon!;
|
|
|
+ btn.title = '复制内容';
|
|
|
+ wrapper.removeEventListener('mouseleave', reset);
|
|
|
+ };
|
|
|
+ wrapper.addEventListener('mouseleave', reset);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleTableAction = (e: Event) => {
|
|
|
+ const target = (e.target as HTMLElement).closest('.table-action-btn') as HTMLElement;
|
|
|
+ if (!target) return;
|
|
|
+
|
|
|
+ const action = target.dataset.action;
|
|
|
+ const encodedContent = target.dataset.content;
|
|
|
+ if (!action || !encodedContent) return;
|
|
|
+
|
|
|
+ const content = decodeURIComponent(encodedContent);
|
|
|
+
|
|
|
+ switch (action) {
|
|
|
+ case 'copy-md': {
|
|
|
+ copyToClipboard(content).then(() => showCopySuccess(target));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'download-md': {
|
|
|
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8;' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = `markdown_${Date.now()}.md`;
|
|
|
+ a.click();
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'preview-md': {
|
|
|
+ // 在右侧面板新建标签页展示渲染效果
|
|
|
+ const title = content.match(/^#{1,3}\s+(.+?)\s*$/m)?.[1] || 'Markdown预览';
|
|
|
+ emit('open-markdown-tab', { title, content });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const csvRows = computed(() => {
|
|
|
const content = props.file.content || '';
|
|
|
if (!content.trim()) return [];
|
|
|
@@ -770,6 +847,45 @@
|
|
|
border: none;
|
|
|
border-top: 1px solid rgb(255 255 255 / 10%);
|
|
|
}
|
|
|
+
|
|
|
+ :deep(.markdown-code-wrapper) {
|
|
|
+ position: relative;
|
|
|
+ margin: 6px 0;
|
|
|
+
|
|
|
+ .table-actions {
|
|
|
+ position: absolute;
|
|
|
+ top: 4px;
|
|
|
+ right: 4px;
|
|
|
+ display: flex;
|
|
|
+ gap: 2px;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover .table-actions {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-action-btn {
|
|
|
+ width: 26px;
|
|
|
+ height: 26px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #8b949e;
|
|
|
+ cursor: pointer;
|
|
|
+ background: rgba(22, 27, 34, 0.8);
|
|
|
+ backdrop-filter: blur(4px);
|
|
|
+ transition: all 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #e0e6ed;
|
|
|
+ background: rgba(10, 132, 255, 0.3);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|