Skip to content

Commit 47bf8f4

Browse files
committed
feat:
- 在 FileDetailModal 组件中,新增下载原文和下载 Markdown 按钮。 - 实现下载原文和 Markdown 的逻辑,包括文件名处理和错误提示。 - 优化模态框标题样式,增强用户体验。
1 parent 2f64a4b commit 47bf8f4

File tree

2 files changed

+164
-5
lines changed

2 files changed

+164
-5
lines changed

docs/latest/changelog/roadmap.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
### 新增
2020
- 优化知识库详情页面,更加简洁清晰
2121
- 新增对于上传文件的智能体中间件
22+
- 增强文件下载功能
2223

2324
### 修复
2425
- 修复重排序模型实际未生效的问题

web/src/components/FileDetailModal.vue

Lines changed: 163 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
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" />
@@ -25,8 +51,13 @@
2551
</template>
2652

2753
<script setup>
28-
import { computed } from 'vue';
54+
import { computed, ref, h } from 'vue';
2955
import { 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
3162
const store = useDatabaseStore();
3263
@@ -37,16 +68,122 @@ const visible = computed({
3768
3869
const file = computed(() => store.selectedFile);
3970
const loading = computed(() => store.state.fileDetailLoading);
71+
const downloadingOriginal = ref(false);
72+
const downloadingMarkdown = ref(false);
4073
4174
const 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

Comments
 (0)