|
|
@@ -11,26 +11,58 @@
|
|
|
<img :src="file.preview" alt="预览图片" />
|
|
|
</div>
|
|
|
<div v-else-if="isPdfFile(file.name)" class="pdf-preview">
|
|
|
- <iframe v-if="file.preview" :src="file.preview" frameborder="0"></iframe>
|
|
|
+ <iframe v-if="file.preview" :src="pdfSrc" frameborder="0"></iframe>
|
|
|
<div v-else class="no-preview-text">无法加载PDF预览</div>
|
|
|
</div>
|
|
|
- <div v-else-if="isVideoFile(file.name)" class="video-preview">
|
|
|
- <video v-if="file.preview" controls>
|
|
|
- <source :src="file.preview" />
|
|
|
- 您的浏览器不支持视频预览
|
|
|
- </video>
|
|
|
- <div v-else class="no-preview-text">无法加载视频预览</div>
|
|
|
+ <div v-else-if="isOldDocFile(file.name)" class="no-preview">
|
|
|
+ <div class="no-preview-icon"></div>
|
|
|
+ <div class="no-preview-text">暂不支持 .doc 格式预览</div>
|
|
|
+ <div class="no-preview-hint">请将文件转换为 .docx 格式后重新上传</div>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="isWordFile(file.name)" class="word-preview">
|
|
|
+ <VueOfficeDocx v-if="file.arrayBuffer" :src="file.arrayBuffer" @rendered="handleRendered" @error="handleError" />
|
|
|
+ <div v-else class="no-preview-text">无法加载Word文档</div>
|
|
|
</div>
|
|
|
- <div v-else-if="isAudioFile(file.name)" class="audio-preview">
|
|
|
- <div class="audio-info">
|
|
|
- <div class="audio-icon"></div>
|
|
|
- <span class="audio-name">{{ file.name }}</span>
|
|
|
+ <div v-else-if="isExcelFile(file.name)" class="excel-preview">
|
|
|
+ <div v-if="excelSheets.length > 1" class="excel-tabs">
|
|
|
+ <div v-for="(sheet, i) in excelSheets" :key="i" :class="['excel-tab', { active: activeSheet === i }]" @click="activeSheet = i">
|
|
|
+ {{ sheet.name }}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <audio v-if="file.preview" controls>
|
|
|
- <source :src="file.preview" />
|
|
|
- 您的浏览器不支持音频预览
|
|
|
- </audio>
|
|
|
- <div v-else class="no-preview-text">无法加载音频预览</div>
|
|
|
+ <div v-if="excelLoading" class="no-preview-text">加载中...</div>
|
|
|
+ <div v-else-if="currentSheetData.length > 0" class="excel-table-wrapper">
|
|
|
+ <table class="excel-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th v-for="(cell, i) in currentSheetData[0]" :key="i">{{ cell }}</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="(row, ri) in currentSheetData.slice(1)" :key="ri">
|
|
|
+ <td v-for="(cell, ci) in row" :key="ci">{{ cell }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ <div v-else class="no-preview-text">Excel文件内容为空</div>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="isCsvFile(file.name)" class="csv-preview">
|
|
|
+ <table v-if="csvRows.length > 0" class="csv-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th v-for="(cell, i) in csvRows[0]" :key="i">{{ cell }}</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="(row, ri) in csvRows.slice(1)" :key="ri">
|
|
|
+ <td v-for="(cell, ci) in row" :key="ci">{{ cell }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ <div v-else class="no-preview-text">CSV文件内容为空</div>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="isMdFile(file.name)" class="markdown-preview">
|
|
|
+ <div class="markdown-body" v-html="renderedMarkdown"></div>
|
|
|
</div>
|
|
|
<div v-else-if="isTextFile(file.name)" class="text-preview">
|
|
|
<pre>{{ file.content || '文件内容加载中...' }}</pre>
|
|
|
@@ -45,16 +77,121 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
- import { isImageFile, isPdfFile, isVideoFile, isAudioFile, isTextFile, getFileExtension } from './utils';
|
|
|
+ import { ref, computed, watch } from 'vue';
|
|
|
+ import VueOfficeDocx from '@vue-office/docx/lib/v3/vue-office-docx.mjs';
|
|
|
+ import * as XLSX from 'xlsx';
|
|
|
+ import {
|
|
|
+ isImageFile,
|
|
|
+ isPdfFile,
|
|
|
+ isWordFile,
|
|
|
+ isOldDocFile,
|
|
|
+ isExcelFile,
|
|
|
+ isCsvFile,
|
|
|
+ isMdFile,
|
|
|
+ isTextFile,
|
|
|
+ getFileExtension,
|
|
|
+ renderMarkdown,
|
|
|
+ } from './utils';
|
|
|
import type { AttachedFile } from './types';
|
|
|
|
|
|
- defineProps<{
|
|
|
+ const props = defineProps<{
|
|
|
file: AttachedFile;
|
|
|
}>();
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
(e: 'close'): void;
|
|
|
}>();
|
|
|
+
|
|
|
+ // Excel 解析
|
|
|
+ const excelSheets = ref<{ name: string; data: string[][] }[]>([]);
|
|
|
+ const activeSheet = ref(0);
|
|
|
+ const excelLoading = ref(false);
|
|
|
+
|
|
|
+ const currentSheetData = computed(() => {
|
|
|
+ return excelSheets.value[activeSheet.value]?.data || [];
|
|
|
+ });
|
|
|
+
|
|
|
+ const parseExcel = async (file: AttachedFile) => {
|
|
|
+ excelLoading.value = true;
|
|
|
+ try {
|
|
|
+ let buffer = file.arrayBuffer;
|
|
|
+ if (!buffer && file.originalFile) {
|
|
|
+ buffer = await file.originalFile.arrayBuffer();
|
|
|
+ }
|
|
|
+ if (!buffer) return;
|
|
|
+ const workbook = XLSX.read(buffer, { type: 'array' });
|
|
|
+ excelSheets.value = workbook.SheetNames.map((name) => {
|
|
|
+ const sheet = workbook.Sheets[name];
|
|
|
+ const rows = XLSX.utils.sheet_to_json<string[]>(sheet, { header: 1, defval: '' });
|
|
|
+ return { name, data: rows };
|
|
|
+ });
|
|
|
+ activeSheet.value = 0;
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Excel解析失败:', err);
|
|
|
+ excelSheets.value = [];
|
|
|
+ } finally {
|
|
|
+ excelLoading.value = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.file,
|
|
|
+ (file) => {
|
|
|
+ if (isExcelFile(file.name)) {
|
|
|
+ parseExcel(file);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+ );
|
|
|
+
|
|
|
+ const pdfSrc = computed(() => {
|
|
|
+ const base = props.file.preview || '';
|
|
|
+ if (!base) return '';
|
|
|
+ return `${base}#toolbar=0&navpanes=0&scrollbar=0`;
|
|
|
+ });
|
|
|
+
|
|
|
+ const renderedMarkdown = computed(() => {
|
|
|
+ return renderMarkdown(props.file.content || '');
|
|
|
+ });
|
|
|
+
|
|
|
+ const csvRows = computed(() => {
|
|
|
+ const content = props.file.content || '';
|
|
|
+ if (!content.trim()) return [];
|
|
|
+ return content
|
|
|
+ .split(/\r?\n/)
|
|
|
+ .filter((line) => line.trim())
|
|
|
+ .map((line) => {
|
|
|
+ const cells: string[] = [];
|
|
|
+ let current = '';
|
|
|
+ let inQuotes = false;
|
|
|
+ for (let i = 0; i < line.length; i++) {
|
|
|
+ const ch = line[i];
|
|
|
+ if (ch === '"') {
|
|
|
+ if (inQuotes && line[i + 1] === '"') {
|
|
|
+ current += '"';
|
|
|
+ i++;
|
|
|
+ } else {
|
|
|
+ inQuotes = !inQuotes;
|
|
|
+ }
|
|
|
+ } else if (ch === ',' && !inQuotes) {
|
|
|
+ cells.push(current);
|
|
|
+ current = '';
|
|
|
+ } else {
|
|
|
+ current += ch;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ cells.push(current);
|
|
|
+ return cells;
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ const handleRendered = () => {
|
|
|
+ console.log('文件渲染完成');
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleError = (err: any) => {
|
|
|
+ console.error('文件预览失败:', err);
|
|
|
+ };
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
@@ -88,9 +225,6 @@
|
|
|
cursor: pointer;
|
|
|
width: 36px;
|
|
|
height: 36px;
|
|
|
- // background-image: var(--img-chat-close-bg);
|
|
|
- // background-repeat: no-repeat;
|
|
|
- // background-size: 100% 100%;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
@@ -143,49 +277,286 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .video-preview {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
+ .word-preview {
|
|
|
+ width: calc(100% + 32px);
|
|
|
+ height: calc(100% + 32px);
|
|
|
+ margin: -16px;
|
|
|
+ overflow-x: auto;
|
|
|
+ overflow-y: auto;
|
|
|
|
|
|
- video {
|
|
|
- width: 100%;
|
|
|
- max-height: 100%;
|
|
|
- border-radius: 4px;
|
|
|
+ :deep(.docx-wrapper) {
|
|
|
+ padding: 0;
|
|
|
+ background: transparent;
|
|
|
+ display: inline-block;
|
|
|
+ min-width: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .audio-preview {
|
|
|
- width: 100%;
|
|
|
+ .excel-preview {
|
|
|
+ width: calc(100% + 32px);
|
|
|
+ height: calc(100% + 32px);
|
|
|
+ margin: -16px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- gap: 16px;
|
|
|
- padding: 20px 0;
|
|
|
+ overflow: hidden;
|
|
|
|
|
|
- .audio-info {
|
|
|
+ .excel-tabs {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
+ gap: 0;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 0 16px;
|
|
|
+
|
|
|
+ .excel-tab {
|
|
|
+ padding: 8px 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #8b949e;
|
|
|
+ cursor: pointer;
|
|
|
+ border-bottom: 2px solid transparent;
|
|
|
+ transition: all 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #e0e6ed;
|
|
|
+ }
|
|
|
|
|
|
- .audio-icon {
|
|
|
- width: 40px;
|
|
|
- height: 40px;
|
|
|
- background-image: var(--img-chat-attach-icon);
|
|
|
- background-repeat: no-repeat;
|
|
|
- background-size: 100% 100%;
|
|
|
+ &.active {
|
|
|
+ color: #e0e6ed;
|
|
|
+ border-bottom-color: #3a8fd4;
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- .audio-name {
|
|
|
- font-size: 14px;
|
|
|
- color: #e0e6ed;
|
|
|
+ .excel-table-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ overflow: auto;
|
|
|
+ padding: 0 16px;
|
|
|
+
|
|
|
+ .excel-table {
|
|
|
+ border-collapse: collapse;
|
|
|
+ width: max-content;
|
|
|
+ min-width: 100%;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ th,
|
|
|
+ td {
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.15);
|
|
|
+ padding: 6px 10px;
|
|
|
+ text-align: left;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background: rgba(10, 132, 255, 0.15);
|
|
|
+ color: #e0e6ed;
|
|
|
+ font-weight: 600;
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ td {
|
|
|
+ color: #c9d1d9;
|
|
|
+ }
|
|
|
+
|
|
|
+ tr:hover td {
|
|
|
+ background: rgba(10, 132, 255, 0.08);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- audio {
|
|
|
+ .csv-preview {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ overflow: auto;
|
|
|
+
|
|
|
+ .csv-table {
|
|
|
+ border-collapse: collapse;
|
|
|
width: 100%;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ th,
|
|
|
+ td {
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.15);
|
|
|
+ padding: 6px 10px;
|
|
|
+ text-align: left;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background: rgba(10, 132, 255, 0.15);
|
|
|
+ color: #e0e6ed;
|
|
|
+ font-weight: 600;
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ td {
|
|
|
+ color: #c9d1d9;
|
|
|
+ }
|
|
|
+
|
|
|
+ tr:hover td {
|
|
|
+ background: rgba(10, 132, 255, 0.08);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .doc-preview {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ overflow: auto;
|
|
|
+
|
|
|
+ .doc-html-body {
|
|
|
+ color: #e0e6ed;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.8;
|
|
|
+
|
|
|
+ :deep(table) {
|
|
|
+ border-collapse: collapse;
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ th,
|
|
|
+ td {
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.15);
|
|
|
+ padding: 8px 12px;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background: rgba(10, 132, 255, 0.1);
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(img) {
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(p) {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .markdown-preview {
|
|
|
+ .markdown-body {
|
|
|
+ color: #e0e6ed;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.8;
|
|
|
+
|
|
|
+ :deep(h1),
|
|
|
+ :deep(h2),
|
|
|
+ :deep(h3),
|
|
|
+ :deep(h4),
|
|
|
+ :deep(h5),
|
|
|
+ :deep(h6) {
|
|
|
+ color: #fff;
|
|
|
+ margin-top: 16px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(h1) {
|
|
|
+ font-size: 24px;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(h2) {
|
|
|
+ font-size: 20px;
|
|
|
+ padding-bottom: 6px;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(h3) {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(p) {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(code) {
|
|
|
+ background: rgba(10, 132, 255, 0.15);
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-family: monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #79c0ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(pre) {
|
|
|
+ background: rgba(0, 0, 0, 0.3);
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow-x: auto;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ code {
|
|
|
+ background: none;
|
|
|
+ padding: 0;
|
|
|
+ color: #e0e6ed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(blockquote) {
|
|
|
+ border-left: 3px solid #3a8fd4;
|
|
|
+ padding-left: 12px;
|
|
|
+ margin: 12px 0;
|
|
|
+ color: #8b949e;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(ul),
|
|
|
+ :deep(ol) {
|
|
|
+ padding-left: 24px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ li {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(table) {
|
|
|
+ border-collapse: collapse;
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ th,
|
|
|
+ td {
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.15);
|
|
|
+ padding: 8px 12px;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background: rgba(10, 132, 255, 0.1);
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ tr:hover td {
|
|
|
+ background: rgba(10, 132, 255, 0.05);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(a) {
|
|
|
+ color: #3a8fd4;
|
|
|
+ text-decoration: none;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(img) {
|
|
|
+ max-width: 100%;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(hr) {
|
|
|
+ border: none;
|
|
|
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ margin: 16px 0;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|