11<template >
22 <a-modal
33 v-model:open =" visible"
4- :title =" file?.filename || '文件详情'"
54 width =" 1200px"
65 :footer =" null"
76 wrap-class-name =" file-detail"
87 @after-open-change =" afterOpenChange"
98 :bodyStyle =" { height: '80vh', padding: '0' }"
109 >
10+ <template #title >
11+ <div class =" modal-title-wrapper" >
12+ <span >{{ file?.filename || '文件详情' }}</span >
13+ <div class =" download-buttons" v-if =" file" >
14+ <a-button
15+ type =" text"
16+ size =" small"
17+ @click =" handleDownloadOriginal"
18+ :loading =" downloadingOriginal"
19+ :disabled =" !file.file_id"
20+ :icon =" h(DownloadOutlined)"
21+ >
22+ 下载原文
23+ </a-button >
24+ <a-button
25+ type =" text"
26+ size =" small"
27+ @click =" handleDownloadMarkdown"
28+ :loading =" downloadingMarkdown"
29+ :disabled =" !file.lines || file.lines.length === 0"
30+ :icon =" h(DownloadOutlined)"
31+ >
32+ 下载 Markdown
33+ </a-button >
34+ </div >
35+ </div >
36+ </template >
1137 <div class =" file-detail-content" v-if =" file" >
1238 <div class =" file-content-section" v-if =" file.lines && file.lines.length > 0" >
1339 <MarkdownContentViewer :chunks =" file.lines" />
2551</template >
2652
2753<script setup>
28- import { computed } from ' vue' ;
54+ import { computed , ref , h } from ' vue' ;
2955import { useDatabaseStore } from ' @/stores/database' ;
56+ import { message } from ' ant-design-vue' ;
57+ import { DownloadOutlined } from ' @ant-design/icons-vue' ;
58+ import { documentApi } from ' @/apis/knowledge_api' ;
59+ import { mergeChunks } from ' @/utils/chunkUtils' ;
60+ import MarkdownContentViewer from ' ./MarkdownContentViewer.vue' ;
3061
3162const store = useDatabaseStore ();
3263
@@ -37,16 +68,122 @@ const visible = computed({
3768
3869const file = computed (() => store .selectedFile );
3970const loading = computed (() => store .state .fileDetailLoading );
71+ const downloadingOriginal = ref (false );
72+ const downloadingMarkdown = ref (false );
4073
4174const afterOpenChange = (open ) => {
4275 if (! open) {
4376 store .selectedFile = null ;
4477 }
4578};
4679
47- // 导入工具函数
48- import { getStatusText , formatStandardTime } from ' @/utils/file_utils' ;
49- import MarkdownContentViewer from ' ./MarkdownContentViewer.vue' ;
80+ // 下载原文
81+ const handleDownloadOriginal = async () => {
82+ if (! file .value || ! file .value .file_id ) {
83+ message .error (' 文件信息不完整' );
84+ return ;
85+ }
86+
87+ const dbId = store .databaseId ;
88+ if (! dbId) {
89+ message .error (' 无法获取数据库ID,请刷新页面后重试' );
90+ return ;
91+ }
92+
93+ downloadingOriginal .value = true ;
94+ try {
95+ const response = await documentApi .downloadDocument (dbId, file .value .file_id );
96+
97+ // 获取文件名
98+ const contentDisposition = response .headers .get (' content-disposition' );
99+ let filename = file .value .filename ;
100+ if (contentDisposition) {
101+ // 首先尝试匹配RFC 2231格式 filename*=UTF-8''...
102+ const rfc2231Match = contentDisposition .match (/ filename\* =UTF-8''([^ ;] + )/ );
103+ if (rfc2231Match) {
104+ try {
105+ filename = decodeURIComponent (rfc2231Match[1 ]);
106+ } catch (error) {
107+ console .warn (' Failed to decode RFC2231 filename:' , rfc2231Match[1 ], error);
108+ }
109+ } else {
110+ // 回退到标准格式 filename="..."
111+ const filenameMatch = contentDisposition .match (/ filename[^ ;=\n ] * =((['"] ). *? \2 | [^ ;\n ] * )/ );
112+ if (filenameMatch && filenameMatch[1 ]) {
113+ filename = filenameMatch[1 ].replace (/ ['"] / g , ' ' );
114+ // 解码URL编码的文件名
115+ try {
116+ filename = decodeURIComponent (filename);
117+ } catch (error) {
118+ console .warn (' Failed to decode filename:' , filename, error);
119+ }
120+ }
121+ }
122+ }
123+
124+ // 创建blob并下载
125+ const blob = await response .blob ();
126+ const url = window .URL .createObjectURL (blob);
127+ const link = document .createElement (' a' );
128+ link .href = url;
129+ link .download = filename;
130+ link .style .display = ' none' ;
131+ document .body .appendChild (link);
132+ link .click ();
133+ document .body .removeChild (link);
134+ window .URL .revokeObjectURL (url);
135+ message .success (' 下载成功' );
136+ } catch (error) {
137+ console .error (' 下载文件时出错:' , error);
138+ message .error (error .message || ' 下载文件失败' );
139+ } finally {
140+ downloadingOriginal .value = false ;
141+ }
142+ };
143+
144+ // 下载 Markdown
145+ const handleDownloadMarkdown = () => {
146+ if (! file .value || ! file .value .lines || file .value .lines .length === 0 ) {
147+ message .error (' 没有可下载的 Markdown 内容' );
148+ return ;
149+ }
150+
151+ downloadingMarkdown .value = true ;
152+ try {
153+ // 合并 chunks 生成完整的 Markdown 内容
154+ const { content } = mergeChunks (file .value .lines );
155+
156+ // 生成文件名(如果原文件没有 .md 扩展名,则添加)
157+ let filename = file .value .filename || ' document.md' ;
158+ if (! filename .toLowerCase ().endsWith (' .md' )) {
159+ // 移除原扩展名,添加 .md
160+ const lastDotIndex = filename .lastIndexOf (' .' );
161+ if (lastDotIndex > 0 ) {
162+ filename = filename .substring (0 , lastDotIndex) + ' .md' ;
163+ } else {
164+ filename = filename + ' .md' ;
165+ }
166+ }
167+
168+ // 创建 blob 并下载
169+ const blob = new Blob ([content], { type: ' text/markdown;charset=utf-8' });
170+ const url = window .URL .createObjectURL (blob);
171+ const link = document .createElement (' a' );
172+ link .href = url;
173+ link .download = filename;
174+ link .style .display = ' none' ;
175+ document .body .appendChild (link);
176+ link .click ();
177+ document .body .removeChild (link);
178+ window .URL .revokeObjectURL (url);
179+ message .success (' 下载成功' );
180+ } catch (error) {
181+ console .error (' 下载 Markdown 时出错:' , error);
182+ message .error (error .message || ' 下载 Markdown 失败' );
183+ } finally {
184+ downloadingMarkdown .value = false ;
185+ }
186+ };
50187 </script >
51188
52189<style scoped>
@@ -82,5 +219,26 @@ import MarkdownContentViewer from './MarkdownContentViewer.vue';
82219 .ant-modal {
83220 top : 20px ;
84221 }
222+
223+ .ant-modal-header {
224+ .ant-modal-title {
225+ width : 100% ;
226+ }
227+ }
228+ }
229+
230+ .modal-title-wrapper {
231+ display : flex ;
232+ justify-content : space-between ;
233+ align-items : center ;
234+ width : 100% ;
235+ padding-right : 8px ;
236+ }
237+
238+ .download-buttons {
239+ display : flex ;
240+ gap : 8px ;
241+ margin : 0 16px ;
242+ flex-shrink : 0 ;
85243}
86244 </style >
0 commit comments