yangdr 1 ماه پیش
والد
کامیت
16639f34e2

+ 7 - 0
package-lock.json

@@ -16,6 +16,7 @@
         "@vue-office/pptx": "^1.0.1",
         "animate.css": "^4.1.1",
         "axios": "^1.8.4",
+        "crypto-js": "^4.2.0",
         "date-fns": "^4.1.0",
         "element-plus": "^2.9.6",
         "file-saver": "^2.0.5",
@@ -3227,6 +3228,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://mirrors.huaweicloud.com/repository/npm/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
     "node_modules/csstype": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "@vue-office/pptx": "^1.0.1",
     "animate.css": "^4.1.1",
     "axios": "^1.8.4",
+    "crypto-js": "^4.2.0",
     "date-fns": "^4.1.0",
     "element-plus": "^2.9.6",
     "file-saver": "^2.0.5",

+ 1 - 1
src/api/AgentApi.ts

@@ -265,7 +265,7 @@ export function getFileContent(url: string): Promise<StandardResponse> {
 }
 
 export function getKnowledgeBase(params: any = { name: "", pageNo: 1, pageSize: 10 }): Promise<StandardResponse> {
-    return serverGetRequest(`/open-platform/knowledge-base?name=${params.name}&pageNo=${params.pageNo}&pageSize=${params.pageSize}`)
+    return serverGetRequest(`/open-platform/knowledge-base/?name=${params.name}&pageNo=${params.pageNo}&pageSize=${params.pageSize}`)
 }
 
 export function getKnowledgeBaseFilesList(params: any): Promise<StandardResponse> {

+ 7 - 5
src/components/CreateKBFileDialog.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="add-kb-file-dialog">
-    <el-dialog v-model="dialogVisible" title="知识库" width="1200px" align-center :show-close="false"
+    <el-dialog v-model="dialogVisible" title="知识库" width="1000px" align-center :show-close="false"
       @closed="() => { emit('update:modelValue', false) }">
       <el-form ref="formRef" style="max-width: 100%" :model="formData" label-width="auto" class="demo-dynamic">
         <el-form-item prop="name" label="知识库名称:">
@@ -10,7 +10,7 @@
           {{ formData.description }}
         </el-form-item>
 
-        <el-form-item prop="" label="导入方式:">
+        <el-form-item v-if="false" prop="" label="导入方式:">
           <div class="import-method">
             <el-radio-group v-model="formData.importMethod">
               <el-radio-button label="按文件类型导入" value="file" />
@@ -18,7 +18,7 @@
             </el-radio-group>
           </div>
         </el-form-item>
-        <el-form-item prop="" label="选择文件类型:" required>
+        <el-form-item v-if="false" prop="" label="选择文件类型:" required>
           <div class="file-type">
             <div :class="formData.fileType === 'document' ? 'file-type-box active' : 'file-type-box'"
               @click="handleSelectFileType('document')">
@@ -41,13 +41,14 @@
             </div>
           </div>
         </el-form-item>
-        <el-form-item prop="fileList" label="上传:" required>
+        <el-form-item prop="fileList" label="上传文档:" required>
           <div class="file-upload">
             <el-upload ref="fileUploadRef" v-model:file-list="formData.fileList" class="upload-demo" :limit="100"
+              :headers="{ Authorization: 'Beaver ' + getSessionVar('username') + ' ' + getSessionVar('session_id') }"
               :accept="formData.accept" drag :on-exceed="handleExceed" :on-preview="handlePreview"
               :before-upload="handleBeforeUpload" :on-error="handleUploadError" :on-remove="handleRemove"
               :before-remove="beforeRemove" :on-success="handleSuccess"
-              :action="api.knowledgeBase + `/${formData.id}/files/`" multiple name="files">
+              :action="api.knowledgeBase + `${formData.id}/files/`" multiple name="files">
               <el-icon size="64" color="#2468F2"><upload-filled /></el-icon>
               <div class="el-upload__text">
                 <p>将文档拖到此处,或<i style="color:#2468F2">点击上传</i></p>
@@ -134,6 +135,7 @@ import { cloneDeep } from "lodash"
 import { ref, watch, getCurrentInstance, toRaw } from "vue"
 import { api } from "@/utils/config"
 import { ElMessage } from 'element-plus'
+import { getSessionVar, deleteSessionVar } from "@/utils/session";
 const { proxy } = getCurrentInstance()
 
 const props = defineProps({ modelValue: Boolean, knowledgeBase: Object })

+ 230 - 0
src/components/EditKBFileDialog.vue

@@ -0,0 +1,230 @@
+<template>
+  <div class="add-kb-file-dialog">
+    <el-dialog v-model="dialogVisible" title="文件修改" width="1000px" align-center :show-close="false"
+      @closed="() => { emit('update:modelValue', false) }">
+      <el-form ref="formRef" style="max-width: 100%" :model="formData" label-width="auto" class="demo-dynamic">
+        <el-form-item prop="fileTableData" v-show="formData.fileTableData.length > 0">
+          <div class="file-table">
+            <el-table :data="formData.fileTableData" border table-layout="fixed" height="400"
+              style="max-width: 100%;box-sizing: border-box;min-width: 0px;">
+              <el-table-column label="导入文件标题" prop="file_name">
+                <template #default="{ row, $index }">
+                  <p :contenteditable="true" class="input-box" @blur="updateInputBox($event, $index, 'file_name')">{{
+                    row.file_name }}</p>
+                </template>
+              </el-table-column>
+              <el-table-column label="知识类型" prop="knowledge_type" width="210">
+                <template #default="{ row }">
+                  <!-- <p>{{ row.knowledge_type }}</p> -->
+                  <el-select v-model="row.knowledge_type" placeholder="Select" style="width: 180px">
+                    <el-option v-for="item in formData.knowledgeTypeOptions" :key="item.value" :label="item.label"
+                      :value="item.value" />
+                  </el-select>
+                </template>
+              </el-table-column>
+              <el-table-column label="版本号" prop="version">
+                <template #default="{ row, $index }">
+                  <p :contenteditable="true" class="input-box" @blur="updateInputBox($event, $index, 'version')">{{
+                    row.version }}</p>
+                </template>
+              </el-table-column>
+              <el-table-column label="作者(主编)" prop="author">
+                <template #default="{ row, $index }">
+                  <p :contenteditable="true" class="input-box" @blur="updateInputBox($event, $index, 'author')">{{
+                    row.author }}</p>
+                </template>
+              </el-table-column>
+
+              <el-table-column label="年份" prop="year" width="80">
+                <template #default="{ row, $index }">
+                  <p :contenteditable="true" class="input-box" @blur="updateInputBox($event, $index, 'year', 'number')">
+                    {{ row.year }}
+                  </p>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-form-item>
+
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" :disabled="formData.fileTableData.length === 0" @click="handleConfirm">
+            确认
+          </el-button>
+          <el-button @click="handleCancel">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { cloneDeep } from "lodash"
+import { ref, watch, getCurrentInstance, toRaw } from "vue"
+import { api } from "@/utils/config"
+import { ElMessage } from 'element-plus'
+const { proxy } = getCurrentInstance()
+
+const props = defineProps({ modelValue: Boolean, fileTable: Array })
+const emit = defineEmits(['update:modelValue', 'updateFiles'])
+const formData = ref({
+  id: null,
+  fileTableData: [],
+  knowledgeTypeOptions: [
+    { value: "中华医学会诊疗指南", label: "中华医学会诊疗指南" },
+    { value: "规培十四五教材", label: "规培十四五教材" },
+    { value: "临床路径", label: "临床路径" },
+  ]
+})
+let dialogVisible = ref(false)
+const handleCancel = () => {
+  emit('update:modelValue', false)
+}
+
+const handleConfirm = () => {
+  updateFiles()
+}
+watch(() => props.modelValue, (newVal) => {
+  dialogVisible.value = newVal
+}, { immediate: true })
+watch(() => props.fileTable, (newVal) => {
+  formData.value.fileTableData = cloneDeep(newVal) || []
+}, { immediate: true, deep: true })
+
+const updateFiles = async () => {
+  try {
+    const data = await proxy.$http.put(api.batchUpdate, {
+      files: formData.value.fileTableData
+    })
+    formData.value.fileTableData = []
+    emit('updateFiles')
+    emit('update:modelValue', false)
+  } catch (e) {
+    console.log(e)
+  }
+}
+
+function updateInputBox(e, index, key, valueType = 'string') {
+  let value = e.target.innerText || ""
+  if (valueType === 'number') {
+    value = Number(value)
+  }
+  formData.value.fileTableData[index][key] = value
+}
+
+</script>
+
+<style lang="less" scoped>
+.add-kb-file-dialog {
+  /deep/ .el-dialog {
+    label {
+      margin: 20px 0px 10px;
+      display: block;
+    }
+
+    .dialog-footer {
+      text-align: center;
+      // gap: 40px;
+
+      .el-button {
+        padding: 19px 50px;
+        margin-left: 50px;
+
+        &:first-child {
+          margin-left: 0px;
+        }
+      }
+    }
+  }
+
+  /deep/ .el-form {
+    .input-box {
+      &:focus {
+        outline: none;
+      }
+    }
+
+    .el-textarea__inner,
+    .el-input__wrapper {
+      background-color: #F1F3F6;
+    }
+
+    .import-method {
+      .el-radio-button {
+        // margin: 10px 0px;
+        padding: 2px;
+        background: #E3E8F0;
+
+        &:is(.is-active) {
+          .el-radio-button__inner {
+            background-color: white;
+            color: #306FF3;
+          }
+        }
+
+        &:first-child {
+          border-radius: 10px 0px 0px 10px;
+        }
+
+        &:last-child {
+          border-radius: 0px 10px 10px 0px;
+        }
+
+        .el-radio-button__inner {
+          color: #859299;
+          border-radius: 10px;
+          background-color: transparent;
+          border: none;
+          padding: 10px 30px;
+          box-shadow: none;
+        }
+      }
+    }
+
+    .file-type {
+      display: flex;
+      gap: 10px;
+
+      .file-type-box {
+        width: 250px;
+        border: 1px solid #EDEDEF;
+        padding: 10px;
+        border-radius: 10px;
+
+        &:is(.active) {
+          border-color: #4B73F2;
+          background-color: #EEF3FE;
+
+          .title {
+            .name {
+              color: #4B73F2;
+            }
+
+
+          }
+        }
+
+        .title {
+          display: flex;
+          align-items: center;
+
+          .name {
+            color: #000;
+          }
+        }
+      }
+    }
+
+    .file-table {
+      flex: 1 1 auto;
+    }
+  }
+
+  .moxing1-icon {
+    width: 24px;
+    height: 24px;
+  }
+}
+</style>

+ 103 - 0
src/components/EditPasswordDialog.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-dialog v-model="visible" width="400" title="修改密码" class="password-dialog"
+    @closed="emit('update:modelValue', false)">
+    <el-form :model="formData" status-icon :rules="rules" ref="ruleFormRef" class="demo-ruleForm">
+      <!-- <el-form-item label="" prop="username">
+      <el-input type="username" v-model.trim="formData.username" autocomplete="off" placeholder="请输入账号">
+        <template #prefix>
+          <i class="icon-username"></i>
+        </template>
+</el-input>
+</el-form-item> -->
+      <el-form-item label="旧密码" prop="oldPassword">
+        <el-input type="text" v-model.trim="formData.oldPassword" placeholder="请输入旧密码">
+          <template #prefix><el-icon>
+              <Lock />
+            </el-icon></template>
+        </el-input>
+      </el-form-item>
+      <el-form-item label="新密码" prop="newPassword">
+        <el-input type="password" v-model.trim="formData.newPassword" placeholder="请输入新密码">
+          <template #prefix><el-icon>
+              <Lock />
+            </el-icon></template>
+        </el-input>
+      </el-form-item>
+      <el-form-item label="新密码" prop="reNewPassword">
+        <el-input type="password" v-model.trim="formData.reNewPassword" placeholder="请再次输入新密码">
+          <template #prefix><el-icon>
+              <Lock />
+            </el-icon></template>
+        </el-input>
+      </el-form-item>
+
+      <!-- <el-form-item label="" prop="captcha" class="captcha">
+      <el-input class="input-captcha" v-model.trim="formData.captcha" placeholder="请输入验证码"></el-input>
+      <img src="@/assets/images/组11拷贝2x.png" alt="验证码" />
+    </el-form-item> -->
+      <el-form-item>
+        <footer class="form-footer">
+          <el-button @click="onCancel"><span>取消</span></el-button>
+          <el-button type="primary" @click="onSubmit" class="submit"><span>提交</span></el-button>
+        </footer>
+        <!-- <div class="other clearfix">
+        <span class="forgot-password">忘记密码?</span>
+        <span class="sign-up">立即注册</span>
+        <span class="no-account">还没账号?</span>
+      </div> -->
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+
+</template>
+
+
+<script setup>
+import { ref, watch } from 'vue'
+
+let visible = ref(true)
+const ruleFormRef = ref()
+let formData = ref({
+  oldPassword: "",
+  newPassword: "",
+  reNewPassword: ""
+})
+const props = defineProps({ show: Boolean })
+const emit = defineEmits(['update:modelValue'])
+const validatePass = (rule, value, callback) => {
+  if (value !== formData.value.newPassword) {
+    callback(new Error('输入的密码不同'))
+  } else {
+    callback()
+  }
+}
+const rules = {
+  username: [{ required: true, message: "账号是必填", trigger: ["change", "blur"] }],
+  oldPassword: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
+  newPassword: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
+  reNewPassword: [{ required: true, message: "必填", trigger: ["change", "blur"] },
+  { validator: validatePass, trigger: 'change' }
+  ],
+};
+watch(() => props.show, (newVal) => {
+  visible.value = newVal
+})
+function onCancel() {
+  emit('update:modelValue', false)
+}
+function onSubmit() {
+  ruleFormRef.value.validate((valid) => {
+    if (valid) {
+
+    }
+  })
+}
+</script>
+
+
+<style scoped lang="less">
+.form-footer {
+  text-align: right;
+  margin-left: auto;
+}
+</style>

+ 10 - 9
src/components/FileViewer/FileViewer.vue

@@ -1,14 +1,15 @@
 <template>
   <div class="document-viewer">
     <div class="mask"></div>
-    <div class="top">
-      <span class="close">
-        <el-icon @click="emit('closeViewer')">
-          <Close />
-        </el-icon>
-      </span>
-    </div>
     <div ref="containerRef" class="container" :style="{ width: props.width + 'px' }">
+      <div class="top">
+        <span v-show="props.fileName">{{ props.fileName }}</span>
+        <span class="close">
+          <el-icon @click="emit('closeViewer')">
+            <Close />
+          </el-icon>
+        </span>
+      </div>
       <VueOfficeDocx v-if="props.fileType === 'docx'" :src="props.fileUrl" style="height: auto;"
         @rendered="renderedHandler" @error="errorHandler" />
       <!-- <VueOfficePdf v-else-if="props.fileType === 'pdf'" :src="props.fileUrl" style="height: auto;"
@@ -40,7 +41,7 @@ import VueOfficeExcel from '@vue-office/excel'
 import MarkdownViewer from './MarkdownViewer.vue'
 import TextViewer from './TextViewer.vue'
 import PdfViewer from './PdfViewer.vue'
-const props = defineProps({ fileType: String, fileUrl: String, width: { type: Number, default: 1000 } })
+const props = defineProps({ fileType: String, fileUrl: String, fileName: String, width: { type: Number, default: 1000 } })
 const emit = defineEmits(['closeViewer'])
 const containerRef = ref() //.containerd的ref
 let containerSize = ref({
@@ -147,7 +148,7 @@ onBeforeUnmount(() => {
 
   .container {
     // opacity: 1;
-    min-height: calc(100% - 28px);
+    min-height: 100%;
     // height: 100%;
     // height: 10000px;
     // flex: 1 1 auto;

+ 4 - 2
src/components/LayoutHeader.vue

@@ -15,13 +15,14 @@
         <template #dropdown>
           <el-dropdown-menu>
             <el-dropdown-item>用户资料</el-dropdown-item>
-            <el-dropdown-item>修改密码</el-dropdown-item>
+            <el-dropdown-item @click="editPassShow = true">修改密码</el-dropdown-item>
             <el-dropdown-item @click="handleLogout">登出</el-dropdown-item>
           </el-dropdown-menu>
         </template>
       </el-dropdown>
     </div>
   </div>
+  <EditPasswordDialog v-model="editPassShow" />
 </template>
 
 <script setup>
@@ -30,7 +31,7 @@ import { computed } from "vue";
 import { useMenuStore } from "@/stores/menu.js"
 import { useRoute, useRouter } from "vue-router";
 import { getSessionVar, clearSessionVar } from '@/utils/session'
-
+import EditPasswordDialog from "@/components/EditPasswordDialog.vue"
 const route = useRoute()
 const router = useRouter()
 // console.log('route', route)
@@ -45,6 +46,7 @@ user.value = {
   full_name: getSessionVar('full_name') || 'John Doe',
   username: getSessionVar('username') || 'johndoe',
 }
+let editPassShow = ref(false)
 const currentPath = computed(() => {
   let temp = ""
   for (let i = 0; i < routeList.length; i++) {

+ 119 - 31
src/dialogs/OCRDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <el-dialog :title="title" v-model="dialogFormVisible" @closed="handleClosed">
-    <el-form :model="form">
-      <el-form-item label="任务名称" :label-width="formLabelWidth">
+    <el-form :model="form" ref="formRef">
+      <el-form-item label="任务名称" prop="name" :label-width="formLabelWidth" required>
         <el-input v-model="form.name" autocomplete="off"></el-input>
       </el-form-item>
       <el-form-item label="任务状态" :label-width="formLabelWidth">
@@ -57,12 +57,12 @@
                 <el-input v-model="knowledgeBase.querySearch" size="large" placeholder="搜索"
                   @keydown.enter="debounceGetKBfileList" :prefix-icon="Search" />
               </span>
-              <span class="add-file" @click="handleImportFiles(toRaw(knowledgeBase.filesList))">
-                <span class="text">全部导入</span>
+              <span class="add-file" @click="handleSelectedImport()">
+                <span class="text">批量导入</span>
               </span>
             </div>
             <el-scrollbar class="management-content-middle">
-              <el-table :data="knowledgeBase.filesList">
+              <el-table :data="knowledgeBase.filesList" ref="KBTableRef">
                 <el-table-column type="selection" width="30" />
                 <el-table-column label="#" prop="index" width="50" />
                 <el-table-column prop="file_name" min-width="150" label="标题">
@@ -123,14 +123,17 @@
 </template>
 
 <script setup lang="ts">
-import { ref, watch, toRaw, onMounted, computed } from 'vue'
+import { ref, watch, toRaw, onMounted, computed, getCurrentInstance } from 'vue'
 import { Search } from '@element-plus/icons-vue'
+import CryptoJS from 'crypto-js';
+import encHex from 'crypto-js/enc-hex'
 import { createJob, putQueueJob, getKnowledgeBase, getKnowledgeBaseFilesList } from '@/api/AgentApi'
 import axios from 'axios'
 import { genFileId, ElMessage } from 'element-plus'
 import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
 import { throttle, debounce } from 'lodash'
-
+const KBTableRef = ref()
+const formRef = ref()
 const dialogFormVisible = ref(false)
 const formLabelWidth = ref('120px')
 
@@ -254,16 +257,17 @@ function fetchFile(fileName: string, fileUrl: string) {
       const rawFile = new File([response.data], fileName, { type: response.data.type });
       const file = rawFile as UploadRawFile
       file.uid = genFileId()
-      const fileHashHex = await calculateFileHash(file)
+      // const fileHashHex = await calculateFileHashForCryptoJS(file)
+      // const { md5, sha256 } = await hashFile(file)
       let fileExist = false;
-      await Promise.allSettled(fileList.value.map(async (it: any) => {
-        const itFileHashHex = await calculateFileHash(it.raw);
-        if (itFileHashHex === fileHashHex) {
-          fileExist = true;
-          return true;
-        }
-        return false;
-      }))
+      // await Promise.allSettled(fileList.value.map(async (it: any) => {
+      //   const itFileHash = await hashFile(it.raw);
+      //   if (sha256 === itFileHash.sha256) {
+      //     fileExist = true;
+      //     return true;
+      //   }
+      //   return false;
+      // }))
       if (fileExist) {
         ElMessage({
           message: `文件“${fileName}”已存在上传列表中`,
@@ -288,15 +292,50 @@ function fetchFile(fileName: string, fileUrl: string) {
       console.log(error);
     });
 }
-// 计算文件的hash值
-async function calculateFileHash(file: File) {
+function handleSelectedImport() {
+  const SelectionRows = KBTableRef.value.getSelectionRows()
+  handleImportFiles(toRaw(SelectionRows))
+  // console.log(SelectionRows)
+}
+// 使用原生方法计算文件的hash值,该方法兼容性较差
+async function calculateFileHashForNative(file: File) {
   const arrayBuffer = await file.arrayBuffer(); // 将文件读取为 ArrayBuffer
   const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer); // 计算哈希值
   const hashArray = Array.from(new Uint8Array(hashBuffer)); // 将哈希转换为字节数组
   const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); // 转换为十六进制字符串
+  // console.log("calculateFileHashForNative1:", hashHex)
   return hashHex;
 }
 
+// 使用crypto-js计算文件的hash值 ,该方法易发生卡顿
+async function calculateFileHashForCryptoJS(file: File) {
+  return new Promise((resolve, reject) => {
+    try {
+      const reader = new FileReader();
+      reader.readAsArrayBuffer(file)
+      reader.onload = function (e) {
+        const arrayBuffer = e.target?.result;  // 读取的文件内容
+        // 将 ArrayBuffer 转换为 CryptoJS 可以处理的 WordArray
+        const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer);
+        // 计算文件的 SHA-256 哈希值并转为 Base64 编码字符串
+        const hashBase64 = CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.Base64);
+        // 计算文件的 SHA-256 哈希值并转为 16 进制字符串
+        const hashHex = CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.hex);
+        // console.log('calculateFileHashForCryptoJS:', hashHex);
+        resolve(hashHex)
+      };
+
+      reader.onerror = () => {
+        reject('')
+      }
+    } catch (e) {
+      console.log(e)
+      reject('')
+    }
+  })
+}
+
+
 function handleImportFiles(filesList: any[]) {
   for (let i = 0; i < filesList.length; i++) {
     // if (i > 0) break;
@@ -304,6 +343,51 @@ function handleImportFiles(filesList: any[]) {
   }
 }
 
+/**
+ * 用于计算文件的hash值,包括sha256值和md5值
+ */
+function hashFile(file: any) {
+  /**
+   * 使用指定的算法计算hash值
+   */
+  function hashFileInternal(file: any, alog: any) {
+    // 指定块的大小,这里设置为20MB,可以根据实际情况进行配置
+    const chunkSize = 20 * 1024 * 1024
+    let promise: any = Promise.resolve()
+    // 使用promise来串联hash计算的顺序。因为FileReader是在事件中处理文件内容的,必须要通过某种机制来保证update的顺序是文件正确的顺序
+    for (let index = 0; index < file.size; index += chunkSize) {
+      promise = promise.then(() => hashBlob(file.slice(index, index + chunkSize)))
+    }
+
+    /**
+     * 更新文件块的hash值
+     */
+    function hashBlob(blob: any) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = ({ target }) => {
+          const wordArray = CryptoJS.lib.WordArray.create(target?.result)
+          // 增量更新计算结果
+          alog.update(wordArray)
+          resolve(true)
+        }
+        reader.readAsArrayBuffer(blob)
+      })
+    }
+
+    // 使用promise返回最终的计算结果
+    return promise.then(() => encHex.stringify(alog.finalize()))
+  }
+
+  // 同时计算文件的sha256和md5,并使用promise返回
+  return Promise.all([hashFileInternal(file, CryptoJS.algo.SHA256.create()),
+  hashFileInternal(file, CryptoJS.algo.MD5.create())])
+    .then(([sha256, md5]) => ({
+      sha256,
+      md5
+    }))
+}
+
 const handlePreview = (file: any) => {
   console.log(file)
 }
@@ -326,19 +410,23 @@ const handleSuccess = (response: any, file: any, fileList: any) => {
 }
 
 const handleConfirm = () => {
-  // console.log('submit', props)
-  form.value.job_category = props.queue_category + "_" + props.queue_name;
-  form.value.job_name = form.value.name;
-  form.value.queue_category = props.queue_category;
-  form.value.queue_name = props.queue_name;
-  form.value.job_details = props.job_details;
-  form.value.job_creator = props.job_creator;
-
-  // console.log('form values ', form.value)
-  createJob(form.value).then((res: any) => {
-    var job = res.records[0]
-    form.value.job_id = job.id;
-    upload.value.submit();
+  formRef.value.validate((valid: any) => {
+    if (valid) {
+      // console.log('submit', props)
+      form.value.job_category = props.queue_category + "_" + props.queue_name;
+      form.value.job_name = form.value.name;
+      form.value.queue_category = props.queue_category;
+      form.value.queue_name = props.queue_name;
+      form.value.job_details = props.job_details;
+      form.value.job_creator = props.job_creator;
+
+      // console.log('form values ', form.value)
+      createJob(form.value).then((res: any) => {
+        var job = res.records[0]
+        form.value.job_id = job.id;
+        upload.value.submit();
+      })
+    }
   })
 }
 defineExpose({ showDialog })

+ 3 - 4
src/router/index.ts

@@ -32,7 +32,6 @@ const router = createRouter({
 
       }
     },
-
     {
       path: "/kmplatform",
       name: "kmplatform",
@@ -110,19 +109,19 @@ const router = createRouter({
                 {
                   path: "graph",
                   name: "graph",
-                  component: () => import("../views/GraphView.vue"),
+                  component: () => import("@/views/GraphView.vue"),
                 },
                 {
                   path: "graph-mgr/:id",
                   name: "graph-mgr",
-                  component: () => import("../views/GraphManagement.vue"),
+                  component: () => import("@/views/GraphManagement.vue"),
                 },
               ],
             },
             {
               path: "about",
               name: "about",
-              component: () => import("../views/AboutView.vue"),
+              component: () => import("@/views/AboutView.vue"),
             },
           ]
         },

+ 2 - 2
src/utils/config.js

@@ -1,6 +1,6 @@
 export const api = {
-  knowledgeBase: "/open-platform/knowledge-base",
-  files: "/open-platform/files",
+  knowledgeBase: "/open-platform/knowledge-base/",
+  files: "/open-platform/files/",
   batchUpdate: '/open-platform/files/batch-update'
 }
 

+ 7 - 0
src/utils/hash_worker.js

@@ -0,0 +1,7 @@
+import CryptoJS from 'crypto-js';
+self.onmessage = function (e) {
+  const arrayBuffer = e.data;
+  const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer);
+  const hashHex = CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.hex);
+  postMessage(hashHex);  // 将计算结果返回给主线程
+};

+ 3 - 1
src/utils/js-modules.d.ts

@@ -1 +1,3 @@
-declare module 'jquery'
+declare module 'jquery'
+declare module 'crypto-js'
+declare module 'crypto-js/enc-hex'

+ 1 - 1
src/views/KMPlatform/Home/Home.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="home">
     <div class="container">
-      <h1>磐医学知识图谱构建流程:</h1>
+      <!-- <h1>磐医学知识图谱构建流程:</h1> -->
       <div class="process">
         <div class="box-item">
           <div class="box-rect" v-for="(item, idx) in process">

+ 21 - 9
src/views/KMPlatform/KnowledgeBase/KBM/KnowledgeBaseManagement.vue

@@ -37,7 +37,9 @@
             </span>
             <span class="text">
               <div class="title" @click="toKMById(item.id)">{{ item.name }}</div>
-              <div class="remark">{{ item.tags }}</div>
+              <div class="remark">
+                <el-tag type="info" effect="plain" hit>{{ item.tags }}</el-tag>
+              </div>
             </span>
           </div>
           <div class="middle">
@@ -46,9 +48,12 @@
             </el-text>
           </div>
           <div class="bottom">
-            <span class="add-label" @click="handleAddKBTags(index)">
+            <span class="add-label">
               <i class="label-icon"></i>
               <i class="label-text">添加标签</i>
+              <i class="circle-plus" @click="handleAddKBTags(index)"> <el-icon>
+                  <CirclePlus />
+                </el-icon></i>
             </span>
 
             <el-popover class="box-item" popper-class="grid-arrange-operation" title="Title"
@@ -101,14 +106,14 @@
               <el-button link type="primary" @click="handleEditKB(row)">
                 编辑
               </el-button>
-              <el-button link type="primary" @click="handleDeleteKB(row.id)">删除</el-button>
+              <el-button link type="danger" @click="handleDeleteKB(row.id)">删除</el-button>
             </template>
           </el-table-column>
         </el-table>
       </div>
     </main>
     <footer>
-      <div class="pagination" v-if="totalPage > 1">
+      <div class="pagination" v-if="totalPage > 1 || paginationData.defaultPageSize !== paginationData.currentPageSize">
         <el-pagination :current-page="paginationData.currentPage" :page-sizes="paginationData.pageSizes"
           :disabled="paginationData.disabled" :default-page-size="paginationData.defaultPageSize"
           layout="total, sizes, prev, pager, next, jumper" :total="paginationData.total" @size-change="handleSizeChange"
@@ -130,7 +135,7 @@
       <el-form :model="formData" label-width="auto" :rules="rules" ref="formRef" style="max-width: 700px">
         <el-form-item label="知识库名称:" label-width="150" prop="name" required label-position="left">
           <el-input maxlength="50" v-model.trim="formData.name" show-word-limit />
-          <div class="name-tip">仅支持中文、英文、数字、下划线()、中划线(-)、英文点(.)</div>
+          <div class="name-tip">仅支持中文、英文、数字、下划线(_)、中划线(-)、英文点(.)</div>
         </el-form-item>
 
 
@@ -140,7 +145,7 @@
         </el-form-item>
         <div style="margin-bottom: 20px;"></div>
         <el-form-item label="知识库标签:" prop="tags" label-width="150" label-position="left">
-          <el-input type="text" v-model="formData.tags" />
+          <el-input-tag type="text" placeholder="按Enter回车键添加输入内容为标签" v-model="formData.tags" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -152,8 +157,8 @@
         </div>
       </template>
     </el-dialog>
-    <EditKBDialog v-model="editKBDialogData.show" :knowledgeBase="editKBDialogData.kb" @updateKB="handleUpdateKB">
-    </EditKBDialog>
+    <EditKBDialog v-model="editKBDialogData.show" :knowledgeBase="editKBDialogData.kb" @updateKB="handleUpdateKB" />
+
   </div>
 </template>
 
@@ -508,11 +513,18 @@ onMounted(() => {
           .add-label {
             display: flex;
             align-items: center;
+            gap: 0px 5px;
+
+            .circle-plus {
+              &:hover {
+                color: #409EFF;
+              }
+            }
 
             .label-icon {
               width: 20px;
               height: 20px;
-              margin-right: 10px;
+              // margin-right: 10px;
             }
 
             .label-text {

+ 68 - 20
src/views/KMPlatform/KnowledgeBase/KM/KnowledgeManagement.vue

@@ -16,12 +16,15 @@
             <el-input v-model="querySearch" size="large" placeholder="搜索" @keydown.enter="getFilesList"
               :prefix-icon="Search" />
           </div>
+
           <span @click="addFileVisible = true" class="add-file"><el-icon color="#fff">
               <Plus />
             </el-icon> <i class="text">添加文件</i></span>
+          <span class="add-file" style="margin-right: 10px;" @click="handleBatchEditFiles"><i class="text"
+              style="margin: 0px;">批量修改文件</i></span>
         </div>
         <div class="management-content-middle">
-          <el-table :data="filesList" size="large">
+          <el-table :data="filesList" ref="fileTableRef" size="large">
             <el-table-column type="selection" width="30" />
             <el-table-column label="#" prop="index" width="60" />
             <el-table-column prop="file_name" min-width="150" label="标题">
@@ -39,37 +42,43 @@
             <el-table-column prop="knowledge_type" label="知识类型" />
             <el-table-column prop="version" label="版本" />
             <el-table-column prop="author" label="作者(主编)" />
-            <el-table-column prop="year" label="年份" />
+            <el-table-column prop="year" label="年份" width="70" />
             <el-table-column prop="creator" label="上传人" />
-            <el-table-column prop="created_at" label="上传时间" min-width="155" />
+            <el-table-column prop="created_at" label="上传时间" width="170" />
             <el-table-column prop="" label="状态" width="90">
               <template #default="{ row }">
-                <span v-if="row.isValid">
+                <span v-if="row.status">
                   <i class="circle" type="success"></i>
                   <el-text type="success">可用</el-text>
                 </span>
-                <span v-else>
+                <!-- <span v-else>
                   <i class="circle" type="warning"></i>
                   <el-text type="warning">异常</el-text>
+                </span> -->
+                <span v-else>
+                  <i class="circle" type="danger"></i>
+                  <el-text type="danger">禁用</el-text>
                 </span>
               </template>
             </el-table-column>
             <el-table-column prop="created_at" label="下载" width="70">
               <template #default="{ row }">
-                <i class="document-download-icon" @click="saveAs(row.minio_url, row.file_name)"></i>
+                <i class="document-download-icon" :disabled="row.status ? true : false"
+                  @click="handleDownloadFile(row.minio_url, row.file_name, row.status)"></i>
               </template>
             </el-table-column>
-            <el-table-column fixed="right" label="操作" width="120">
+            <el-table-column fixed="right" label="操作" width="150">
               <template #default="{ row }">
                 <div class="operation">
-                  <el-switch v-model="switchValue" />
+                  <el-switch v-model="row.status" />
                   <!-- <el-icon size="24">
                     <Operation />
                   </el-icon>
                   <el-icon size="24">
                     <MoreFilled />
                   </el-icon> -->
-                  <el-button link type="primary" @click="handleDeleteFile(row.id)">删除</el-button>
+                  <el-button link type="primary" @click="handleEditFile(toRaw([row]))">修改</el-button>
+                  <el-button link type="danger" @click="handleDeleteFile(row.id)">删除</el-button>
                 </div>
               </template>
             </el-table-column>
@@ -78,7 +87,7 @@
       </div>
     </main>
     <footer>
-      <div class="pagination" v-if="totalPage > 1">
+      <div class="pagination" v-if="totalPage > 1 || paginationData.defaultPageSize !== paginationData.currentPageSize">
         <el-pagination :current-page="paginationData.currentPage" :page-sizes="paginationData.pageSizes"
           :disabled="paginationData.disabled" :default-page-size="paginationData.defaultPageSize"
           layout="total, sizes, prev, pager, next, jumper" :total="paginationData.total" @size-change="handleSizeChange"
@@ -86,8 +95,10 @@
       </div>
     </footer>
     <CreateKBFileDialog v-model="addFileVisible" :knowledgeBase="KBData" @updateFiles="getFilesList()" />
-    <FileViewer v-if="viewFileData.show" :fileType="viewFileData.type" :fileUrl="viewFileData.url"
-      :width="viewFileData.width" @closeViewer="() => viewFileData.show = false" />
+    <FileViewer v-if="viewFileData.show" :fileType="viewFileData.type" :fileName="viewFileData.name"
+      :fileUrl="viewFileData.url" :width="viewFileData.width" @closeViewer="() => viewFileData.show = false" />
+    <EditKBFileDialog v-model="editKBFileData.show" :fileTable="editKBFileData.data"
+      @updateFiles="() => { getFilesList() }" />
   </div>
 
 </template>
@@ -95,15 +106,15 @@
 <script setup>
 import { saveAs } from 'file-saver'
 import { ElMessage } from 'element-plus'
-import { ref, getCurrentInstance, computed, watch } from 'vue'
-import { Search } from '@element-plus/icons-vue'
+import { ref, getCurrentInstance, computed, watch, toRaw } from 'vue'
+import { Download, Search } from '@element-plus/icons-vue'
 import { useRoute, useRouter } from 'vue-router';
 import axios from 'axios';
 import { api } from '@/utils/config';
 import { confirmDelete } from "@/utils/confirmation"
 import CreateKBFileDialog from '@/components/CreateKBFileDialog.vue';
 import FileViewer from "@/components/FileViewer/FileViewer.vue"
-
+import EditKBFileDialog from "@/components/EditKBFileDialog.vue"
 const { proxy } = getCurrentInstance()
 const route = useRoute()
 const router = useRouter()
@@ -111,13 +122,18 @@ const kbId = route.params.kbId
 let filesList = ref([])
 let KBData = ref({})
 let addFileVisible = ref(false)
-let switchValue = ref(true)
+
 let querySearch = ref("")
+let editKBFileData = ref({
+  show: false,
+  data: []
+})
 let viewFileData = ref({
   url: "",
+  name: "",
   type: "",
   show: false,
-  width: 1000
+  width: 1000,
 })
 const paginationData = ref({
   currentPage: 1,
@@ -146,12 +162,30 @@ async function checkLinkValidity(index, url) {
     filesList.value[index].isValid = false // 请求失败,返回 false
   }
 }
+
+function handleBatchEditFiles() {
+  const fileTableRef = proxy.$refs['fileTableRef']
+  const selectionRows = fileTableRef.getSelectionRows()
+  if (selectionRows.length == 0) return;
+  handleEditFile(toRaw(selectionRows))
+}
+
+function handleEditFile(data) {
+  editKBFileData.value.show = true;
+  editKBFileData.value.data = data;
+}
+
 const handleViewFile = (url, type) => {
   viewFileData.value.show = true;
   viewFileData.value.url = url;
   viewFileData.value.type = type
   console.log(viewFileData.value)
 }
+function handleDownloadFile(file_url, file_name, status = true) {
+  if (status) {
+    saveAs(file_url, file_name)
+  }
+}
 const handleCurrentChange = (page) => {
   paginationData.value.currentPage = page
   // console.log('handleCurrentChange', page)
@@ -182,8 +216,9 @@ const getFilesList = async () => {
     paginationData.value.total = data.total
     for (let i = 0; i < filesList.value.length; i++) {
       filesList.value[i].index = (paginationData.value.currentPage - 1) * paginationData.value.currentPageSize + i + 1
-      filesList.value[i].isValid = true
-      checkLinkValidity(i, filesList.value[i].minio_url)
+      // filesList.value[i].isValid = true
+      filesList.value[i].status = true
+      // checkLinkValidity(i, filesList.value[i].minio_url)
     }
   } catch (e) {
     console.log(e)
@@ -328,6 +363,11 @@ getKnowledgeBaseById()
         width: 28px;
         height: 28px;
         cursor: pointer;
+
+        &:is([disabled='false']) {
+          cursor: not-allowed;
+          opacity: 0.5;
+        }
       }
 
       .list-name {
@@ -337,7 +377,7 @@ getKnowledgeBaseById()
 
         .text-area,
         .icon-area {
-          vertical-align: middle;
+          vertical-align: bottom;
         }
       }
 
@@ -352,6 +392,10 @@ getKnowledgeBaseById()
         &:is([type='warning']) {
           background-color: #E6A23C;
         }
+
+        &:is([type='danger']) {
+          background-color: #F56C6C;
+        }
       }
 
       .operation {
@@ -359,6 +403,10 @@ getKnowledgeBaseById()
         align-items: center;
         justify-content: space-evenly;
 
+        .el-button+.el-button {
+          margin: 0px;
+        }
+
         .el-switch {
           &:is(.is-checked) {
             .el-switch__core {

+ 2 - 0
vite.config.ts

@@ -7,9 +7,11 @@ import { loadEnv } from 'vite'
 import vueJsx from "@vitejs/plugin-vue-jsx";
 
 export default defineConfig(({ command, mode }) => {
+
   const env = loadEnv(mode, process.cwd(), '');
   // console.log('env', env)
   return {
+    base: '/',  // 设置相对路径
     define: {
       'process.env': env
     },