[前端] 研读模块改造为三栏布局:文件树+预览+问答
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<Operates v-show="url != '/login' && url != '/writing/edit'&& url != '/knowledgeBase/fileDetail'" />
|
||||
<Operates v-show="url != '/login' && url != '/writing/edit'" />
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -59,11 +59,11 @@
|
||||
import {copyToClip} from "@/utils";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {fileGuidInfo} from "@/api";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {computed, inject, ref, type Ref} from "vue";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import {transforMd} from "@/utils/markdown";
|
||||
|
||||
const state=history.state;
|
||||
const selectedFile = inject('selectedFile') as Ref<any>;
|
||||
|
||||
const emits=defineEmits(["refreshAbs","refreshCon"]);
|
||||
const mardown=new MarkdownIt();
|
||||
@@ -73,18 +73,29 @@ const refreshAbs=()=>{
|
||||
const refreshCon=()=>{
|
||||
emits('refreshCon');
|
||||
}
|
||||
const historyParams=history.state;
|
||||
const articleAbstract=ref(historyParams.articleAbstract);
|
||||
const articleKeywords=ref(historyParams.articleKeywords);
|
||||
const articleParagraph=ref(historyParams.articleParagraph);
|
||||
|
||||
//总结提炼
|
||||
onMounted(async () => {
|
||||
if(articleKeywords.value&&(articleKeywords.value.indexOf('关键词:')>-1||articleKeywords.value.indexOf('关键词:')>-1)){
|
||||
articleKeywords.value=articleKeywords.value.substring(articleKeywords.value.indexOf('关键词:')+4,articleKeywords.value.length);
|
||||
articleKeywords.value=articleKeywords.value.substring(articleKeywords.value.indexOf('关键词:')+4,articleKeywords.value.length);
|
||||
const articleAbstract=ref('');
|
||||
const articleKeywords=ref('');
|
||||
const articleParagraph=ref('');
|
||||
|
||||
// 监听选中文件变化,更新导读内容
|
||||
import {watch} from "vue";
|
||||
watch(() => selectedFile.value, (newFile) => {
|
||||
if (newFile) {
|
||||
articleAbstract.value = newFile.articleAbstract || '';
|
||||
let kw = newFile.articleKeywords || '';
|
||||
if (kw && (kw.indexOf('关键词:') > -1 || kw.indexOf('关键词:') > -1)) {
|
||||
kw = kw.substring(kw.indexOf('关键词:') + 4, kw.length);
|
||||
kw = kw.substring(kw.indexOf('关键词:') + 4, kw.length);
|
||||
}
|
||||
articleKeywords.value = kw;
|
||||
articleParagraph.value = newFile.articleParagraph || '';
|
||||
} else {
|
||||
articleAbstract.value = '';
|
||||
articleKeywords.value = '';
|
||||
articleParagraph.value = '';
|
||||
}
|
||||
})
|
||||
}, {immediate: true});
|
||||
const copyText=(text)=>{
|
||||
copyToClip(text);
|
||||
ElMessage.success("复制成功");
|
||||
@@ -95,7 +106,7 @@ const refreshGuide=async (type:string)=>{
|
||||
let param = {
|
||||
type: type,
|
||||
context: fileBox.innerText,
|
||||
fileId: historyParams.fileId,
|
||||
fileId: selectedFile.value?.fileId,
|
||||
}
|
||||
if(type==='6'){
|
||||
articleAbstract.value=''
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {getPaper} from "@/api";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {inject, onMounted, ref, watch, type Ref} from "vue";
|
||||
|
||||
const historyParams=history.state;
|
||||
const selectedFile = inject('selectedFile') as Ref<any>;
|
||||
|
||||
type Literature = {
|
||||
doc_id: string,
|
||||
@@ -53,9 +53,9 @@ const showNoData = ref(false);
|
||||
const getLiteratureList = async () => {
|
||||
try {
|
||||
let res = await getPaper({
|
||||
fileName: historyParams.fileName,
|
||||
keywords: historyParams.articleKeywords,
|
||||
id: historyParams.fileId,
|
||||
fileName: selectedFile.value?.fileName,
|
||||
keywords: selectedFile.value?.articleKeywords,
|
||||
id: selectedFile.value?.fileId,
|
||||
retry: 0
|
||||
})
|
||||
if (res.code === 200 && res.data && res.data.articles && res.data.articles.length > 0) {
|
||||
@@ -147,8 +147,17 @@ const publishDateFormat = (articles: Literature[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 监听文件切换,重新加载文献
|
||||
watch(() => selectedFile.value?.fileId, () => {
|
||||
if (selectedFile.value?.fileId) {
|
||||
getLiteratureList();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getLiteratureList();
|
||||
if (selectedFile.value?.fileId) {
|
||||
getLiteratureList();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import {inject, onMounted, reactive, ref} from "vue";
|
||||
import {inject, onMounted, reactive, ref, watch, type Ref} from "vue";
|
||||
import {
|
||||
deleteChatMessageById,
|
||||
delHistoryChatById,
|
||||
@@ -62,6 +62,9 @@ import ReadingCreateMessage from "@/components/ReadingCreateMessage.vue";
|
||||
import {withLoading} from "@/utils/loading";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {debounce} from "@/utils";
|
||||
|
||||
const selectedFile = inject('selectedFile') as Ref<any>;
|
||||
|
||||
// 引用内容
|
||||
const quoteMsg = inject("quoteMsg");
|
||||
const updateQuoteMsg = inject("updateQuoteMsg");
|
||||
@@ -79,7 +82,6 @@ const sendStatus = ref(false);
|
||||
const chatStatus = ref(false);
|
||||
const chatNumber = ref(0);
|
||||
const chatId = ref('');
|
||||
const historyParam = history.state;
|
||||
const conversationId = ref('');
|
||||
const chatInfo = reactive([]);
|
||||
let controller = null;
|
||||
@@ -143,7 +145,7 @@ const getFetchChatAPI = async (prompt) => {
|
||||
const chatData = {
|
||||
prompt,
|
||||
type: 99,
|
||||
fileId: historyParam.fileId
|
||||
fileId: selectedFile.value?.fileId
|
||||
};
|
||||
try {
|
||||
const fetchChatAPIData = await fetchChatAPI(chatData);
|
||||
@@ -196,10 +198,10 @@ const getFetchChatAPIProcess = async (type: string) => {
|
||||
headers: headers,
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
fileNames: [historyParam.embeddingId],
|
||||
fileNames: [selectedFile.value?.embeddingId],
|
||||
conversationId: conversationId.value,
|
||||
promptName: "default",
|
||||
knowledgeBaseIdList: [historyParam.folderId],
|
||||
knowledgeBaseIdList: [selectedFile.value?.folderId],
|
||||
chatType: type,
|
||||
quote: quoteMsg.value
|
||||
}),
|
||||
@@ -313,15 +315,21 @@ const autoPositionToBottom = () => {
|
||||
element.scrollTop = element.scrollHeight + 55;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
//请求文件对话的dataId
|
||||
const loadChatHistory = async () => {
|
||||
// 重置对话状态
|
||||
chatInfo.length = 0;
|
||||
chatId.value = '';
|
||||
firstChat.value = true;
|
||||
chatNumber.value = 0;
|
||||
conversationId.value = '';
|
||||
|
||||
if (!selectedFile.value?.fileId) return;
|
||||
|
||||
try {
|
||||
let hisResponse = await listFileChatHistory(historyParam.fileId);
|
||||
//根据dataId请求获取历史记录
|
||||
let hisResponse = await listFileChatHistory(selectedFile.value.fileId);
|
||||
if (hisResponse.code == 200 && hisResponse.data) {
|
||||
chatNumber.value = hisResponse.data;
|
||||
firstChat.value = false;
|
||||
//请求文件对话的dataId
|
||||
let historyList = await listChatMessage({
|
||||
chatNumber: chatNumber.value,
|
||||
fileType: 1
|
||||
@@ -358,9 +366,17 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
// 新文件没有历史记录,不报错
|
||||
}
|
||||
};
|
||||
|
||||
// 监听文件切换,重新加载对话历史
|
||||
watch(() => selectedFile.value?.fileId, () => {
|
||||
loadChatHistory();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadChatHistory();
|
||||
handleReadingMessageContentScroll();
|
||||
})
|
||||
let messageInstance = null;
|
||||
|
||||
@@ -34,16 +34,6 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'KnowledgeBase',
|
||||
component: () => import('@/views/reading/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/knowledgeBase/fileList',
|
||||
name: 'FileList',
|
||||
component: () => import('@/views/reading/fileList.vue'),
|
||||
},
|
||||
{
|
||||
path: '/knowledgeBase/fileDetail',
|
||||
name: 'FileDetail',
|
||||
component: () => import('@/views/reading/fileDetail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/applications',
|
||||
name: 'Application',
|
||||
|
||||
@@ -1,883 +0,0 @@
|
||||
<template>
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<img @click="goback" src="@/assets/images/writing/back.png" alt="">
|
||||
<div class="title" v-text="fileName"></div>
|
||||
</div>
|
||||
<div class="file-content-container">
|
||||
<div class="file-content" ref="fileContent" id="file-content">
|
||||
<div class="view-md" id="file-html-content" v-html="docHtml" ></div><!--@click="handleAnchorClick"-->
|
||||
|
||||
<div id="note-content" :title="noteContent" class="file-note"></div>
|
||||
</div>
|
||||
<div class="doc-right">
|
||||
<ReadingBox ref="readingBox"/>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- 添加笔记弹框 -->
|
||||
<el-dialog v-model="showAddNoteDialog" class="add-note-dialog" title="写下你的想法吧"
|
||||
width="715" style="padding: 0; box-shadow: 0px 0px 8px 1px rgba(180,189,221,0.56); border-radius: 16px;"
|
||||
:close-on-click-modal="false" :modal-append-to-body="false" :close-on-press-escape="false"
|
||||
@open="openAddNoteDialog">
|
||||
<el-input ref="noteMsgRef" v-model="noteMsg" type="textarea" :rows="10" placeholder="请输入笔记内容"/>
|
||||
<template #footer>
|
||||
<div class="dialog-footer" style="text-align: center;">
|
||||
<el-button type="primary" @click="submitNote()">
|
||||
发表
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {nextTick, onBeforeUnmount, onMounted, provide, reactive, ref,getCurrentInstance} from 'vue';
|
||||
|
||||
import ReadingBox from "@/components/ReadingBox.vue";
|
||||
import {addFileNote, getFileContent} from "@/api";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import {copyToClip, escapeRegExp, getGlobalSelectionPosition} from "@/utils";
|
||||
import {withLoading} from "@/utils/loading";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {transforMd} from "@/utils/markdown";
|
||||
import Mark from "mark.js";
|
||||
|
||||
const readingBox = ref(null);
|
||||
const docxFlow = ref<any>(null)
|
||||
const pdfFlow = ref<any>(null)
|
||||
const historyParams = history.state;
|
||||
const fileContent = ref(null);
|
||||
const shortMenuDom = ref(null);
|
||||
const shortMenuShow = ref(false);
|
||||
const selectText = ref('');
|
||||
|
||||
|
||||
const docHtml = ref('');
|
||||
|
||||
const fileName = historyParams.fileName;
|
||||
|
||||
const suffix = fileName.slice(fileName.lastIndexOf('.') + 1);
|
||||
|
||||
const goback = () => {
|
||||
history.back();
|
||||
}
|
||||
|
||||
const contentMenuList = ['引用', '复制', '名词解释', '添加笔记','翻译'];
|
||||
|
||||
// 引用内容
|
||||
const quoteMsg = ref<string>('');
|
||||
provide('quoteMsg', quoteMsg);
|
||||
provide('updateQuoteMsg', (newValue) => {
|
||||
quoteMsg.value = newValue;
|
||||
});
|
||||
|
||||
const fileContentMenu = (index) => {
|
||||
disShowShortMenu();
|
||||
if (index === 0) {
|
||||
// 引用内容
|
||||
quoteMsg.value = selectText.value;
|
||||
if (quoteMsg.value && quoteMsg.value.length > 1000) {
|
||||
quoteMsg.value.slice(0, 1000);
|
||||
}
|
||||
readingBox.value.changeHead(2);
|
||||
return;
|
||||
}
|
||||
if (index === 1) {
|
||||
//复制内容
|
||||
copyToClip(selectText.value);
|
||||
ElMessage.success("复制成功");
|
||||
}
|
||||
if (index === 2) {
|
||||
//名词解释
|
||||
let sendText = "我想请你解释一下这个词语的含义:" + selectText.value;
|
||||
if (readingBox.value) {
|
||||
readingBox.value.changeHead(2);
|
||||
readingBox.value.readingBoxSend(sendText, '1');
|
||||
}
|
||||
}
|
||||
if (index === 3) {
|
||||
// 添加笔记
|
||||
readingBox.value.changeHead(3);
|
||||
showAddNoteDialog.value = true;
|
||||
}
|
||||
if (index === 4) {
|
||||
let sendText = selectText.value;
|
||||
// 添加笔记
|
||||
readingBox.value.changeHead(4);
|
||||
readingBox.value.translateQuery(sendText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏
|
||||
*/
|
||||
const disShowShortMenu = () => {
|
||||
shortMenuShow.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件html格式的内容
|
||||
*/
|
||||
const fileNote = reactive({
|
||||
notes: []
|
||||
});
|
||||
const noteContent = ref('');
|
||||
provide('fileNote', fileNote);
|
||||
|
||||
// LaTeX 公式正则表达式
|
||||
|
||||
const pattern = /(\$\$.*?\$\$|\$.*?\$)/g;
|
||||
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
*/
|
||||
const getFileHtmlContent = async () => {
|
||||
let offsets = [];
|
||||
try {
|
||||
let res = await withLoading(getFileContent)({
|
||||
fileId: historyParams.fileId,
|
||||
embeddingId: historyParams.embeddingId,
|
||||
knowledgeBaseId: historyParams.folderId
|
||||
});
|
||||
if (res && res.code === 200 && res.data && res.data.content) {
|
||||
let content = res.data.content;
|
||||
// 处理公式
|
||||
content = content.replace(pattern, (match, captureGroup, offset, originalString) => {
|
||||
offsets.push(offset);
|
||||
return transforMd(match);
|
||||
});
|
||||
docHtml.value = content.replace(/<p>(.*?<span class="katex">.*?<\/span>.*?)<\/p>/g, '$1');
|
||||
fileNote.notes = res.data.notes;
|
||||
} else {
|
||||
ElMessage.error('文件内容获取失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
console.log(error);
|
||||
}
|
||||
// 给公式标签加id
|
||||
try {
|
||||
await nextTick();
|
||||
handelLaTeXElements(offsets);
|
||||
handelCommentElements();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// 给LaTeX公式标签加id
|
||||
const handelLaTeXElements = (offsets) => {
|
||||
let katexElements = document.querySelectorAll('span[class="katex"]');
|
||||
for (let i = 0; i < katexElements.length; i++) {
|
||||
katexElements[i].setAttribute('id', 'katex-'+ offsets[i]);
|
||||
}
|
||||
}
|
||||
// 给批注的a标签添加点击事件
|
||||
const handelCommentElements = () => {
|
||||
document.querySelectorAll('a[href^="#comment-"]').forEach(element => {
|
||||
let targetId = element.getAttribute('href');
|
||||
element.onclick = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 笔记
|
||||
const showAddNoteDialog = ref(false);
|
||||
const noteMsg = ref('');
|
||||
const noteMsgRef = ref(null);
|
||||
type NoteParam = {
|
||||
id:string,
|
||||
fileId: number,
|
||||
pstartId: string,
|
||||
pendId: string,
|
||||
wordContent: string,
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
noteContent: string
|
||||
}
|
||||
type PositionInfo = {
|
||||
start: number,
|
||||
end: number,
|
||||
parentStart: string | null,
|
||||
parentEnd: string | null
|
||||
}
|
||||
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);
|
||||
});
|
||||
// 显示笔记的方式,mouseover | click
|
||||
const showFileNoteEvent = ref('');
|
||||
/**
|
||||
* 打开添加笔记弹框的监听钩子方法
|
||||
*/
|
||||
const openAddNoteDialog = async () => {
|
||||
await nextTick();
|
||||
if (noteMsgRef.value) {
|
||||
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 updateFileContent();
|
||||
} else {
|
||||
ElMessage.error('笔记修改失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
}
|
||||
} else {
|
||||
noteParam.value.fileId = historyParams.fileId;
|
||||
noteParam.value.pstartId = positionInfo.value.parentStart ? positionInfo.value.parentStart : '';
|
||||
noteParam.value.pendId = positionInfo.value.parentEnd ? 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 updateFileContent();
|
||||
} else {
|
||||
ElMessage.error('笔记添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 更新文章内容
|
||||
*/
|
||||
const updateFileContent = async () => {
|
||||
await getFileHtmlContent();
|
||||
handelNoteFlagMouseEvent();
|
||||
}
|
||||
provide('updateFileContent', updateFileContent);
|
||||
/**
|
||||
* 隐藏笔记效果
|
||||
*/
|
||||
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();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 显示笔记内容
|
||||
* @param note
|
||||
*/
|
||||
const showFileNote = (note: any, isPosition: boolean = false) => {
|
||||
displayFileNote();
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
let noteElement = document.getElementById('note-' + (activeNoteIndex.value + 1));
|
||||
let noteContentElement = document.getElementById('note-content');
|
||||
let fileContentElement = document.getElementById("file-content");
|
||||
const rectFileContent = fileContentElement.getBoundingClientRect();
|
||||
if (noteElement) {
|
||||
// 选中正文中的笔记标号
|
||||
noteElement.classList.add('note-flag-active');
|
||||
|
||||
// 高亮显示
|
||||
highlightSelectedWord(note);
|
||||
|
||||
// 是否需要定位
|
||||
if (isPosition) {
|
||||
let selectedElements = document.querySelectorAll('mark[class^="highlight"]');
|
||||
const fileContentElement = document.getElementById('file-content');
|
||||
if (selectedElements.length > 0 && fileContentElement) {
|
||||
selectedElements[0].scrollIntoView({
|
||||
behavior:'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 控制笔记弹框的位置及内容
|
||||
noteContent.value = note.noteContent;
|
||||
const rectNoteElement = noteElement.getBoundingClientRect();
|
||||
if (noteContentElement) {
|
||||
noteContentElement.innerHTML = noteContent.value;
|
||||
noteContentElement.style.display = 'block';
|
||||
let rectNoteContentElement = noteContentElement.getBoundingClientRect();
|
||||
let left = rectNoteElement.left - 10;
|
||||
if (left + rectNoteContentElement.width > rectFileContent.width) {
|
||||
left = rectFileContent.width - rectNoteContentElement.width;
|
||||
}
|
||||
noteContentElement.style.left = left + 'px';
|
||||
noteContentElement.style.top = rectNoteElement.top - rectNoteElement.height - 8 + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 对指定的文字高亮显示
|
||||
* @param word
|
||||
* @param pstartId
|
||||
* @param pendId
|
||||
*/
|
||||
const highlightSelectedWord = (note: any = null) => {
|
||||
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']
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取p标签id为note-span标签以外的文字
|
||||
* @param p
|
||||
*/
|
||||
const getPContent = (p: HTMLElement) => {
|
||||
const clone = p.cloneNode(true) as HTMLElement;
|
||||
const spans = clone.querySelectorAll('span[id^="note-"]');
|
||||
spans.forEach(span => {
|
||||
clone.removeChild(span);
|
||||
});
|
||||
return clone.textContent?.trim();
|
||||
}
|
||||
/**
|
||||
* 移除高亮
|
||||
*/
|
||||
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', (e) => {
|
||||
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', (e) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 打开笔记tab页面
|
||||
if (readingBox.value) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
docxFlow.value = null;
|
||||
pdfFlow.value = null;
|
||||
try {
|
||||
await getFileHtmlContent();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
}
|
||||
|
||||
if (fileContent.value) {
|
||||
// 绑定笔记标记的鼠标事件
|
||||
handelNoteFlagMouseEvent();
|
||||
|
||||
//绑定mouseup事件
|
||||
fileContent.value.addEventListener('mouseup', function (event) {
|
||||
activeNoteIndex.value = -1;
|
||||
displayFileNote();
|
||||
removeHighlightSelectedWord();
|
||||
// 实现右侧笔记的定位
|
||||
setTimeout(() => {
|
||||
const selection = window.getSelection();
|
||||
selectText.value = selection.toString();
|
||||
let position = getGlobalSelectionPosition(selection);
|
||||
positionInfo.value = position;
|
||||
if (selectText.value && shortMenuDom.value && fileContent.value.contains(selection.anchorNode)) {
|
||||
//给selection赋值,然后弹出菜单,再添加一个菜单隐藏的事件
|
||||
let offset = getViewportOffsetByEvent(event);
|
||||
shortMenuShow.value = true;
|
||||
shortMenuDom.value.style.left = offset.left + 'px';
|
||||
shortMenuDom.value.style.top = offset.top + 'px';
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 绑定滚动条滚动事件
|
||||
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 = rectNoteElement.top - rectNoteElement.height - 8 + 'px';
|
||||
if (top < 75) {
|
||||
noteContentElement.style.display = 'none';
|
||||
} else if (activeNoteIndex.value != -1) {
|
||||
noteContentElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousedown', function (event) {
|
||||
let execlute = document.getElementById('shortMenuDom');
|
||||
if (execlute && execlute.contains(event.target)) {
|
||||
} else {
|
||||
disShowShortMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
})
|
||||
|
||||
// 用于触发重新渲染的响应式变量
|
||||
const reRenderDocKey = ref(0);
|
||||
const reRenderPdfKey = ref(0);
|
||||
// 处理窗口大小改变的函数
|
||||
const handleResize = async () => {
|
||||
if (suffix == 'docx') {
|
||||
reRenderDocKey.value++;
|
||||
} else if (suffix == 'pdf') {
|
||||
reRenderPdfKey.value++;
|
||||
}
|
||||
|
||||
// 计算笔记弹框的位置
|
||||
let noteElement = document.getElementById('note-' + (activeNoteIndex.value + 1));
|
||||
let noteContentElement = document.getElementById('note-content');
|
||||
let fileContentElement = document.getElementById("file-content");
|
||||
|
||||
if (noteElement && noteContentElement && fileContentElement) {
|
||||
const rectFileContent = fileContentElement.getBoundingClientRect();
|
||||
const rectNoteElement = noteElement.getBoundingClientRect();
|
||||
let rectNoteContentElement = noteContentElement.getBoundingClientRect();
|
||||
let left = rectNoteElement.left - 10;
|
||||
if (left + rectNoteContentElement.width > rectFileContent.width) {
|
||||
left = rectFileContent.width - rectNoteContentElement.width;
|
||||
}
|
||||
noteContentElement.style.left = left + 'px';
|
||||
let top = rectNoteElement.top - rectNoteElement.height - 8;
|
||||
noteContentElement.style.top = rectNoteElement.top - rectNoteElement.height - 8 + 'px';
|
||||
if (top < 75) {
|
||||
noteContentElement.style.display = 'none';
|
||||
} else if (activeNoteIndex.value != -1) {
|
||||
noteContentElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getViewportOffsetByEvent = (evt) => {
|
||||
var el = evt.target || evt.srcElement;
|
||||
var frameEl = getWindow(el).frameElement;
|
||||
var offset = {
|
||||
left: evt.clientX,
|
||||
top: evt.clientY
|
||||
};
|
||||
if (frameEl && el.ownerDocument !== document) {
|
||||
var rect = getClientRect(frameEl);
|
||||
offset.left += rect.left;
|
||||
offset.top += rect.top;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
const getClientRect = (element) => {
|
||||
var bcr;
|
||||
//trace IE6下在控制编辑器显隐时可能会报错,catch一下
|
||||
try {
|
||||
bcr = element.getBoundingClientRect();
|
||||
} catch (e) {
|
||||
bcr = {left: 0, top: 0, height: 0, width: 0}
|
||||
}
|
||||
var rect = {
|
||||
left: Math.round(bcr.left),
|
||||
top: Math.round(bcr.top),
|
||||
height: Math.round(bcr.bottom - bcr.top),
|
||||
width: Math.round(bcr.right - bcr.left)
|
||||
};
|
||||
var doc;
|
||||
while ((doc = element.ownerDocument) !== document &&
|
||||
(element = getWindow(doc).frameElement)) {
|
||||
bcr = element.getBoundingClientRect();
|
||||
rect.left += bcr.left;
|
||||
rect.top += bcr.top;
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
const getWindow = (node) => {
|
||||
var doc = node.ownerDocument || node;
|
||||
return doc.defaultView || doc.parentWindow;
|
||||
}
|
||||
|
||||
const handleAnchorClick = (event) => {
|
||||
// 检查点击的是否是 a 标签
|
||||
if (event.target.tagName === 'A') {
|
||||
const href = event.target.getAttribute('href');
|
||||
|
||||
// 检查是否是锚点链接(以 # 开头)
|
||||
if (href && href.startsWith('#')) {
|
||||
event.preventDefault();
|
||||
const anchorId = href.substring(1);
|
||||
scrollToAnchor(anchorId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToAnchor = (id) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
provide('noteMsg',noteMsg);
|
||||
provide('showAddNoteDialog',showAddNoteDialog);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.body {
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 65px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 0px 0px 0px 0px;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 20px;
|
||||
margin-left: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: calc(100% - 40px);
|
||||
line-height: 65px;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-content-container {
|
||||
margin-top: 20px;
|
||||
height: calc(100% - 95px);
|
||||
display: flex;
|
||||
|
||||
.file-content {
|
||||
overflow: auto;
|
||||
width: calc(100% - 580px);
|
||||
height: calc(100% - 30px);
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 0px 8px 1px rgba(180, 189, 221, 0.56);
|
||||
border-radius: 16px 16px 16px 16px;
|
||||
margin-left: 20px;
|
||||
.mark{
|
||||
background: #D0EAC8;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 35px 35px;
|
||||
border: none; /* 去掉边框 */
|
||||
resize: none; /* 禁止调整大小 */
|
||||
//overflow: hidden; /* 隐藏溢出的内容,如果内容超出可视区域 */
|
||||
background: none;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.view-md {
|
||||
padding: 30px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
::v-deep(p) {
|
||||
font-size: 16px;
|
||||
line-height: 1.8rem;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
::v-deep(.highlight) {
|
||||
background: #D0EAC8;
|
||||
}
|
||||
|
||||
::v-deep(.table-container) {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
::v-deep(.note-flag) {
|
||||
width: 23px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-family: DIN Alternate, DIN Alternate;
|
||||
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;
|
||||
}
|
||||
|
||||
::v-deep(.note-flag:hover) {
|
||||
color: #FAFBFF;
|
||||
background: url("@/assets/images/reading/note_active.png");
|
||||
}
|
||||
|
||||
::v-deep(.note-flag-active) {
|
||||
color: #FAFBFF;
|
||||
background: url("@/assets/images/reading/note_active.png");
|
||||
}
|
||||
|
||||
::v-deep(#page-container-1) {
|
||||
.note-flag {
|
||||
width: 69px !important;
|
||||
height: 84px !important;
|
||||
font-size: 42px !important;
|
||||
line-height: 84px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 8px 8px 8px;
|
||||
border: 1px solid #D5DDFF;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
::v-deep(.docx-wrapper) {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
|
||||
span::selection {
|
||||
background: #D0EAC8;
|
||||
}
|
||||
|
||||
textarea::selection {
|
||||
background: #D0EAC8;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.docx-wrapper .docx) {
|
||||
width: 100% !important;
|
||||
padding: 20px 40px !important;
|
||||
min-height: 100% !important;
|
||||
}
|
||||
|
||||
::v-deep(.docx-wrapper > section.docx) {
|
||||
box-shadow: none !important;
|
||||
background: none;
|
||||
}
|
||||
|
||||
::v-deep(.docx *) {
|
||||
font-size: 16px !important;
|
||||
font-family: PingFangSC !important;
|
||||
line-height: 30px !important;
|
||||
color: #333639 !important;
|
||||
}
|
||||
|
||||
::v-deep(.vue-office-pdf) {
|
||||
padding: 20px 40px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::v-deep(.vue-office-pdf-wrapper) {
|
||||
padding: 0px !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.vue-office-pdf-wrapper > canvas) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
::v-deep(p::selection) {
|
||||
background: #D0EAC8;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.doc-right {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
width: 510px;
|
||||
height: calc(100% - 30px);
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 8px 8px 8px;
|
||||
border: 1px solid #D5DDFF;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.add-note-dialog {
|
||||
box-shadow: 0px 0px 8px 1px rgba(180, 189, 221, 0.56);
|
||||
border-radius: 16px 16px 16px 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,766 +0,0 @@
|
||||
<template>
|
||||
<div class="doc-writing">
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div class="doc-list">
|
||||
<div class="doc-list-top">
|
||||
<div style="display: flex;">
|
||||
<img @click="goBack" src="../../assets/images/writing/back.png" alt="" style="margin-top:5px">
|
||||
<div class="doc-list-title" style="margin-left:15px" v-text="pageTitle"></div>
|
||||
</div>
|
||||
|
||||
<div class="doc-list-operate">
|
||||
<el-input style="width: 316px;height: 32px" v-model="searchWord" placeholder="请输入文件名称关键字搜索">
|
||||
<template #suffix>
|
||||
<img src="@/assets/images/search.png" class="doc-search">
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<div class="doc-import" @click="clickUpload">
|
||||
<img src="@/assets/images/writing/doc_import.png">
|
||||
<span>导入</span>
|
||||
</div>
|
||||
<div class="batch-delete" @click="batchDelete">
|
||||
<img src="@/assets/images/reading/del.png" alt="">
|
||||
<p class="batch-del">批量删除</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="doc-list-empty" v-if="myFileList.list.length === 0">
|
||||
<img src="@/assets/images/writing/doc_file.png" alt="">
|
||||
<div class="empty-tips">
|
||||
<div class="tips-top">
|
||||
<span>暂无相关内容,</span>
|
||||
<span @click="clickUpload" style="color: #004EA0;cursor: pointer">点击上传文档</span>
|
||||
</div>
|
||||
<div class="tips-bottom">支持{{ allowFileTypes.join('/') }}格式文件,文件大小不超过10M。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="myFileList.list.length > 0" class="doc-list-non-empty">
|
||||
<el-table :data="myFileList.list" style="width: 100%"
|
||||
:row-style="myFileRowStyle"
|
||||
:header-row-style="myFileHeaderRowStyle"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column label="文件名称" min-width="300">
|
||||
<template #default="scope">
|
||||
<div class="doc-file-title" @click="showDetail(scope.row)">{{ scope.row.filename }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文档大小" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ formateSize(scope.row.size) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<div class="doc-operate">
|
||||
<img @click="renameDoc(scope.row)" title="重命名" src="@/assets/images/writing/doc_edit.png" alt="">
|
||||
<img @click="downLoadDoc(scope.row)" title="下载" src="@/assets/images/writing/doc_download.png" alt="">
|
||||
<img @click="delDoc(scope.row)" title="删除" src="@/assets/images/writing/doc_del.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件重命名弹框 -->
|
||||
<el-dialog v-model="showRenameDialog" title="重命名" width="500"
|
||||
:close-on-click-modal="false" :modal-append-to-body="false" :close-on-press-escape="false">
|
||||
<el-input v-model="docName" placeholder="请输入文件名称"/>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="showRenameDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveDocName()">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 上传文件弹框 -->
|
||||
<el-dialog v-model="showUploadDialog" title="上传文件" width="715" @close="closeUpload()"
|
||||
:close-on-click-modal="false" :modal-append-to-body="false" :close-on-press-escape="false">
|
||||
<el-upload
|
||||
style="display: inline-block;"
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
accept=".pdf,.docx,.txt,.xlsx,.xls"
|
||||
name="files"
|
||||
multiple
|
||||
:show-file-list="false"
|
||||
:file-list="uploadingFileList"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<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">
|
||||
支持{{ allowFileTypes.join('/') }}格式文件,文件大小不超过10M
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div class="files-upload-result">
|
||||
<div class="result" v-for="item in uploadingFileList">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer" 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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {delFile, delFiles, downloadFile, editFile, getKnowledgeBaseContent, getSize, uploadFile} from "@/api";
|
||||
import {withLoading} from "@/utils/loading";
|
||||
import {formatFileSize} from "@/utils/index"
|
||||
import {ElMessage, ElMessageBox, type UploadFile, type UploadFiles} from "element-plus";
|
||||
import {UploadFilled} from '@element-plus/icons-vue'
|
||||
import Loading from '@/components/Loading.vue'
|
||||
|
||||
type DocInfo = {
|
||||
id: number;
|
||||
filename: string;
|
||||
context: string;
|
||||
articleAbstract: string;
|
||||
articleKeywords: string;
|
||||
articleParagraph: string;
|
||||
embeddingId: string;
|
||||
}
|
||||
|
||||
interface CusUploadFile extends UploadFile {
|
||||
message?: string | null
|
||||
}
|
||||
|
||||
const historyParams = history.state;
|
||||
|
||||
// 允许上传的文件类型
|
||||
const allowFileTypes = ["TXT", "DOCX", 'PDF','XLSX','XLS'];
|
||||
|
||||
//文件夹id
|
||||
const knowledgeBaseId = historyParams.folderId;
|
||||
const pageTitle = historyParams.fileName;
|
||||
const uploadingFileList = ref([] as CusUploadFile[]);
|
||||
const isUploading = ref(false);
|
||||
const uploadFinishFlag = ref(true);
|
||||
const showUploadDialog = ref(false);
|
||||
// 单文件大小限制3M
|
||||
const FILE_MAX_SIZE = 10 * 1024 * 1024;
|
||||
// 总文件大小限制50M
|
||||
const TOTAL_FILE_MAX_SIZE = 50 * 1024 * 1024;
|
||||
// 已上传文件的大小
|
||||
const totalFileSize = ref<number>(0);
|
||||
// 当前正在上传的文件
|
||||
const currentIndex = ref(0);
|
||||
|
||||
/**
|
||||
* 导入按钮及上传文档按钮的点击事件
|
||||
*/
|
||||
const clickUpload = async () => {
|
||||
await getUploadedSize();
|
||||
showUploadDialog.value = true;
|
||||
if (uploadFinishFlag.value) {
|
||||
uploadingFileList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件状态改变时的钩子
|
||||
* @param file
|
||||
* @param newFileList
|
||||
*/
|
||||
const handleFileChange = async (file: UploadFile, newFileList: UploadFiles) => {
|
||||
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;
|
||||
}
|
||||
|
||||
totalFileSize.value += file.size ? file.size : 0;
|
||||
if (totalFileSize.value > TOTAL_FILE_MAX_SIZE) {
|
||||
totalFileSize.value -= file.size ? file.size : 0;
|
||||
ElMessage.warning('个人素材总量不能超过50M,文件' + file.name + '添加失败');
|
||||
return;
|
||||
}
|
||||
|
||||
uploadingFileList.value.push(file);
|
||||
};
|
||||
// 文件自动上传
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {immediate: true, deep: true});
|
||||
/**
|
||||
* 文件删除时的钩子
|
||||
* @param file
|
||||
* @param newFileList
|
||||
*/
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
uploadingFileList.value = uploadingFileList.value.filter(item => item.uid !== file.uid);
|
||||
}
|
||||
/**
|
||||
* 开始上传文件
|
||||
*/
|
||||
const startUpload = async () => {
|
||||
await uploadNextFile();
|
||||
};
|
||||
/**
|
||||
* 获取已上传文件的大小
|
||||
*/
|
||||
const getUploadedSize = async () => {
|
||||
try {
|
||||
let res = await withLoading(getSize)();
|
||||
if (res.code === 200) {
|
||||
totalFileSize.value = res.data;
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传下一个文件
|
||||
*/
|
||||
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);
|
||||
formData.append('folderId', 0);
|
||||
formData.append('knowledgeBaseId', knowledgeBaseId);
|
||||
|
||||
try {
|
||||
let res = await uploadFile(formData);
|
||||
if (res.code && res.code != 200) {
|
||||
ElMessage.error(res.msg + "【" + file.name + "】");
|
||||
file.status = 'fail';
|
||||
file.message = res.msg;
|
||||
} else {
|
||||
// 文件上传成功
|
||||
file.status = 'success';
|
||||
}
|
||||
} catch (error) {
|
||||
// 文件上传失败
|
||||
file.status = 'fail';
|
||||
file.message = error && error.message ? error.message : '未知错误';
|
||||
} finally {
|
||||
// 处理下一个文件
|
||||
currentIndex.value++;
|
||||
await uploadNextFile();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 关闭上传文件弹框
|
||||
*/
|
||||
const closeUpload = () => {
|
||||
showUploadDialog.value = false;
|
||||
// uploadingFileList.value = [];
|
||||
currentIndex.value = 0;
|
||||
getMyFileList();
|
||||
};
|
||||
|
||||
// 路由
|
||||
const router = useRouter();
|
||||
const goBack = () => {
|
||||
router.push({name: 'KnowledgeBase'})
|
||||
}
|
||||
|
||||
// 文件检索关键词
|
||||
const searchWord = ref('');
|
||||
// 我的文件列表
|
||||
const myFileList = reactive({list: []});
|
||||
const netFileList = reactive({list: []});
|
||||
|
||||
/**
|
||||
* 获取我的文件列表
|
||||
*/
|
||||
const getMyFileList = async () => {
|
||||
try {
|
||||
let res = await withLoading(getKnowledgeBaseContent)({
|
||||
folderType: "1",
|
||||
knowledgeBaseId: knowledgeBaseId + "",
|
||||
patentId: "0"
|
||||
});
|
||||
if (res && res.data) {
|
||||
netFileList.list = res.data;
|
||||
myFileList.list = netFileList.list;
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
}
|
||||
}
|
||||
|
||||
watch(searchWord, (newValue, oldValue) => {
|
||||
if (newValue && newValue.trim()) {
|
||||
myFileList.list = netFileList.list.filter(item => {
|
||||
return item.filename.indexOf(newValue.trim()) !== -1
|
||||
})
|
||||
} else {
|
||||
myFileList.list = netFileList.list;
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 表格体背景色
|
||||
*/
|
||||
const myFileRowStyle = () => {
|
||||
return '--el-table-tr-bg-color: rgba(250, 251, 255, 0.5);'
|
||||
}
|
||||
|
||||
/**
|
||||
* 表头背景色
|
||||
*/
|
||||
const myFileHeaderRowStyle = () => {
|
||||
return '--el-table-header-bg-color: #FAFBFF;'
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看文件详情
|
||||
* @param doc
|
||||
*/
|
||||
const showDetail = (doc: DocInfo) => {
|
||||
if(!doc.articleAbstract){
|
||||
doc.articleAbstract="暂无内容,请重试"
|
||||
}
|
||||
if(!doc.articleKeywords){
|
||||
doc.articleKeywords="暂无内容,请重试"
|
||||
}
|
||||
if(!doc.articleParagraph){
|
||||
doc.articleParagraph="暂无内容,请重试"
|
||||
}
|
||||
if (doc.articleAbstract.startsWith("\"")) {
|
||||
doc.articleAbstract = JSON.parse(doc.articleAbstract);
|
||||
}
|
||||
if (doc.articleKeywords.startsWith("\"")) {
|
||||
doc.articleKeywords = JSON.parse(doc.articleKeywords);
|
||||
}
|
||||
if (doc.articleParagraph.startsWith("\"")) {
|
||||
doc.articleParagraph = JSON.parse(doc.articleParagraph);
|
||||
}
|
||||
router.push({
|
||||
name: "FileDetail",
|
||||
state: {
|
||||
folderName: pageTitle,
|
||||
folderId: knowledgeBaseId,
|
||||
fileId: doc.id,
|
||||
embeddingId: doc.embeddingId,
|
||||
fileName: doc.filename,
|
||||
articleAbstract: doc.articleAbstract,
|
||||
articleKeywords: doc.articleKeywords,
|
||||
articleParagraph: doc.articleParagraph,
|
||||
fullContent: doc.context
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名
|
||||
* @param doc
|
||||
*/
|
||||
const showRenameDialog = ref(false);
|
||||
const docName = ref('');
|
||||
const docType = ref('');
|
||||
const docId = ref(0);
|
||||
const renameDoc = (doc: DocInfo) => {
|
||||
if (doc.filename) {
|
||||
let names = doc.filename.split('.');
|
||||
docName.value = names[0];
|
||||
if (names.length === 2) {
|
||||
docType.value = names[1];
|
||||
}
|
||||
}
|
||||
docId.value = doc.id;
|
||||
showRenameDialog.value = true;
|
||||
}
|
||||
const saveDocName = async () => {
|
||||
if (docName.value === '' || docName.value.trim() === '') {
|
||||
ElMessage.error("请输入文件名");
|
||||
return;
|
||||
}
|
||||
let param = new FormData();
|
||||
param.append('fileId', docId.value + '');
|
||||
param.append('fileName', docName.value + '.' + docType.value);
|
||||
param.append('folderId', '0');
|
||||
//保存成功后关闭弹窗
|
||||
try {
|
||||
await withLoading(editFile)(param);
|
||||
//调用重命名接口
|
||||
ElMessage.success("修改成功");
|
||||
getMyFileList();
|
||||
} catch {
|
||||
ElMessage.error("修改失败");
|
||||
}
|
||||
showRenameDialog.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文档
|
||||
*/
|
||||
const downLoadDoc = async function (file) {
|
||||
try {
|
||||
const response = await downloadFile({fileId: file.id});
|
||||
const url = URL.createObjectURL(new Blob([response]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', file.filename); // 设置下载文件的名称
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error && error.message ? error.message : '未知错误');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*/
|
||||
const delDoc = (doc: DocInfo) => {
|
||||
delHistory(doc.id);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMyFileList();
|
||||
})
|
||||
|
||||
// 删除文档
|
||||
const delHistory = async (id: number) => {
|
||||
ElMessageBox.confirm("确认删除文档吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
let res = await withLoading(delFile)(id + '');
|
||||
if (res && res.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
getMyFileList();
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessage.error("删除失败");
|
||||
} catch (error: any) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const batchDelete = async () => {
|
||||
if (idList.value.length > 0) {
|
||||
ElMessageBox.confirm("确认删除文档吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
let res = await withLoading(delFiles)({
|
||||
fileList: idList.value,
|
||||
folderList: []
|
||||
});
|
||||
|
||||
if (res && res.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
} else {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
|
||||
await getMyFileList();
|
||||
} catch (err) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const formateSize = (size: string) => {
|
||||
return formatFileSize(Number(size.substring(0, size.length - 1)));
|
||||
}
|
||||
|
||||
const idList = ref([]);
|
||||
const handleSelectionChange = (rows) => {
|
||||
idList.value.length = 0;
|
||||
if (rows.length > 0) {
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
idList.value.push(rows[i].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.doc-writing {
|
||||
width: 100%;
|
||||
padding: 20px 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.doc-list {
|
||||
width: calc(100% - 100px);
|
||||
height: 100%;
|
||||
margin-top: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top img {
|
||||
width: 16px;
|
||||
height: 13.8px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .doc-list-title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .doc-list-operate {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .batch-delete {
|
||||
height: 32px;
|
||||
width: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
cursor: pointer;
|
||||
background: #FFFFFF;
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
border: 1px solid #004EA0;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .batch-delete img {
|
||||
margin-right: 5px;
|
||||
width: 16px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .batch-delete .batch-del {
|
||||
color: #004EA0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .doc-list-operate .doc-search {
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .doc-list-operate .doc-import {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
margin-left: 16px;
|
||||
background: #004EA0;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #FAFBFF;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-top .doc-list-operate .doc-import img {
|
||||
width: 9px;
|
||||
height: 11px;
|
||||
margin-right: 8px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-empty {
|
||||
height: 235px;
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-empty img {
|
||||
height: 44px;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
.doc-list .doc-list-empty .empty-tips .tips-top {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/*.doc-list .doc-list-empty .empty-tips .tips-top span {
|
||||
color: #004EA0;
|
||||
}*/
|
||||
|
||||
.doc-list .doc-list-empty .empty-tips .tips-bottom {
|
||||
font-size: 14px;
|
||||
color: #858A94;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.doc-list-non-empty {
|
||||
height: calc(100% - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.doc-list-non-empty .doc-file-title {
|
||||
font-size: 14px;
|
||||
color: #004EA0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.doc-list-non-empty .doc-operate {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.doc-list-non-empty .doc-operate img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.upload-btn {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
|
||||
.file-name {
|
||||
width: 434px;
|
||||
font-size: 14px;
|
||||
color: #606771;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user