ForcePilot/web/src/components/MarkdownContentViewer.vue
Wenjie Zhang 62685b7ffe refactor: 重构组件和样式以改善UI/UX
- 重构FileDetailModal.vue,增加了下载选项下拉菜单并优化了布局。
- 移除部分页面的响应式媒体查询,不做响应式适配,仅适配大众尺寸
- 改进了ChunkParamsConfig.vue的布局,更好地分组参数。
- 调整了FileTable.vue,改善排序和筛选UI,包括状态筛选逻辑。
- 优化了FileUploadModal.vue的布局,提高OCR引擎配置的可见性。
- 其他  AiTextarea.vue / DataBaseView.vue / AppLayout.vue / file_utils.js / DataBaseInfoView.vue / FeedbackModalComponent.vue / FileTable.vue
2026-01-15 04:54:53 +08:00

414 lines
9.3 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>
<div class="markdown-content-viewer">
<div class="viewer-header">
<h4>文件内容</h4>
<div class="header-controls">
<div class="header-info">
<span v-if="mappedChunks.length > 0">共 {{ mappedChunks.length }} 个片段</span>
<span>总长度 {{ formatTextLength(mergedContent.length) }}</span>
</div>
<button
class="toggle-btn"
v-if="mappedChunks.length > 0"
@click="toggleChunkPanel"
:title="chunkPanelVisible ? '隐藏片段列表' : '显示片段列表'"
>
<ChevronLeft v-if="chunkPanelVisible" :size="14" />
<ChevronRight v-else :size="14" />
</button>
</div>
</div>
<div class="viewer-container">
<!-- 左侧Markdown内容 -->
<div class="content-panel">
<MdPreview
:modelValue="mergedContent"
:theme="theme"
previewTheme="github"
class="markdown-content"
/>
</div>
<!-- 右侧Chunk信息 -->
<div
class="chunk-panel"
v-show="chunkPanelVisible"
>
<div class="chunk-list">
<div
v-for="(chunk, index) in mappedChunks"
:key="chunk.id"
class="chunk-item"
:class="{
'active': activeChunkIndex === index,
'highlighted': highlightedChunkIndex === index
}"
@click="handleChunkClick(index)"
@mouseenter="highlightChunk(index)"
@mouseleave="unhighlightChunk()"
>
<div class="chunk-header">
<span class="chunk-order">#{{ chunk.chunk_order_index }}</span>
<span class="chunk-id">{{ chunk.id }}</span>
</div>
<div class="chunk-meta">
<span class="chunk-length">{{ formatTextLength(chunk.content.length) }}</span>
<span class="chunk-range">{{ chunk.startOffset }}-{{ chunk.endOffset }}</span>
</div>
<div class="chunk-preview">{{ getChunkPreview(chunk.content) }}</div>
</div>
</div>
</div>
</div>
<!-- 悬浮提示 -->
<div
v-if="showTooltip && currentChunk"
class="chunk-tooltip"
:style="tooltipStyle"
>
<div class="tooltip-header">
<strong>片段信息</strong>
</div>
<div class="tooltip-content">
<div class="tooltip-row">
<span class="label">ID:</span>
<span class="value">{{ currentChunk.id }}</span>
</div>
<div class="tooltip-row">
<span class="label">序号:</span>
<span class="value">{{ currentChunk.chunk_order_index }}</span>
</div>
<div class="tooltip-row">
<span class="label">位置:</span>
<span class="value">{{ currentChunk.startOffset }} - {{ currentChunk.endOffset }}</span>
</div>
<div class="tooltip-row">
<span class="label">长度:</span>
<span class="value">{{ formatTextLength(currentChunk.content.length) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
import { mergeChunks, getChunkPreview } from '@/utils/chunkUtils';
import { useThemeStore } from '@/stores/theme';
import { ChevronRight, ChevronLeft } from 'lucide-vue-next';
const props = defineProps({
chunks: {
type: Array,
default: () => []
},
content: {
type: String,
default: ''
}
});
// 使用主题store
const themeStore = useThemeStore();
// 响应式引用
const showTooltip = ref(false);
const currentChunk = ref(null);
const tooltipStyle = ref({ top: '0px', left: '0px' });
const activeChunkIndex = ref(null);
const highlightedChunkIndex = ref(null);
const chunkPanelVisible = ref(false);
// 主题设置 - 根据系统主题动态切换
const theme = computed(() => themeStore.isDark ? 'dark' : 'light');
// 合并chunks
const mergeResult = computed(() => mergeChunks(props.chunks));
const mergedContent = computed(() => props.content || mergeResult.value.content);
const mappedChunks = computed(() => mergeResult.value.chunks);
// 格式化文本长度
function formatTextLength(length) {
if (!length && length !== 0) return '0 字符';
if (length < 1000) {
return `${length} 字符`;
} else {
return `${(length / 1000).toFixed(1)}k 字符`;
}
}
// 高亮chunk
function highlightChunk(index) {
if (!mappedChunks.value?.[index]) return;
highlightedChunkIndex.value = index;
currentChunk.value = mappedChunks.value[index];
}
function unhighlightChunk() {
highlightedChunkIndex.value = null;
currentChunk.value = null;
}
// 处理chunk点击
function handleChunkClick(index) {
if (!mappedChunks.value?.[index]) return;
activeChunkIndex.value = index;
}
// 切换chunk面板显示
function toggleChunkPanel() {
chunkPanelVisible.value = !chunkPanelVisible.value;
}
// 处理鼠标移动显示tooltip
function handleMouseMove(event) {
if (!currentChunk.value) return;
tooltipStyle.value = {
top: event.clientY + 10 + 'px',
left: event.clientX + 10 + 'px'
};
}
// 生命周期
onMounted(() => {
document.addEventListener('mousemove', handleMouseMove);
});
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove);
});
</script>
<style scoped>
.markdown-content-viewer {
display: flex;
flex-direction: column;
height: 100%;
border: 1px solid var(--gray-200);
border-radius: 6px;
overflow: hidden;
}
.viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 16px;
background: var(--gray-25);
border-bottom: 1px solid var(--gray-200);
}
.header-controls {
display: flex;
align-items: center;
gap: 16px;
}
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-100);
border: none;
border-radius: 4px;
width: 24px;
height: 24px;
padding: 0;
cursor: pointer;
transition: all 0.2s ease;
color: var(--gray-700);
}
.toggle-btn:hover {
background: var(--gray-200);
}
.viewer-header h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.header-info {
display: flex;
gap: 16px;
font-size: 12px;
color: var(--gray-500);
}
.viewer-container {
display: flex;
flex: 1;
overflow: hidden;
}
.content-panel {
flex: 1;
overflow-y: auto;
padding: 16px;
background: var(--gray-0);
border-right: 1px solid var(--gray-200);
min-height: 0; /* 关键确保flex项目可以缩小 */
}
.markdown-content {
min-height: 100%;
overflow: visible;
}
/* MdPreview 组件样式覆盖 */
.markdown-content :deep(.md-editor) {
/* height: auto !important; */
min-height: 100%;
}
.markdown-content :deep(.md-editor-preview) {
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
/* height: auto !important; */
font-size: small;
min-height: 100%;
}
.markdown-content :deep(.md-editor-preview-wrapper) {
padding: 0;
/* height: auto !important; */
min-height: 100%;
}
.chunk-panel {
width: 300px;
overflow-y: auto;
background: var(--gray-25);
padding: 16px;
min-height: 0; /* 确保flex项目可以缩小 */
}
.chunk-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.chunk-item {
background: var(--gray-0);
border: 1px solid var(--gray-200);
border-radius: 6px;
padding: 12px;
/* cursor: pointer; */
/* transition: all 0.2s ease; */
/* user-select: none; */
}
.chunk-item:hover {
border-color: var(--main-color);
box-shadow: 0 2px 8px rgba(1, 97, 121, 0.1);
}
/* .chunk-item.active {
border-color: var(--main-color);
background: var(--main-50);
box-shadow: 0 2px 8px rgba(1, 97, 121, 0.2);
} */
/*
.chunk-item.highlighted {
border-color: var(--color-success-500);
background: var(--main-50);
} */
.chunk-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.chunk-order {
font-weight: 600;
color: var(--main-color);
font-size: 12px;
}
.chunk-id {
font-size: 11px;
color: var(--gray-500);
font-family: monospace;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chunk-meta {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 11px;
color: var(--gray-600);
}
.chunk-preview {
font-size: 11px;
color: var(--gray-500);
line-height: 1.4;
/* max-height: 40px; */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
line-clamp: 4;
}
.chunk-tooltip {
position: fixed;
background: var(--gray-0);
border: 1px solid var(--gray-300);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 12px;
z-index: 1000;
min-width: 250px;
max-width: 350px;
pointer-events: none;
}
.tooltip-header {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid var(--gray-200);
}
.tooltip-content {
display: flex;
flex-direction: column;
gap: 6px;
}
.tooltip-row {
display: flex;
align-items: flex-start;
gap: 8px;
}
.tooltip-row .label {
font-weight: 500;
color: var(--gray-600);
min-width: 50px;
flex-shrink: 0;
}
.tooltip-row .value {
color: var(--gray-1000);
flex: 1;
word-break: break-all;
}
</style>