ForcePilot/web/src/components/FileDetailModal.vue

248 lines
6.6 KiB
Vue
Raw Normal View History

<template>
<a-modal
v-model:open="visible"
width="1200px"
:footer="null"
wrap-class-name="file-detail"
@after-open-change="afterOpenChange"
:bodyStyle="{ height: '80vh', padding: '0' }"
>
<template #title>
<div class="modal-title-wrapper">
<span>{{ file?.filename || '文件详情' }}</span>
<div class="download-buttons" v-if="file">
<a-button
type="text"
size="small"
@click="handleDownloadOriginal"
:loading="downloadingOriginal"
:disabled="!file.file_id"
:icon="h(DownloadOutlined)"
>
下载原文
</a-button>
<a-button
type="text"
size="small"
@click="handleDownloadMarkdown"
:loading="downloadingMarkdown"
:disabled="!((file.lines && file.lines.length > 0) || file.content)"
:icon="h(DownloadOutlined)"
>
下载 Markdown
</a-button>
</div>
</div>
</template>
<div class="file-detail-content" v-if="file">
<div class="file-content-section" v-if="(file.lines && file.lines.length > 0) || file.content">
<MarkdownContentViewer :chunks="file.lines" :content="file.content" />
</div>
<div v-else-if="loading" class="loading-container">
<a-spin />
</div>
<div v-else class="empty-content">
<p>暂无文件内容</p>
</div>
</div>
</a-modal>
</template>
<script setup>
import { computed, ref, h } from 'vue';
import { useDatabaseStore } from '@/stores/database';
import { message } from 'ant-design-vue';
import { DownloadOutlined } from '@ant-design/icons-vue';
import { documentApi } from '@/apis/knowledge_api';
import { mergeChunks } from '@/utils/chunkUtils';
import MarkdownContentViewer from './MarkdownContentViewer.vue';
const store = useDatabaseStore();
const visible = computed({
get: () => store.state.fileDetailModalVisible,
set: (value) => store.state.fileDetailModalVisible = value
});
const file = computed(() => store.selectedFile);
const loading = computed(() => store.state.fileDetailLoading);
const downloadingOriginal = ref(false);
const downloadingMarkdown = ref(false);
const afterOpenChange = (open) => {
if (!open) {
store.selectedFile = null;
}
};
// 下载原文
const handleDownloadOriginal = async () => {
if (!file.value || !file.value.file_id) {
message.error('文件信息不完整');
return;
}
const dbId = store.databaseId;
if (!dbId) {
message.error('无法获取数据库ID请刷新页面后重试');
return;
}
downloadingOriginal.value = true;
try {
const response = await documentApi.downloadDocument(dbId, file.value.file_id);
// 获取文件名
const contentDisposition = response.headers.get('content-disposition');
let filename = file.value.filename;
if (contentDisposition) {
// 首先尝试匹配RFC 2231格式 filename*=UTF-8''...
const rfc2231Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
if (rfc2231Match) {
try {
filename = decodeURIComponent(rfc2231Match[1]);
} catch (error) {
console.warn('Failed to decode RFC2231 filename:', rfc2231Match[1], error);
}
} else {
// 回退到标准格式 filename="..."
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1].replace(/['"]/g, '');
// 解码URL编码的文件名
try {
filename = decodeURIComponent(filename);
} catch (error) {
console.warn('Failed to decode filename:', filename, error);
}
}
}
}
// 创建blob并下载
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success('下载成功');
} catch (error) {
console.error('下载文件时出错:', error);
message.error(error.message || '下载文件失败');
} finally {
downloadingOriginal.value = false;
}
};
// 下载 Markdown
const handleDownloadMarkdown = () => {
let content = '';
if (file.value.content) {
content = file.value.content;
} else if (file.value.lines && file.value.lines.length > 0) {
content = mergeChunks(file.value.lines).content;
}
if (!content) {
message.error('没有可下载的 Markdown 内容');
return;
}
downloadingMarkdown.value = true;
try {
// 生成文件名(如果原文件没有 .md 扩展名,则添加)
let filename = file.value.filename || 'document.md';
if (!filename.toLowerCase().endsWith('.md')) {
// 移除原扩展名,添加 .md
const lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex > 0) {
filename = filename.substring(0, lastDotIndex) + '.md';
} else {
filename = filename + '.md';
}
}
// 创建 blob 并下载
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success('下载成功');
} catch (error) {
console.error('下载 Markdown 时出错:', error);
message.error(error.message || '下载 Markdown 失败');
} finally {
downloadingMarkdown.value = false;
}
};
</script>
<style scoped>
.file-detail-content {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.file-content-section h4 {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.empty-content {
text-align: center;
padding: 40px 0;
Add dark/light theme toggle feature (#343) * Add dark/light theme toggle feature * update: refine dark/light theme and related components * 调整语义化主题变量,优化暗/亮模式切换效果 * Revert "调整语义化主题变量,优化暗/亮模式切换效果" This reverts commit 85d9373297686fdbacb252a880b2847d20a39641. * 深色模式适配:减少 !important,迁移 CSS 变量,优化布局 * style: 替换硬编码颜色值为CSS变量以支持主题切换 将多处硬编码的颜色值(如#fff、#f0f0f0等)替换为CSS变量(如--gray-0、--gray-150等),统一管理颜色样式,便于主题切换和样式维护。主要修改包括背景色、边框色、文字色等视觉元素,同时移除不再需要的深色模式适配代码。 * style: 使用CSS变量替换硬编码颜色值 * refactor(theme): 重构主题系统,统一使用CSS变量并优化暗色模式实现 - 移除冗余的theme.js配置文件,将主题配置集中到theme store - 使用ant-design-vue的darkAlgorithm实现暗色模式 - 在多个组件中替换硬编码颜色值为CSS变量 - 优化用户信息组件,整合文档中心和主题切换功能 - 更新基础CSS样式,完善颜色系统和滚动条样式 * reset: 恢复被覆盖的修改(96d257a7ace38c94e2b113d56472d211d1141087) * feat(theme): 实现深色主题支持并重构样式系统 重构颜色变量系统,添加深色主题支持 移除硬编码颜色值,统一使用CSS变量 优化图表组件以响应主题切换 清理无用样式代码,提升可维护性 * docs: 更新开发规范文档 * style: 调整边框和背景颜色样式 --------- Co-authored-by: Wenjie Zhang <xerrors@163.com>
2025-11-23 01:39:44 +08:00
color: var(--gray-400);
}
</style>
<style lang="less">
.file-detail {
.ant-modal {
top: 20px;
}
.ant-modal-header {
.ant-modal-title {
width: 100%;
}
}
}
.modal-title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 8px;
}
.download-buttons {
display: flex;
gap: 8px;
margin: 0 16px;
flex-shrink: 0;
}
</style>