2026-04-02 11:36:05 +08:00
|
|
|
|
<template>
|
2026-04-02 12:01:27 +08:00
|
|
|
|
<div class="reading-layout">
|
|
|
|
|
|
<!-- 左栏:文件管理树 -->
|
|
|
|
|
|
<div class="left-panel">
|
|
|
|
|
|
<div class="left-header">
|
|
|
|
|
|
<div class="left-title">文件管理</div>
|
|
|
|
|
|
<div class="left-actions">
|
|
|
|
|
|
<img src="@/assets/images/reading/create.png" alt="新建" title="新建文件夹" class="action-icon" @click="createFolder()">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="left-search">
|
|
|
|
|
|
<el-input v-model="searchWord" placeholder="搜索文件..." size="small" clearable>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<img src="@/assets/images/search.png" style="width:14px;height:14px;">
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
2026-04-02 12:01:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="left-tree">
|
|
|
|
|
|
<el-tree
|
|
|
|
|
|
ref="treeRef"
|
|
|
|
|
|
:data="filteredTreeData"
|
|
|
|
|
|
:props="treeProps"
|
|
|
|
|
|
node-key="id"
|
|
|
|
|
|
:expand-on-click-node="false"
|
|
|
|
|
|
:default-expand-all="false"
|
|
|
|
|
|
highlight-current
|
|
|
|
|
|
@node-click="handleNodeClick"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default="{ node, data }">
|
|
|
|
|
|
<div class="tree-node" :class="{ 'is-file': !data.isFolder, 'is-selected-file': selectedFile && selectedFile.fileId === data.id && !data.isFolder }">
|
|
|
|
|
|
<div class="tree-node-content">
|
|
|
|
|
|
<img v-if="data.isFolder" src="@/assets/images/reading/folder.png" class="tree-icon folder-icon">
|
|
|
|
|
|
<span v-else class="tree-file-icon">{{ getFileIcon(data.label) }}</span>
|
|
|
|
|
|
<span class="tree-label" :title="data.label">{{ data.label }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="tree-node-actions" v-if="data.isFolder" @click.stop>
|
|
|
|
|
|
<el-dropdown trigger="click" size="small">
|
|
|
|
|
|
<img src="@/assets/images/reading/operates.png" class="tree-operate-icon">
|
|
|
|
|
|
<template #dropdown>
|
|
|
|
|
|
<el-dropdown-menu>
|
|
|
|
|
|
<el-dropdown-item @click="uploadToFolder(data)">上传文件</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item @click="createFolder(data)">重命名</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item @click="delFolder(data)" style="color:#BE0000">删除</el-dropdown-item>
|
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="tree-node-actions" v-if="!data.isFolder" @click.stop>
|
|
|
|
|
|
<el-dropdown trigger="click" size="small">
|
|
|
|
|
|
<img src="@/assets/images/reading/operates.png" class="tree-operate-icon">
|
|
|
|
|
|
<template #dropdown>
|
|
|
|
|
|
<el-dropdown-menu>
|
|
|
|
|
|
<el-dropdown-item @click="renameFile(data)">重命名</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item @click="downLoadDoc(data)">下载</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item @click="delFile(data)" style="color:#BE0000">删除</el-dropdown-item>
|
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-tree>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="left-footer">
|
|
|
|
|
|
<div class="batch-del-btn" @click="delBatch">
|
2026-04-02 11:36:05 +08:00
|
|
|
|
<img src="@/assets/images/reading/del.png" alt="">
|
2026-04-02 12:01:27 +08:00
|
|
|
|
<span>批量删除</span>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 中间栏:文件预览 -->
|
|
|
|
|
|
<div class="center-panel">
|
|
|
|
|
|
<div v-if="!selectedFile" class="center-placeholder">
|
|
|
|
|
|
<div class="placeholder-content">
|
|
|
|
|
|
<img src="@/assets/images/reading/empty.png" alt="" style="width:80px;height:80px;opacity:0.5;">
|
|
|
|
|
|
<p>点击左侧文件查看内容</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="center-content">
|
|
|
|
|
|
<div class="center-header">
|
|
|
|
|
|
<span class="center-title" :title="selectedFile.fileName">{{ selectedFile.fileName }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="file-content" ref="fileContent" id="file-content">
|
|
|
|
|
|
<div class="view-md" id="file-html-content" v-html="docHtml"></div>
|
|
|
|
|
|
<div id="note-content" :title="noteContent" class="file-note"></div>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 右栏:ReadingBox -->
|
|
|
|
|
|
<div class="right-panel">
|
|
|
|
|
|
<div v-if="!selectedFile" class="right-placeholder">
|
|
|
|
|
|
<div class="placeholder-content">
|
|
|
|
|
|
<img src="@/assets/images/reading/empty.png" alt="" style="width:60px;height:60px;opacity:0.4;">
|
|
|
|
|
|
<p class="placeholder-title">知识库问答</p>
|
|
|
|
|
|
<p class="placeholder-sub">选择文件后开始提问</p>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-02 12:01:27 +08:00
|
|
|
|
<ReadingBox v-else ref="readingBox" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右键菜单 -->
|
|
|
|
|
|
<div class="click-bar-button" id="shortMenuDom" ref="shortMenuDom" v-show="shortMenuShow">
|
|
|
|
|
|
<template v-for="(item, index) in contentMenuList" :key="index">
|
|
|
|
|
|
<div class="menu-name" @click="fileContentMenu(index)">{{ item }}</div>
|
|
|
|
|
|
<div class="vertical-line" v-if="index < contentMenuList.length - 1"></div>
|
|
|
|
|
|
</template>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
<!-- 创建/重命名文件夹弹框 -->
|
|
|
|
|
|
<el-dialog v-model="folderDialogVisible" :title="folderDialogTitle" width="500"
|
|
|
|
|
|
:close-on-click-modal="false">
|
|
|
|
|
|
<el-input v-model="submitFolderParam.name" placeholder="请输入文件夹名称"
|
|
|
|
|
|
@keyup.enter="submitFolderName"></el-input>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="folderDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="submitFolderName">确定</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
<!-- 重命名文件弹框 -->
|
|
|
|
|
|
<el-dialog v-model="showRenameDialog" title="重命名" width="500" :close-on-click-modal="false">
|
|
|
|
|
|
<el-input v-model="renameDocName" placeholder="请输入文件名称" />
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showRenameDialog = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="saveDocName">保存</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
<!-- 上传文件弹框 -->
|
|
|
|
|
|
<el-dialog v-model="showUploadDialog" title="上传文件" width="715" @close="closeUpload()"
|
|
|
|
|
|
:close-on-click-modal="false">
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
class="upload-demo"
|
|
|
|
|
|
action="#"
|
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
|
accept=".pdf,.docx,.txt,.xlsx,.xls"
|
|
|
|
|
|
name="files"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
|
:on-change="handleFileChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon :size="100" class="el-icon--upload" style="display: flex;flex-wrap: wrap">
|
|
|
|
|
|
<upload-filled style="color: #004EA0"/>
|
|
|
|
|
|
<span style="margin-top:-50px;font-size:13px; font-style: normal;color:#004EA0">选择文件</span>
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
<template #tip>
|
|
|
|
|
|
<div class="el-upload__tip">
|
|
|
|
|
|
支持TXT/DOCX/PDF/XLSX/XLS格式文件,文件大小不超过10M
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<div class="files-upload-result">
|
|
|
|
|
|
<div class="result" v-for="item in uploadingFileList" :key="item.uid">
|
|
|
|
|
|
<div class="file-name" :title="item.name">{{ item.name }}</div>
|
|
|
|
|
|
<div class="result-info">
|
|
|
|
|
|
<div class="fail-msg" v-if="item.status === 'fail'" :title="item.message">{{ item.message }}</div>
|
|
|
|
|
|
<img class="upload-remove" v-if="item.status === 'ready'" src="@/assets/images/reading/upload_remove.png"
|
|
|
|
|
|
alt="" @click="handleRemove(item)">
|
|
|
|
|
|
<div class="loading" v-if="item.status === 'uploading'">
|
|
|
|
|
|
<Loading></Loading>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<img v-if="item.status === 'success'" src="@/assets/images/reading/upload_success.png" alt="">
|
|
|
|
|
|
<img v-if="item.status === 'fail'" src="@/assets/images/reading/upload_fail.png" alt="">
|
|
|
|
|
|
</div>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-02 12:01:27 +08:00
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div style="text-align: center" v-if="uploadingFileList.length > 0">
|
|
|
|
|
|
<el-button @click="closeUpload()">关闭</el-button>
|
|
|
|
|
|
<div class="dialog-tips">
|
|
|
|
|
|
<img src="@/assets/images/reading/tips.png" alt="">
|
|
|
|
|
|
<span>关闭弹窗后文件将继续上传</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 添加笔记弹框 -->
|
|
|
|
|
|
<el-dialog v-model="showAddNoteDialog" title="写下你的想法吧" width="715"
|
|
|
|
|
|
:close-on-click-modal="false" @open="openAddNoteDialog">
|
|
|
|
|
|
<el-input ref="noteMsgRef" v-model="noteMsg" type="textarea" :rows="10" placeholder="请输入笔记内容"/>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div style="text-align: center;">
|
|
|
|
|
|
<el-button type="primary" @click="submitNote()">发表</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-04-02 11:36:05 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-02 12:01:27 +08:00
|
|
|
|
import {onMounted, onUnmounted, ref, reactive, provide, nextTick, computed, watch} from "vue";
|
|
|
|
|
|
import {
|
|
|
|
|
|
getKnowledgeBaseList, addKnowledgeBase, editKnowledgeBase, delKnowledgeBase,
|
|
|
|
|
|
getKnowledgeBaseContent, uploadFile, editFile, delFile, delFiles,
|
|
|
|
|
|
downloadFile, getFileContent, addFileNote, getSize
|
|
|
|
|
|
} from "@/api";
|
|
|
|
|
|
import {withLoading} from "@/utils/loading";
|
|
|
|
|
|
import {copyToClip, getGlobalSelectionPosition} from "@/utils";
|
|
|
|
|
|
import {transforMd} from "@/utils/markdown";
|
|
|
|
|
|
import ReadingBox from "@/components/ReadingBox.vue";
|
|
|
|
|
|
import Loading from "@/components/Loading.vue";
|
|
|
|
|
|
import {UploadFilled} from '@element-plus/icons-vue';
|
|
|
|
|
|
import {ElMessage, ElMessageBox, type UploadFile, type UploadFiles} from "element-plus";
|
|
|
|
|
|
import Mark from "mark.js";
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ===================== 文件树数据 =====================
|
|
|
|
|
|
const treeRef = ref(null);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
const searchWord = ref('');
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const treeData = ref<any[]>([]);
|
|
|
|
|
|
const treeProps = { children: 'children', label: 'label' };
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ===================== 选中文件 =====================
|
|
|
|
|
|
const selectedFile = ref<any>(null);
|
|
|
|
|
|
provide('selectedFile', selectedFile);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ===================== 文件预览 =====================
|
|
|
|
|
|
const docHtml = ref('');
|
|
|
|
|
|
const fileContent = ref(null);
|
|
|
|
|
|
const readingBox = ref(null);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ===================== 笔记 =====================
|
|
|
|
|
|
const fileNote = reactive({ notes: [] as any[] });
|
|
|
|
|
|
const noteContent = ref('');
|
|
|
|
|
|
provide('fileNote', fileNote);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ===================== 右键菜单 =====================
|
|
|
|
|
|
const shortMenuDom = ref(null);
|
|
|
|
|
|
const shortMenuShow = ref(false);
|
|
|
|
|
|
const selectText = ref('');
|
|
|
|
|
|
const contentMenuList = ['引用', '复制', '名词解释', '添加笔记', '翻译'];
|
|
|
|
|
|
|
|
|
|
|
|
// ===================== 引用 (provide to ReadingBox) =====================
|
|
|
|
|
|
const quoteMsg = ref<string>('');
|
|
|
|
|
|
provide('quoteMsg', quoteMsg);
|
|
|
|
|
|
provide('updateQuoteMsg', (newValue: string) => { quoteMsg.value = newValue; });
|
|
|
|
|
|
|
|
|
|
|
|
// ===================== 笔记弹框 =====================
|
|
|
|
|
|
const showAddNoteDialog = ref(false);
|
|
|
|
|
|
const noteMsg = ref('');
|
|
|
|
|
|
const noteMsgRef = ref(null);
|
|
|
|
|
|
provide('noteMsg', noteMsg);
|
|
|
|
|
|
provide('showAddNoteDialog', showAddNoteDialog);
|
|
|
|
|
|
|
|
|
|
|
|
interface NoteParam {
|
|
|
|
|
|
id: string; fileId: number; pstartId: string; pendId: string;
|
|
|
|
|
|
wordContent: string; startIndex: number; endIndex: number; noteContent: string;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
interface PositionInfo {
|
|
|
|
|
|
start: number; end: number; parentStart: string | null; parentEnd: string | null;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const noteParam = ref<NoteParam>({
|
|
|
|
|
|
id: '', fileId: 0, pstartId: '', pendId: '',
|
|
|
|
|
|
wordContent: '', startIndex: 0, endIndex: 0, noteContent: ''
|
|
|
|
|
|
});
|
|
|
|
|
|
provide('noteParam', noteParam);
|
|
|
|
|
|
const positionInfo = ref<PositionInfo>({ start: -1, end: -1, parentStart: '', parentEnd: '' });
|
|
|
|
|
|
const activeNoteIndex = ref(-1);
|
|
|
|
|
|
provide('activeNoteIndex', activeNoteIndex);
|
|
|
|
|
|
provide('highlightContentFlag', (note: any, newIndex: number) => {
|
|
|
|
|
|
activeNoteIndex.value = newIndex;
|
|
|
|
|
|
showFileNote(note, true);
|
|
|
|
|
|
});
|
|
|
|
|
|
const showFileNoteEvent = ref('');
|
|
|
|
|
|
|
|
|
|
|
|
// ===================== 文件夹弹框 =====================
|
|
|
|
|
|
const folderDialogVisible = ref(false);
|
|
|
|
|
|
const folderDialogTitle = ref("新建文件夹");
|
|
|
|
|
|
const submitFolderParam = reactive({ id: 0, name: "" });
|
|
|
|
|
|
|
|
|
|
|
|
// ===================== 文件重命名弹框 =====================
|
|
|
|
|
|
const showRenameDialog = ref(false);
|
|
|
|
|
|
const renameDocName = ref('');
|
|
|
|
|
|
const renameDocType = ref('');
|
|
|
|
|
|
const renameDocId = ref(0);
|
|
|
|
|
|
|
|
|
|
|
|
// ===================== 上传文件弹框 =====================
|
|
|
|
|
|
const showUploadDialog = ref(false);
|
|
|
|
|
|
const uploadFolderId = ref(0);
|
|
|
|
|
|
interface CusUploadFile extends UploadFile { message?: string | null }
|
|
|
|
|
|
const uploadingFileList = ref([] as CusUploadFile[]);
|
|
|
|
|
|
const isUploading = ref(false);
|
|
|
|
|
|
const uploadFinishFlag = ref(true);
|
|
|
|
|
|
const currentIndex = ref(0);
|
|
|
|
|
|
const totalFileSize = ref<number>(0);
|
|
|
|
|
|
const allowFileTypes = ["TXT", "DOCX", "PDF", "XLSX", "XLS"];
|
|
|
|
|
|
const FILE_MAX_SIZE = 10 * 1024 * 1024;
|
|
|
|
|
|
const TOTAL_FILE_MAX_SIZE = 50 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 文件树:加载、过滤
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const loadTreeData = async () => {
|
2026-04-02 11:36:05 +08:00
|
|
|
|
try {
|
2026-04-02 12:01:27 +08:00
|
|
|
|
let res = await getKnowledgeBaseList<any>({ folderType: "0" });
|
|
|
|
|
|
if (res && res.data) {
|
|
|
|
|
|
const folders = res.data.map((f: any) => ({
|
|
|
|
|
|
id: f.id, label: f.name, isFolder: true, children: [], raw: f, loaded: false
|
|
|
|
|
|
}));
|
|
|
|
|
|
treeData.value = folders;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
2026-04-02 12:01:27 +08:00
|
|
|
|
ElMessage.error(error?.message || '加载文件夹失败');
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const loadFolderFiles = async (folder: any) => {
|
|
|
|
|
|
if (folder.loaded) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await getKnowledgeBaseContent<any>({
|
|
|
|
|
|
folderType: "1", knowledgeBaseId: folder.id + "", patentId: "0"
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res && res.data) {
|
|
|
|
|
|
folder.children = res.data.map((f: any) => ({
|
|
|
|
|
|
id: f.id, label: f.filename, isFolder: false, isLeaf: true, raw: f,
|
|
|
|
|
|
folderId: folder.id, folderName: folder.label
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
folder.loaded = true;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
ElMessage.error(error?.message || '加载文件列表失败');
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const filteredTreeData = computed(() => {
|
|
|
|
|
|
if (!searchWord.value.trim()) return treeData.value;
|
|
|
|
|
|
const keyword = searchWord.value.toLowerCase();
|
|
|
|
|
|
return treeData.value.filter((folder: any) => {
|
|
|
|
|
|
const folderMatch = folder.label.toLowerCase().includes(keyword);
|
|
|
|
|
|
const childMatch = folder.children?.some((f: any) => f.label.toLowerCase().includes(keyword));
|
|
|
|
|
|
return folderMatch || childMatch;
|
|
|
|
|
|
}).map((folder: any) => {
|
|
|
|
|
|
if (!searchWord.value.trim()) return folder;
|
|
|
|
|
|
return {
|
|
|
|
|
|
...folder,
|
|
|
|
|
|
children: folder.children?.filter((f: any) =>
|
|
|
|
|
|
f.label.toLowerCase().includes(keyword) || folder.label.toLowerCase().includes(keyword)
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const handleNodeClick = async (data: any) => {
|
|
|
|
|
|
if (data.isFolder) {
|
|
|
|
|
|
await loadFolderFiles(data);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// 点击文件
|
|
|
|
|
|
const doc = data.raw;
|
|
|
|
|
|
selectedFile.value = {
|
|
|
|
|
|
fileId: doc.id,
|
|
|
|
|
|
fileName: doc.filename,
|
|
|
|
|
|
embeddingId: doc.embeddingId,
|
|
|
|
|
|
folderId: data.folderId,
|
|
|
|
|
|
folderName: data.folderName,
|
|
|
|
|
|
articleAbstract: doc.articleAbstract || '暂无内容,请重试',
|
|
|
|
|
|
articleKeywords: doc.articleKeywords || '暂无内容,请重试',
|
|
|
|
|
|
articleParagraph: doc.articleParagraph || '暂无内容,请重试',
|
|
|
|
|
|
fullContent: doc.context
|
|
|
|
|
|
};
|
|
|
|
|
|
await loadFileContent();
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const getFileIcon = (filename: string) => {
|
|
|
|
|
|
const ext = filename?.split('.').pop()?.toLowerCase() || '';
|
|
|
|
|
|
const icons: Record<string, string> = {
|
|
|
|
|
|
'pdf': '📄', 'docx': '📝', 'doc': '📝', 'txt': '📃',
|
|
|
|
|
|
'xlsx': '📊', 'xls': '📊', 'md': '📋'
|
|
|
|
|
|
};
|
|
|
|
|
|
return icons[ext] || '📄';
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// 文件预览:加载内容、笔记、交互
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const pattern = /(\$\$.*?\$\$|\$.*?\$)/g;
|
|
|
|
|
|
|
|
|
|
|
|
const loadFileContent = async () => {
|
|
|
|
|
|
if (!selectedFile.value) return;
|
|
|
|
|
|
let offsets: number[] = [];
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await withLoading(getFileContent)({
|
|
|
|
|
|
fileId: selectedFile.value.fileId,
|
|
|
|
|
|
embeddingId: selectedFile.value.embeddingId,
|
|
|
|
|
|
knowledgeBaseId: selectedFile.value.folderId
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res && res.code === 200 && res.data && res.data.content) {
|
|
|
|
|
|
let content = res.data.content;
|
|
|
|
|
|
content = content.replace(pattern, (match: string, _cg: string, offset: number) => {
|
|
|
|
|
|
offsets.push(offset);
|
|
|
|
|
|
return transforMd(match);
|
|
|
|
|
|
});
|
|
|
|
|
|
docHtml.value = content.replace(/<p>(.*?<span class="katex">.*?<\/span>.*?)<\/p>/g, '$1');
|
|
|
|
|
|
fileNote.notes = res.data.notes || [];
|
2026-04-02 11:36:05 +08:00
|
|
|
|
} else {
|
2026-04-02 12:01:27 +08:00
|
|
|
|
docHtml.value = '<p style="color:#999;text-align:center;margin-top:40px;">文件内容获取失败</p>';
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
docHtml.value = '<p style="color:#999;text-align:center;margin-top:40px;">文件内容获取失败</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
try {
|
|
|
|
|
|
handelLaTeXElements(offsets);
|
|
|
|
|
|
handelCommentElements();
|
|
|
|
|
|
bindFileContentEvents();
|
|
|
|
|
|
handelNoteFlagMouseEvent();
|
|
|
|
|
|
} catch (err) { /* ignore */ }
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
provide('updateFileContent', async () => {
|
|
|
|
|
|
await loadFileContent();
|
|
|
|
|
|
});
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const handelLaTeXElements = (offsets: number[]) => {
|
|
|
|
|
|
let katexElements = document.querySelectorAll('span[class="katex"]');
|
|
|
|
|
|
for (let i = 0; i < katexElements.length; i++) {
|
|
|
|
|
|
katexElements[i].setAttribute('id', 'katex-' + offsets[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const handelCommentElements = () => {
|
|
|
|
|
|
document.querySelectorAll('a[href^="#comment-"]').forEach(element => {
|
|
|
|
|
|
let targetId = element.getAttribute('href');
|
|
|
|
|
|
(element as HTMLElement).onclick = function (e) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const targetElement = document.querySelector(targetId!);
|
|
|
|
|
|
if (targetElement) targetElement.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 右键菜单
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const fileContentMenu = (index: number) => {
|
|
|
|
|
|
disShowShortMenu();
|
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
|
quoteMsg.value = selectText.value;
|
|
|
|
|
|
if (quoteMsg.value && quoteMsg.value.length > 1000) quoteMsg.value = quoteMsg.value.slice(0, 1000);
|
|
|
|
|
|
readingBox.value?.changeHead(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (index === 1) {
|
|
|
|
|
|
copyToClip(selectText.value);
|
|
|
|
|
|
ElMessage.success("复制成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (index === 2) {
|
|
|
|
|
|
let sendText = "我想请你解释一下这个词语的含义:" + selectText.value;
|
|
|
|
|
|
readingBox.value?.changeHead(2);
|
|
|
|
|
|
readingBox.value?.readingBoxSend(sendText, '1');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (index === 3) {
|
|
|
|
|
|
readingBox.value?.changeHead(3);
|
|
|
|
|
|
showAddNoteDialog.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (index === 4) {
|
|
|
|
|
|
readingBox.value?.changeHead(4);
|
|
|
|
|
|
readingBox.value?.translateQuery(selectText.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const disShowShortMenu = () => { shortMenuShow.value = false; };
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const bindFileContentEvents = () => {
|
|
|
|
|
|
if (!fileContent.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
fileContent.value.addEventListener('mouseup', function (event: MouseEvent) {
|
|
|
|
|
|
activeNoteIndex.value = -1;
|
|
|
|
|
|
displayFileNote();
|
|
|
|
|
|
removeHighlightSelectedWord();
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const selection = window.getSelection();
|
|
|
|
|
|
if (!selection) return;
|
|
|
|
|
|
selectText.value = selection.toString();
|
|
|
|
|
|
let position = getGlobalSelectionPosition(selection);
|
|
|
|
|
|
positionInfo.value = position;
|
|
|
|
|
|
if (selectText.value && shortMenuDom.value && fileContent.value?.contains(selection.anchorNode)) {
|
|
|
|
|
|
let offset = getViewportOffsetByEvent(event);
|
|
|
|
|
|
shortMenuShow.value = true;
|
|
|
|
|
|
(shortMenuDom.value as HTMLElement).style.left = offset.left + 'px';
|
|
|
|
|
|
(shortMenuDom.value as HTMLElement).style.top = offset.top + 'px';
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
fileContent.value.addEventListener('scroll', () => {
|
|
|
|
|
|
let noteElement = document.getElementById('note-' + (activeNoteIndex.value + 1));
|
|
|
|
|
|
let noteContentElement = document.getElementById('note-content');
|
|
|
|
|
|
if (noteElement && noteContentElement) {
|
|
|
|
|
|
const rectNoteElement = noteElement.getBoundingClientRect();
|
|
|
|
|
|
let top = rectNoteElement.top - rectNoteElement.height - 8;
|
|
|
|
|
|
noteContentElement.style.top = top + 'px';
|
|
|
|
|
|
if (top < 75) noteContentElement.style.display = 'none';
|
|
|
|
|
|
else if (activeNoteIndex.value != -1) noteContentElement.style.display = 'block';
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 笔记功能
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const openAddNoteDialog = async () => {
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
noteMsgRef.value?.focus();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const submitNote = async () => {
|
|
|
|
|
|
if (noteParam.value.id) {
|
|
|
|
|
|
noteParam.value.noteContent = noteMsg.value;
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await withLoading(addFileNote)(noteParam.value);
|
|
|
|
|
|
if (res.code == 200) {
|
|
|
|
|
|
ElMessage.success('笔记修改成功');
|
|
|
|
|
|
showAddNoteDialog.value = false;
|
|
|
|
|
|
noteMsg.value = '';
|
|
|
|
|
|
await loadFileContent();
|
|
|
|
|
|
} else ElMessage.error('笔记修改失败');
|
|
|
|
|
|
} catch (error: any) { ElMessage.error(error?.message || '未知错误'); }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
noteParam.value.fileId = selectedFile.value?.fileId;
|
|
|
|
|
|
noteParam.value.pstartId = positionInfo.value.parentStart || '';
|
|
|
|
|
|
noteParam.value.pendId = positionInfo.value.parentEnd || '';
|
|
|
|
|
|
noteParam.value.wordContent = selectText.value;
|
|
|
|
|
|
noteParam.value.startIndex = positionInfo.value.start;
|
|
|
|
|
|
noteParam.value.endIndex = positionInfo.value.end;
|
|
|
|
|
|
noteParam.value.noteContent = noteMsg.value;
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await withLoading(addFileNote)(noteParam.value);
|
|
|
|
|
|
if (res.code == 200) {
|
|
|
|
|
|
ElMessage.success('笔记添加成功');
|
|
|
|
|
|
showAddNoteDialog.value = false;
|
|
|
|
|
|
noteMsg.value = '';
|
|
|
|
|
|
await loadFileContent();
|
|
|
|
|
|
} else ElMessage.error('笔记添加失败');
|
|
|
|
|
|
} catch (error: any) { ElMessage.error(error?.message || '未知错误'); }
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const displayFileNote = () => {
|
|
|
|
|
|
const noteElements = document.querySelectorAll('span[id^="note-"]');
|
|
|
|
|
|
noteElements.forEach((element) => { element.classList.remove('note-flag-active'); });
|
|
|
|
|
|
let noteContentElement = document.getElementById('note-content');
|
|
|
|
|
|
if (noteContentElement) noteContentElement.style.display = 'none';
|
|
|
|
|
|
const previousHighlights = document.querySelectorAll('.highlight');
|
|
|
|
|
|
previousHighlights.forEach(highlight => {
|
|
|
|
|
|
const parent = highlight.parentNode!;
|
|
|
|
|
|
const textNode = document.createTextNode(highlight.textContent || '');
|
|
|
|
|
|
parent.replaceChild(textNode, highlight);
|
|
|
|
|
|
parent.normalize();
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const showFileNote = (note: any, isPosition: boolean = false) => {
|
|
|
|
|
|
displayFileNote();
|
|
|
|
|
|
if (!note) return;
|
|
|
|
|
|
let noteElement = document.getElementById('note-' + (activeNoteIndex.value + 1));
|
|
|
|
|
|
let noteContentEl = document.getElementById('note-content');
|
|
|
|
|
|
let fileContentEl = document.getElementById("file-content");
|
|
|
|
|
|
if (!fileContentEl) return;
|
|
|
|
|
|
const rectFileContent = fileContentEl.getBoundingClientRect();
|
|
|
|
|
|
if (noteElement) {
|
|
|
|
|
|
noteElement.classList.add('note-flag-active');
|
|
|
|
|
|
highlightSelectedWord(note);
|
|
|
|
|
|
if (isPosition) {
|
|
|
|
|
|
let selectedElements = document.querySelectorAll('mark[class^="highlight"]');
|
|
|
|
|
|
if (selectedElements.length > 0) selectedElements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
|
|
}
|
|
|
|
|
|
noteContent.value = note.noteContent;
|
|
|
|
|
|
const rectNoteElement = noteElement.getBoundingClientRect();
|
|
|
|
|
|
if (noteContentEl) {
|
|
|
|
|
|
noteContentEl.innerHTML = noteContent.value;
|
|
|
|
|
|
noteContentEl.style.display = 'block';
|
|
|
|
|
|
let rectNoteContentElement = noteContentEl.getBoundingClientRect();
|
|
|
|
|
|
let left = rectNoteElement.left - 10;
|
|
|
|
|
|
if (left + rectNoteContentElement.width > rectFileContent.width) left = rectFileContent.width - rectNoteContentElement.width;
|
|
|
|
|
|
noteContentEl.style.left = left + 'px';
|
|
|
|
|
|
noteContentEl.style.top = rectNoteElement.top - rectNoteElement.height - 8 + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const highlightSelectedWord = (note: any) => {
|
|
|
|
|
|
if (!note) return;
|
|
|
|
|
|
let marked = new Mark('#file-html-content');
|
|
|
|
|
|
marked.markRanges([{ start: note.startIndex, length: note.endIndex - note.startIndex }], {
|
|
|
|
|
|
className: 'highlight', exclude: ['.note-flag']
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeHighlightSelectedWord = () => {
|
|
|
|
|
|
const previousHighlights = document.querySelectorAll('.highlight');
|
|
|
|
|
|
previousHighlights.forEach(highlight => {
|
|
|
|
|
|
const parent = highlight.parentNode!;
|
|
|
|
|
|
const textNode = document.createTextNode(highlight.textContent || '');
|
|
|
|
|
|
parent.replaceChild(textNode, highlight);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handelNoteFlagMouseEvent = () => {
|
|
|
|
|
|
const noteElements = document.querySelectorAll('span[id^="note-"]');
|
|
|
|
|
|
noteElements.forEach((element) => {
|
|
|
|
|
|
element.addEventListener('mouseover', () => {
|
|
|
|
|
|
showFileNoteEvent.value = 'mouseover';
|
|
|
|
|
|
let elementId = Number(element.id.replace('note-', ''));
|
|
|
|
|
|
for (let i = 0; i < fileNote.notes.length; i++) {
|
|
|
|
|
|
if (i + 1 == elementId) { activeNoteIndex.value = i; showFileNote(fileNote.notes[i]); break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
element.addEventListener('mouseleave', () => {
|
|
|
|
|
|
if (showFileNoteEvent.value === 'mouseover') { activeNoteIndex.value = -1; displayFileNote(); }
|
|
|
|
|
|
});
|
|
|
|
|
|
element.addEventListener('click', async (e) => {
|
|
|
|
|
|
showFileNoteEvent.value = 'click';
|
|
|
|
|
|
let elementId = Number(element.id.replace('note-', ''));
|
|
|
|
|
|
for (let i = 0; i < fileNote.notes.length; i++) {
|
|
|
|
|
|
if (i + 1 == elementId) { activeNoteIndex.value = i; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
readingBox.value?.changeHead(3);
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
let rightNoteElement = document.getElementById('right-note-' + elementId);
|
|
|
|
|
|
if (rightNoteElement) rightNoteElement.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
|
showFileNote(fileNote.notes[activeNoteIndex.value]);
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 文件夹操作
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const createFolder = (item: any = null) => {
|
|
|
|
|
|
if (item) {
|
|
|
|
|
|
submitFolderParam.id = item.id;
|
|
|
|
|
|
submitFolderParam.name = item.label || item.name;
|
|
|
|
|
|
folderDialogTitle.value = "重命名文件夹";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
submitFolderParam.id = 0;
|
|
|
|
|
|
submitFolderParam.name = "";
|
|
|
|
|
|
folderDialogTitle.value = "新建文件夹";
|
|
|
|
|
|
}
|
|
|
|
|
|
folderDialogVisible.value = true;
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
|
|
|
|
|
const submitFolderName = async () => {
|
|
|
|
|
|
if (submitFolderParam.name.trim().length === 0) {
|
|
|
|
|
|
ElMessage.warning("请输入文件夹名称");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
2026-04-02 12:01:27 +08:00
|
|
|
|
if (submitFolderParam.id !== 0) {
|
|
|
|
|
|
let res = await withLoading(editKnowledgeBase<any>)({
|
|
|
|
|
|
id: submitFolderParam.id + "", name: submitFolderParam.name.trim(), folderType: "0"
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res?.code === 200) ElMessage.success("重命名成功");
|
|
|
|
|
|
else ElMessage.error(res.msg);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
} else {
|
2026-04-02 12:01:27 +08:00
|
|
|
|
let res = await withLoading(addKnowledgeBase<any>)({ name: submitFolderParam.name.trim() });
|
|
|
|
|
|
if (res?.code === 200) ElMessage.success("文件夹创建成功");
|
|
|
|
|
|
else ElMessage.error(res.msg);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
folderDialogVisible.value = false;
|
|
|
|
|
|
await loadTreeData();
|
|
|
|
|
|
} catch (error: any) { ElMessage.error(error?.message || '未知错误'); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const delFolder = async (item: any) => {
|
|
|
|
|
|
ElMessageBox.confirm("确定删除该文件夹?", "提示", {
|
|
|
|
|
|
confirmButtonText: "确定", cancelButtonText: "取消", type: "warning",
|
|
|
|
|
|
}).then(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await withLoading(delKnowledgeBase<any>)({ knowledgeBaseId: item.id + "", folderType: "0" });
|
|
|
|
|
|
if (res?.code === 200) { ElMessage.success("删除成功"); await loadTreeData(); }
|
|
|
|
|
|
else ElMessage.error(res.msg);
|
|
|
|
|
|
} catch (error: any) { ElMessage.error(error?.message || '未知错误'); }
|
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const delBatch = () => {
|
|
|
|
|
|
ElMessage.info("请使用文件夹或文件旁的操作菜单进行删除");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 文件操作
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const renameFile = (data: any) => {
|
|
|
|
|
|
const filename = data.label || data.raw?.filename;
|
|
|
|
|
|
if (filename) {
|
|
|
|
|
|
let names = filename.split('.');
|
|
|
|
|
|
renameDocName.value = names[0];
|
|
|
|
|
|
renameDocType.value = names.length >= 2 ? names[names.length - 1] : '';
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
renameDocId.value = data.id;
|
|
|
|
|
|
showRenameDialog.value = true;
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const saveDocName = async () => {
|
|
|
|
|
|
if (!renameDocName.value.trim()) { ElMessage.error("请输入文件名"); return; }
|
|
|
|
|
|
let param = new FormData();
|
|
|
|
|
|
param.append('fileId', renameDocId.value + '');
|
|
|
|
|
|
param.append('fileName', renameDocName.value + (renameDocType.value ? '.' + renameDocType.value : ''));
|
|
|
|
|
|
param.append('folderId', '0');
|
2026-04-02 11:36:05 +08:00
|
|
|
|
try {
|
2026-04-02 12:01:27 +08:00
|
|
|
|
await withLoading(editFile)(param);
|
|
|
|
|
|
ElMessage.success("修改成功");
|
|
|
|
|
|
showRenameDialog.value = false;
|
|
|
|
|
|
await refreshCurrentFolder();
|
|
|
|
|
|
} catch { ElMessage.error("修改失败"); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const downLoadDoc = async (data: any) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await downloadFile({ fileId: data.id });
|
|
|
|
|
|
const url = URL.createObjectURL(new Blob([response]));
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
link.setAttribute('download', data.label || data.raw?.filename);
|
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
} catch (error: any) { ElMessage.error(error?.message || '下载失败'); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const delFile = (data: any) => {
|
|
|
|
|
|
ElMessageBox.confirm("确认删除文档吗?", "提示", {
|
|
|
|
|
|
confirmButtonText: "确定", cancelButtonText: "取消", type: "warning",
|
|
|
|
|
|
}).then(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await withLoading(delFiles)({ fileList: [data.id], folderList: [] });
|
|
|
|
|
|
if (res?.code === 200) {
|
|
|
|
|
|
ElMessage.success("删除成功");
|
|
|
|
|
|
if (selectedFile.value?.fileId === data.id) { selectedFile.value = null; docHtml.value = ''; }
|
|
|
|
|
|
await refreshCurrentFolder();
|
|
|
|
|
|
} else ElMessage.error("删除失败");
|
|
|
|
|
|
} catch { ElMessage.error("删除失败"); }
|
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const refreshCurrentFolder = async () => {
|
|
|
|
|
|
// 重新加载所有已展开的文件夹
|
|
|
|
|
|
for (let folder of treeData.value) {
|
|
|
|
|
|
if (folder.loaded) {
|
|
|
|
|
|
folder.loaded = false;
|
|
|
|
|
|
await loadFolderFiles(folder);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// 上传文件
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const uploadToFolder = async (data: any) => {
|
|
|
|
|
|
uploadFolderId.value = data.id;
|
|
|
|
|
|
await getUploadedSize();
|
|
|
|
|
|
showUploadDialog.value = true;
|
|
|
|
|
|
if (uploadFinishFlag.value) uploadingFileList.value = [];
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const getUploadedSize = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await getSize();
|
|
|
|
|
|
if (res.code === 200) totalFileSize.value = res.data;
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const handleFileChange = async (file: UploadFile) => {
|
|
|
|
|
|
const fileExtension = file.name.split('.').pop()?.toUpperCase();
|
|
|
|
|
|
if (!allowFileTypes.includes(fileExtension as string)) {
|
|
|
|
|
|
ElMessage.warning(file.name + '文件格式不匹配');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (file.size && file.size > FILE_MAX_SIZE) {
|
|
|
|
|
|
ElMessage.warning(file.name + '文件大小已超过10M');
|
|
|
|
|
|
return;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
totalFileSize.value += file.size || 0;
|
|
|
|
|
|
if (totalFileSize.value > TOTAL_FILE_MAX_SIZE) {
|
|
|
|
|
|
totalFileSize.value -= file.size || 0;
|
|
|
|
|
|
ElMessage.warning('个人素材总量不能超过50M');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uploadingFileList.value.push(file as CusUploadFile);
|
2026-04-02 11:36:05 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
watch(uploadingFileList, newVal => {
|
|
|
|
|
|
if (!isUploading.value) {
|
|
|
|
|
|
for (let i = 0; i < newVal.length; i++) {
|
|
|
|
|
|
if (newVal[i].status === 'ready') {
|
|
|
|
|
|
isUploading.value = true;
|
|
|
|
|
|
newVal[i].status = 'uploading';
|
|
|
|
|
|
startUpload();
|
|
|
|
|
|
break;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, { immediate: true, deep: true });
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const handleRemove = (file: UploadFile) => {
|
|
|
|
|
|
uploadingFileList.value = uploadingFileList.value.filter(item => item.uid !== file.uid);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const startUpload = async () => { await uploadNextFile(); };
|
|
|
|
|
|
|
|
|
|
|
|
const uploadNextFile = async () => {
|
|
|
|
|
|
uploadFinishFlag.value = true;
|
|
|
|
|
|
for (let i = 0; i < uploadingFileList.value.length; i++) {
|
|
|
|
|
|
if (uploadingFileList.value[i].status !== 'success' && uploadingFileList.value[i].status !== 'fail') {
|
|
|
|
|
|
uploadFinishFlag.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (uploadFinishFlag.value) { isUploading.value = false; return; }
|
|
|
|
|
|
|
|
|
|
|
|
const file = uploadingFileList.value[currentIndex.value];
|
|
|
|
|
|
file.status = 'uploading';
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('files', file.raw as Blob);
|
|
|
|
|
|
formData.append('folderId', '0');
|
|
|
|
|
|
formData.append('knowledgeBaseId', uploadFolderId.value + '');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
let res = await uploadFile(formData);
|
|
|
|
|
|
if (res.code && res.code != 200) {
|
|
|
|
|
|
file.status = 'fail';
|
|
|
|
|
|
(file as CusUploadFile).message = res.msg;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
file.status = 'success';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
file.status = 'fail';
|
|
|
|
|
|
(file as CusUploadFile).message = error?.message || '未知错误';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
currentIndex.value++;
|
|
|
|
|
|
await uploadNextFile();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
const closeUpload = () => {
|
|
|
|
|
|
showUploadDialog.value = false;
|
|
|
|
|
|
currentIndex.value = 0;
|
|
|
|
|
|
refreshCurrentFolder();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 工具函数
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
const getViewportOffsetByEvent = (evt: MouseEvent) => {
|
|
|
|
|
|
return { left: evt.clientX, top: evt.clientY };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 生命周期
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadTreeData();
|
|
|
|
|
|
// 自动展开所有文件夹的文件
|
|
|
|
|
|
for (let folder of treeData.value) {
|
|
|
|
|
|
await loadFolderFiles(folder);
|
|
|
|
|
|
}
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
document.addEventListener('mousedown', (event: MouseEvent) => {
|
|
|
|
|
|
let execlute = document.getElementById('shortMenuDom');
|
|
|
|
|
|
if (execlute && execlute.contains(event.target as Node)) { /* noop */ }
|
|
|
|
|
|
else disShowShortMenu();
|
|
|
|
|
|
});
|
2026-04-02 11:36:05 +08:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.reading-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
height: 100%;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
width: 100%;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ========== 左栏 ==========
|
|
|
|
|
|
.left-panel {
|
|
|
|
|
|
width: 280px;
|
|
|
|
|
|
min-width: 280px;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-right: 1px solid #E6EDFF;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
|
|
.left-header {
|
2026-04-02 11:36:05 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
padding: 16px 16px 8px 16px;
|
|
|
|
|
|
.left-title {
|
2026-04-02 11:36:05 +08:00
|
|
|
|
font-weight: bold;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-icon {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
&:hover { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.left-search {
|
|
|
|
|
|
padding: 0 12px 8px 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.left-tree {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
|
&::-webkit-scrollbar-thumb { background: #D5DDFF; border-radius: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-tree-node__content) {
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
padding: 2px 0;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.tree-node {
|
2026-04-02 11:36:05 +08:00
|
|
|
|
display: flex;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
align-items: center;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
justify-content: space-between;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: #F0F4FF;
|
|
|
|
|
|
.tree-node-actions { opacity: 1; }
|
|
|
|
|
|
}
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
&.is-selected-file {
|
|
|
|
|
|
background: #E0EAFF;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.tree-node-content {
|
2026-04-02 11:36:05 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.folder-icon {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
|
}
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.tree-file-icon {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin-right: 6px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
.tree-label {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
cursor: pointer;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.tree-node-actions {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
.tree-operate-icon {
|
|
|
|
|
|
width: 12px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.left-footer {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-top: 1px solid #E6EDFF;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.batch-del-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border: 1px solid #004EA0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: #004EA0;
|
|
|
|
|
|
font-size: 13px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
img { width: 14px; margin-right: 4px; }
|
|
|
|
|
|
&:hover { background: #F0F4FF; }
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 中间栏 ==========
|
|
|
|
|
|
.center-panel {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.center-placeholder {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder-content {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
p { margin-top: 12px; font-size: 14px; }
|
|
|
|
|
|
}
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.center-content {
|
|
|
|
|
|
flex: 1;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
display: flex;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.center-header {
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
border-bottom: 1px solid #E6EDFF;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
|
|
|
|
.center-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #000;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
margin: 10px;
|
|
|
|
|
|
box-shadow: 0px 0px 8px 1px rgba(180, 189, 221, 0.56);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
.view-md {
|
|
|
|
|
|
padding: 24px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
position: relative;
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
:deep(p) {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
line-height: 1.8rem;
|
|
|
|
|
|
margin-block-start: 0;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
:deep(.highlight) { background: #D0EAC8; }
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.note-flag) {
|
|
|
|
|
|
width: 23px; height: 28px; line-height: 28px;
|
|
|
|
|
|
display: inline-block; text-align: center;
|
|
|
|
|
|
font-weight: bold; font-size: 10px; margin-left: 8px; cursor: pointer;
|
|
|
|
|
|
background: url("@/assets/images/reading/note.png");
|
|
|
|
|
|
color: #004EA0;
|
|
|
|
|
|
background-size: contain !important;
|
|
|
|
|
|
background-repeat: no-repeat !important;
|
|
|
|
|
|
background-position: center bottom !important;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
:deep(.note-flag:hover) {
|
|
|
|
|
|
color: #FAFBFF;
|
|
|
|
|
|
background: url("@/assets/images/reading/note_active.png");
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
:deep(.note-flag-active) {
|
|
|
|
|
|
color: #FAFBFF;
|
|
|
|
|
|
background: url("@/assets/images/reading/note_active.png");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-note {
|
|
|
|
|
|
max-width: 240px; height: 36px; line-height: 36px;
|
|
|
|
|
|
padding: 0 12px; position: absolute; display: none;
|
|
|
|
|
|
background: #004EA0;
|
|
|
|
|
|
box-shadow: 0px 0px 8px 1px rgba(180, 189, 221, 0.56);
|
|
|
|
|
|
border-radius: 8px; border: 1px solid #D5DDFF;
|
|
|
|
|
|
font-size: 14px; color: #FFFFFF;
|
|
|
|
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:01:27 +08:00
|
|
|
|
// ========== 右栏 ==========
|
|
|
|
|
|
.right-panel {
|
|
|
|
|
|
width: 400px;
|
|
|
|
|
|
min-width: 400px;
|
|
|
|
|
|
height: calc(100% - 20px);
|
|
|
|
|
|
margin: 10px 10px 10px 0;
|
|
|
|
|
|
background: linear-gradient(180deg, rgba(178, 226, 255, 0.4) 0%, rgba(106, 148, 255, 0.4) 100%);
|
|
|
|
|
|
box-shadow: 0 0 8px 1px rgba(180, 189, 221, 0.56);
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.right-placeholder {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder-content {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
.placeholder-title { margin-top: 12px; font-size: 16px; font-weight: bold; color: #666; }
|
|
|
|
|
|
.placeholder-sub { margin-top: 4px; font-size: 13px; }
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 右键菜单 ==========
|
|
|
|
|
|
.click-bar-button {
|
|
|
|
|
|
height: 36px; padding: 0 4px;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
box-shadow: 0px 0px 8px 1px rgba(180, 189, 221, 0.56);
|
|
|
|
|
|
border-radius: 8px; border: 1px solid #D5DDFF;
|
|
|
|
|
|
display: flex; justify-content: space-evenly; align-items: center;
|
|
|
|
|
|
position: fixed; z-index: 9999; font-size: 14px;
|
|
|
|
|
|
|
|
|
|
|
|
.menu-name { line-height: 36px; cursor: pointer; padding: 0 12px; }
|
|
|
|
|
|
.menu-name:hover { color: #004EA0; }
|
|
|
|
|
|
.vertical-line { height: 16px; width: 2px; background-color: #D5DDFF; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 上传弹框 ==========
|
|
|
|
|
|
.upload-demo {
|
|
|
|
|
|
width: 100%; text-align: center;
|
|
|
|
|
|
.el-upload__tip { text-align: center; margin-top: 12px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.files-upload-result {
|
|
|
|
|
|
max-height: 210px; overflow-y: auto; margin-top: 16px;
|
|
|
|
|
|
.result {
|
|
|
|
|
|
height: 30px; display: flex; justify-content: space-between; align-items: center;
|
|
|
|
|
|
.file-name {
|
|
|
|
|
|
width: 434px; font-size: 14px; color: #606771;
|
|
|
|
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 10px;
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
.result-info {
|
|
|
|
|
|
display: flex; align-items: center;
|
|
|
|
|
|
.fail-msg { width: 192px; text-align: right; font-size: 12px; color: #BD0000; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 8px; }
|
|
|
|
|
|
.loading { width: 38px; height: 38px; }
|
|
|
|
|
|
img { width: 14px; margin-right: 10px; }
|
|
|
|
|
|
.upload-remove { cursor: pointer; }
|
2026-04-02 11:36:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 12:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
.dialog-tips {
|
|
|
|
|
|
font-size: 12px; color: #858A94; margin-top: 8px;
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
|
img { width: 12px; height: 12px; margin-right: 3px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|