ForcePilot/web/src/components/FileDetailModal.vue
Zhao Minhao 55f2e020b5
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

244 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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"
: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">
<MarkdownContentViewer :chunks="file.lines" />
</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 = () => {
if (!file.value || !file.value.lines || file.value.lines.length === 0) {
message.error('没有可下载的 Markdown 内容');
return;
}
downloadingMarkdown.value = true;
try {
// 合并 chunks 生成完整的 Markdown 内容
const { content } = mergeChunks(file.value.lines);
// 生成文件名(如果原文件没有 .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;
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>