OCRDialog.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  1. <template>
  2. <el-dialog :close-on-click-modal="false" :title="title" v-model="dialogFormVisible" width="800"
  3. @closed="handleClosed">
  4. <el-form :model="form" ref="formRef">
  5. <el-form-item label="任务名称" prop="name" :label-width="formLabelWidth" required>
  6. <el-input v-model="form.name" autocomplete="off"></el-input>
  7. </el-form-item>
  8. <el-form-item label="任务状态" :label-width="formLabelWidth">
  9. <el-select v-model="form.status" placeholder="请选择任务状态">
  10. <el-option label="排队" value="queue"></el-option>
  11. <el-option label="立即执行" value="immediately"></el-option>
  12. </el-select>
  13. </el-form-item>
  14. <el-form-item label="文件" :label-width="formLabelWidth">
  15. <el-upload ref="upload" :action="'/api/file/upload/pdf/' + form.job_id" :multiple="true" :limit="10000"
  16. :on-preview="handlePreview" :on-remove="handleRemove" :on-success="handleSuccess" v-model:file-list="fileList"
  17. :on-exceed="handleExceed" :auto-upload="false" style="width: 100%;">
  18. <el-button @click.stop="knowledgeBase.visible = true" size="small" type="primary">知识库导入</el-button>
  19. <el-button slot="trigger" size="small" type="primary">上传文件</el-button>
  20. <div slot="tip" class="el-upload__tip">只能上传pdf文件,且不超过500kb</div>
  21. </el-upload>
  22. </el-form-item>
  23. </el-form>
  24. <footer slot="footer">
  25. <el-button type="primary" @click="handleConfirm">确 定</el-button>
  26. <el-button @click="dialogFormVisible = false">取 消</el-button>
  27. </footer>
  28. <div class="knowledge-base" v-show="knowledgeBase.visible" v-loading="loading" element-loading-text="导入中...">
  29. <div class="topbar">
  30. <span @click="knowledgeBase.visible = false" class="close-knowledge-base"><el-icon>
  31. <Close />
  32. </el-icon>
  33. </span>
  34. </div>
  35. <div class="knowledge-base-content">
  36. <el-scrollbar class="knowledge-base-list">
  37. <div class="knowledge-base-item">
  38. <span class="name">知识库名称</span>
  39. <span class="number">文件数量</span>
  40. </div>
  41. <div :class="knowledgeBase.activeId === item.id ? 'knowledge-base-item active' : 'knowledge-base-item'"
  42. v-for="(item, index) in knowledgeBase.data" :key="item.id"
  43. @click="handleKnowledgeBaseClick(toRaw(item), index)">
  44. <span class="name">
  45. <span class="icon">
  46. <span class="document-icon"></span>
  47. </span>
  48. <span class="text">
  49. <div class="name-text">{{ item.name }}</div>
  50. <div class="description-text">{{ item.description }}</div>
  51. </span>
  52. </span>
  53. <span class="number">{{ item.file_count }}</span>
  54. </div>
  55. </el-scrollbar>
  56. <div class="knowledge-base-detail">
  57. <div class="management-content">
  58. <div class="management-content-top">
  59. <div class="left">
  60. <span class="search">
  61. <el-input v-model="knowledgeBase.querySearch" size="large" placeholder="搜索"
  62. @keydown.enter="debounceGetKBfileList" :prefix-icon="Search" />
  63. </span>
  64. </div>
  65. <div class="right">
  66. <span class="add-file" @click="handleAllImport">
  67. <span class="text">全部导入</span>
  68. </span>
  69. <span class="add-file" :disabled="knowledgeBase.selectedNum > 0 ? 'false' : 'true'"
  70. @click="handleSelectedImport">
  71. <span class="text">批量导入</span>
  72. </span>
  73. </div>
  74. </div>
  75. <el-scrollbar class="management-content-middle">
  76. <el-table :data="knowledgeBase.filesList" ref="KBTableRef" @selection-change="handleSelectionChange">
  77. <el-table-column :selectable="handleSelectable" type="selection" width="30" />
  78. <el-table-column label="#" prop="index" width="50" />
  79. <el-table-column prop="file_name" min-width="150" label="标题">
  80. <template #default="{ row }">
  81. <div class="list-name">
  82. <span class="icon-area">
  83. <i :class="`${row.file_type}-icon`"></i>
  84. </span>
  85. <span class="text-area">
  86. <el-text style="vertical-align: bottom;width: 100%;" line-clamp="1">
  87. {{ row.file_name }}
  88. </el-text>
  89. </span>
  90. </div>
  91. </template>
  92. </el-table-column>
  93. <el-table-column prop="knowledge_type" label="知识类型">
  94. <template #default="{ row }">
  95. <el-text line-clamp="1" style="vertical-align: bottom;">
  96. {{ row.knowledge_type }}
  97. </el-text>
  98. </template>
  99. </el-table-column>
  100. <el-table-column prop="version" label="版本" />
  101. <el-table-column prop="author" label="作者(主编)" />
  102. <el-table-column prop="year" label="年份" />
  103. <el-table-column prop="creator" label="上传人" />
  104. <el-table-column prop="created_at" label="上传时间" width="150" />
  105. <el-table-column prop="" label="状态" width="80">
  106. <template #default="{ row }">
  107. <span v-if="row.status">
  108. <i class="circle" type="success"></i>
  109. <el-text type="success">可用</el-text>
  110. </span>
  111. <span v-else>
  112. <i class="circle" type="danger"></i>
  113. <el-text type="danger">禁用</el-text>
  114. </span>
  115. </template>
  116. </el-table-column>
  117. <el-table-column fixed="right" label="操作" min-width="55">
  118. <template #default="{ row }">
  119. <div class="operation">
  120. <el-button link type="primary" :disabled="!row.status"
  121. @click="handleImportFiles([{ ...row, file_url: `/open-platform/files/${row.id}/download` }])">导入</el-button>
  122. </div>
  123. </template>
  124. </el-table-column>
  125. </el-table>
  126. <div class="pagination"
  127. v-if="totalPage > 1 || paginationData.defaultPageSize !== paginationData.currentPageSize">
  128. <el-pagination :current-page="paginationData.currentPage" :page-sizes="paginationData.pageSizes"
  129. :disabled="paginationData.disabled" :default-page-size="paginationData.defaultPageSize"
  130. layout="total, sizes, prev, pager, next, jumper" :total="paginationData.total"
  131. @size-change="handleSizeChange" @current-change="handleCurrentChange" />
  132. </div>
  133. </el-scrollbar>
  134. </div>
  135. </div>
  136. </div>
  137. </div>
  138. </el-dialog>
  139. </template>
  140. <script setup lang="ts">
  141. import { ref, watch, toRaw, onMounted, computed, getCurrentInstance } from 'vue'
  142. import { Search } from '@element-plus/icons-vue'
  143. import CryptoJS from 'crypto-js';
  144. import encHex from 'crypto-js/enc-hex'
  145. import { createJob, putQueueJob, getKnowledgeBase, getKnowledgeBaseFilesList } from '@/api/AgentApi'
  146. import axios from 'axios'
  147. import { genFileId, ElMessage } from 'element-plus'
  148. import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
  149. import { throttle, debounce } from 'lodash'
  150. const KBTableRef = ref()
  151. const formRef = ref()
  152. const dialogFormVisible = ref(false)
  153. const formLabelWidth = ref('120px')
  154. let loading = ref<boolean>(false)
  155. const paginationData = ref<any>({
  156. currentPage: 1,
  157. pageSizes: [10, 50, 100, 1000, 10000],
  158. disabled: false,
  159. total: 0,
  160. defaultPageSize: 10,
  161. currentPageSize: 10
  162. })
  163. const totalPage = computed(() => {
  164. return Math.ceil(paginationData.value.total / paginationData.value.currentPageSize)
  165. })
  166. const handleSizeChange = (pageSize: number) => {
  167. paginationData.value.currentPageSize = pageSize
  168. debounceGetKBfileList()
  169. }
  170. const handleCurrentChange = (page: number) => {
  171. paginationData.value.currentPage = page
  172. debounceGetKBfileList()
  173. }
  174. type kbData = {
  175. id: number,
  176. name: string | null,
  177. description: string | null,
  178. tags: string | null,
  179. file_count: number,
  180. created_at: string | null,
  181. updated_at: string | null
  182. }
  183. type knowledgeBaseType = {
  184. visible: boolean,
  185. activeId: number | string,
  186. activeIndex: number,
  187. querySearch: string,
  188. data: kbData[],
  189. filesList: any[]
  190. selectedNum: number
  191. }
  192. let knowledgeBase = ref<knowledgeBaseType>({
  193. visible: false,
  194. activeId: 0,
  195. activeIndex: -1,
  196. querySearch: "",
  197. data: [],
  198. filesList: [],
  199. selectedNum: 0,
  200. })
  201. function handleKnowledgeBaseClick(data: kbData, index: number) {
  202. knowledgeBase.value.activeId = data.id
  203. knowledgeBase.value.activeIndex = index
  204. paginationData.value.currentPage = 1
  205. }
  206. function handleGetKBfileList() {
  207. getKnowledgeBaseFilesList({
  208. file_name: knowledgeBase.value.querySearch,
  209. pageSize: paginationData.value.currentPageSize,
  210. pageNo: paginationData.value.currentPage,
  211. kbId: knowledgeBase.value.activeId
  212. }).then((res: any) => {
  213. const { data, code, message } = res
  214. if (code === 200) {
  215. knowledgeBase.value.filesList = data.list
  216. for (let i = 0; i < knowledgeBase.value.filesList.length; i++) {
  217. knowledgeBase.value.filesList[i].index = (paginationData.value.currentPage - 1) * paginationData.value.currentPageSize + i + 1
  218. // knowledgeBase.value.filesList[i].isValid = true
  219. // checkLinkValidity(i, knowledgeBase.value.filesList[i].minio_url)
  220. }
  221. paginationData.value.total = data.total
  222. }
  223. }).catch((e: any) => {
  224. console.log(e)
  225. })
  226. }
  227. const throttleGetKBfileList = throttle(handleGetKBfileList, 1000)
  228. const debounceGetKBfileList = debounce(handleGetKBfileList, 500, { leading: true })
  229. const upload = ref()
  230. const form = ref({
  231. job_id: 0,
  232. job_category: '',
  233. job_name: '',
  234. job_details: '',
  235. job_creator: '',
  236. queue_category: '',
  237. queue_name: '',
  238. name: '',
  239. status: 'queue'
  240. })
  241. const fileList = ref<any>([])
  242. // 记录已导入文件的哈希值
  243. const importedFileHashes = ref<string[]>([])
  244. const props = defineProps({
  245. title: { type: String, required: true, default: '工作' },
  246. queue_category: { type: String, required: true, default: 'SYSTEM' },
  247. queue_name: { type: String, required: true, default: 'DEFAULT' },
  248. job_category: { type: String, required: false, default: 'USER' },
  249. job_name: { type: String, required: false, default: '工作名称' },
  250. job_details: { type: String, required: false, default: '' },
  251. job_creator: { type: String, required: false, default: 'admin' },
  252. status: { type: Number, required: false, default: 0 },
  253. })
  254. const emit = defineEmits(['update:modelValue', 'success', 'cancel', 'closed'])
  255. const showDialog = (visible: boolean = true) => {
  256. // console.log("OCRDialog showDialog")
  257. if (upload.value) {
  258. upload.value.clearFiles();
  259. }
  260. dialogFormVisible.value = visible
  261. }
  262. function handleSelectionChange(newSelection: any[]) {
  263. knowledgeBase.value.selectedNum = newSelection.length
  264. }
  265. // 清除上传文件列表
  266. const clearFileList = () => {
  267. fileList.value = []
  268. if (upload.value) {
  269. upload.value.clearFiles()
  270. }
  271. importedFileHashes.value = [] // 清空已导入文件的哈希值记录
  272. }
  273. function handleGetKnowledgeBase() {
  274. getKnowledgeBase().then((response: any) => {
  275. // console.log('getKnowledgeBase', response)
  276. const { data, code, message } = response
  277. if (code === 200) {
  278. knowledgeBase.value.data = data.list || []
  279. }
  280. }).catch((e: any) => {
  281. console.log(e)
  282. })
  283. }
  284. async function handleAllImport() {
  285. try {
  286. const res = await getKnowledgeBaseFilesList({
  287. file_name: "",
  288. pageSize: knowledgeBase.value.data[knowledgeBase.value.activeIndex].file_count,
  289. pageNo: 1,
  290. kbId: knowledgeBase.value.activeId
  291. })
  292. const { data, code } = res
  293. if (code === 200) {
  294. let filterData = data.list.filter((it: any) => it.status)
  295. filterData = filterData.map((it: any) => {
  296. it.file_url = `/open-platform/files/${it.id}/download`
  297. return it
  298. })
  299. handleImportFiles(filterData)
  300. }
  301. } catch (e) {
  302. console.log(e)
  303. }
  304. }
  305. function fetchFile(fileName: string, fileUrl: string) {
  306. axios.get(fileUrl, { responseType: 'blob' })
  307. .then(async response => {
  308. // upload.value!.clearFiles()
  309. // 创建文件对象并添加到文件列表
  310. const rawFile = new File([response.data], fileName, { type: response.data.type });
  311. const file = rawFile as UploadRawFile
  312. file.uid = genFileId()
  313. // const fileHashHex = await calculateFileHashForCryptoJS(file)
  314. // const { md5, sha256 } = await hashFile(file)
  315. let fileExist = false;
  316. // await Promise.allSettled(fileList.value.map(async (it: any) => {
  317. // const itFileHash = await hashFile(it.raw);
  318. // if (sha256 === itFileHash.sha256) {
  319. // fileExist = true;
  320. // return true;
  321. // }
  322. // return false;
  323. // }))
  324. if (fileExist) {
  325. ElMessage({
  326. message: `文件“${fileName}”已存在上传列表中`,
  327. type: 'info',
  328. plain: true,
  329. })
  330. } else {
  331. upload.value!.handleStart(file)
  332. ElMessage({
  333. message: `文件“${fileName}”导入成功`,
  334. type: 'success',
  335. plain: true,
  336. })
  337. }
  338. // knowledgeBase.value.visible = false
  339. })
  340. .catch(error => {
  341. ElMessage({
  342. message: `从知识库获取文件“${fileName}”失败`,
  343. type: 'error'
  344. })
  345. console.log(error);
  346. });
  347. }
  348. function handleSelectable(row: any, index: number) {
  349. // console.log(row, index)
  350. return row.status
  351. }
  352. function handleSelectedImport() {
  353. const selectionRows = KBTableRef.value.getSelectionRows().map((it: any) => {
  354. let copyIt: any = JSON.parse(JSON.stringify(it))
  355. copyIt.file_url = `/open-platform/files/${copyIt.id}/download`
  356. return copyIt
  357. })
  358. if (selectionRows.length == 0) return;
  359. handleImportFiles(selectionRows)
  360. // console.log(SelectionRows)
  361. }
  362. // 使用原生方法计算文件的hash值,该方法兼容性较差
  363. async function calculateFileHashForNative(file: File) {
  364. const arrayBuffer = await file.arrayBuffer(); // 将文件读取为 ArrayBuffer
  365. const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer); // 计算哈希值
  366. const hashArray = Array.from(new Uint8Array(hashBuffer)); // 将哈希转换为字节数组
  367. const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); // 转换为十六进制字符串
  368. // console.log("calculateFileHashForNative1:", hashHex)
  369. return hashHex;
  370. }
  371. // 使用crypto-js计算文件的hash值 ,该方法易发生卡顿
  372. async function calculateFileHashForCryptoJS(file: File): Promise<string> {
  373. return new Promise((resolve, reject) => {
  374. try {
  375. const reader = new FileReader();
  376. reader.readAsArrayBuffer(file)
  377. reader.onload = function (e) {
  378. const arrayBuffer = e.target?.result; // 读取的文件内容
  379. // 将 ArrayBuffer 转换为 CryptoJS 可以处理的 WordArray
  380. const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer);
  381. // 计算文件的 SHA-256 哈希值并转为 Base64 编码字符串
  382. // const hashBase64 = CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.Base64);
  383. // 计算文件的 SHA-256 哈希值并转为 16 进制字符串
  384. const hashHex = CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.hex);
  385. // console.log('calculateFileHashForCryptoJS:', hashHex);
  386. resolve(hashHex)
  387. };
  388. reader.onerror = () => {
  389. reject('')
  390. }
  391. } catch (e) {
  392. console.log(e)
  393. reject('')
  394. }
  395. })
  396. }
  397. // function handleImportFiles(filesList: any[]) {
  398. // for (let i = 0; i < filesList.length; i++) {
  399. // // if (i > 0) break;
  400. // fetchFile(filesList[i].file_name, filesList[i].minio_url)
  401. // }
  402. // }
  403. /**
  404. * 用于计算文件的hash值,包括sha256值和md5值
  405. */
  406. function hashFile(file: any) {
  407. /**
  408. * 使用指定的算法计算hash值
  409. */
  410. function hashFileInternal(file: any, alog: any) {
  411. // 指定块的大小,这里设置为20MB,可以根据实际情况进行配置
  412. const chunkSize = 20 * 1024 * 1024
  413. let promise: any = Promise.resolve()
  414. // 使用promise来串联hash计算的顺序。因为FileReader是在事件中处理文件内容的,必须要通过某种机制来保证update的顺序是文件正确的顺序
  415. for (let index = 0; index < file.size; index += chunkSize) {
  416. promise = promise.then(() => hashBlob(file.slice(index, index + chunkSize)))
  417. }
  418. /**
  419. * 更新文件块的hash值
  420. */
  421. function hashBlob(blob: any) {
  422. return new Promise((resolve, reject) => {
  423. const reader = new FileReader()
  424. reader.onload = ({ target }) => {
  425. const wordArray = CryptoJS.lib.WordArray.create(target?.result)
  426. // 增量更新计算结果
  427. alog.update(wordArray)
  428. resolve(true)
  429. }
  430. reader.readAsArrayBuffer(blob)
  431. })
  432. }
  433. // 使用promise返回最终的计算结果
  434. return promise.then(() => encHex.stringify(alog.finalize()))
  435. }
  436. // 同时计算文件的sha256和md5,并使用promise返回
  437. return Promise.all([hashFileInternal(file, CryptoJS.algo.SHA256.create()),
  438. hashFileInternal(file, CryptoJS.algo.MD5.create())])
  439. .then(([sha256, md5]) => ({
  440. sha256,
  441. md5
  442. }))
  443. }
  444. const handlePreview = (file: any) => {
  445. console.log(file)
  446. }
  447. const handleExceed: UploadProps['onExceed'] = (files) => {
  448. upload.value!.clearFiles()
  449. const file = files[0] as UploadRawFile
  450. // console.log(handleExceed, file)
  451. file.uid = genFileId()
  452. upload.value!.handleStart(file)
  453. }
  454. const handleRemove = async (file: any, fileList: any) => {
  455. const hashHex: string = await calculateFileHashForCryptoJS(file.raw)
  456. const hashHexIndex = importedFileHashes.value.indexOf(hashHex)
  457. importedFileHashes.value.splice(hashHexIndex, 1)
  458. // console.log(file, fileList)
  459. }
  460. async function calculateFileHash(file: File) {
  461. // const arrayBuffer = await file.arrayBuffer();
  462. // const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
  463. // const hashArray = Array.from(new Uint8Array(hashBuffer));
  464. // const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
  465. // return hashHex;
  466. // 使用 crypto-js 计算哈希值
  467. return new Promise((resolve, reject) => {
  468. const reader = new FileReader();
  469. reader.readAsArrayBuffer(file);
  470. reader.onload = function (e) {
  471. const arrayBuffer = e.target?.result;
  472. const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer);
  473. const hashHex = CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.hex);
  474. resolve(hashHex);
  475. };
  476. reader.onerror = () => {
  477. reject('');
  478. };
  479. });
  480. }
  481. async function handleImportFiles(filesList: any[]) {
  482. loading.value = true
  483. await Promise.allSettled(filesList.map(async (fileInfo) => {
  484. try {
  485. const { file_name, minio_url, file_url } = fileInfo;
  486. const response = await axios.get(file_url, { responseType: 'blob' });
  487. const rawFile = new File([response.data], file_name, { type: response.data.type });
  488. const fileHash = await calculateFileHash(rawFile);
  489. if (importedFileHashes.value.includes(fileHash as string)) {
  490. ElMessage({
  491. message: `文件“${file_name}”已导入过或者存在内容相同的文件,不能重复导入`,
  492. type: 'info',
  493. plain: true,
  494. });
  495. return;
  496. }
  497. const file: UploadRawFile = rawFile as UploadRawFile;
  498. file.uid = genFileId();
  499. upload.value!.handleStart(file);
  500. importedFileHashes.value.push(fileHash as string);
  501. ElMessage({
  502. message: `文件“${file_name}”导入成功`,
  503. type: 'success',
  504. plain: true,
  505. });
  506. } catch (error) {
  507. ElMessage({
  508. message: `从知识库获取文件“${file_name}”失败`,
  509. type: 'error'
  510. });
  511. console.log(error);
  512. }
  513. }))
  514. loading.value = false
  515. knowledgeBase.value.visible = false
  516. }
  517. const handleSuccess = (response: any, file: any, fileList: any) => {
  518. console.log(response, file, fileList);
  519. const fileHash = importedFileHashes.value.find(hash => {
  520. // 这里简单通过文件名匹配,实际可根据需求优化
  521. return file.name === file.raw.name;
  522. });
  523. if (fileHash) {
  524. // 上传成功后保留哈希记录
  525. importedFileHashes.value.push(fileHash);
  526. }
  527. emit('success');
  528. }
  529. const handleConfirm = () => {
  530. formRef.value.validate((valid: any) => {
  531. if (valid) {
  532. // console.log('submit', props)
  533. form.value.job_category = props.queue_category + "_" + props.queue_name;
  534. form.value.job_name = form.value.name;
  535. form.value.queue_category = props.queue_category;
  536. form.value.queue_name = props.queue_name;
  537. form.value.job_details = props.job_details;
  538. form.value.job_creator = props.job_creator;
  539. // console.log('form values ', form.value)
  540. createJob(form.value).then((res: any) => {
  541. var job = res.records[0]
  542. form.value.job_id = job.id;
  543. upload.value.submit();
  544. })
  545. }
  546. })
  547. }
  548. defineExpose({ showDialog, clearFileList })
  549. function handleClosed() {
  550. knowledgeBase.value.visible = false
  551. clearFileList()
  552. }
  553. watch(() => knowledgeBase.value.activeId, (newValue) => {
  554. debounceGetKBfileList()
  555. // console.log('activeId', newValue)
  556. })
  557. watch(() => knowledgeBase.value.visible, (newValue) => {
  558. if (!newValue) {
  559. KBTableRef.value.clearSelection() // 清除选择
  560. }
  561. })
  562. async function checkLinkValidity(index: number, url: string) {
  563. try {
  564. const response = await axios.head(url); // 使用 HEAD 请求,仅请求响应头
  565. if (response.status >= 200 && response.status < 300) {
  566. knowledgeBase.value.filesList[index].isValid = true
  567. } else {
  568. knowledgeBase.value.filesList[index].isValid = false // 返回 false 表示链接无效
  569. }
  570. } catch (error) {
  571. knowledgeBase.value.filesList[index].isValid = false // 请求失败,返回 false
  572. }
  573. }
  574. onMounted(() => {
  575. handleGetKnowledgeBase()
  576. })
  577. </script>
  578. <style lang="less" scoped>
  579. footer {
  580. text-align: right;
  581. }
  582. :deep(.el-upload-list) {
  583. max-height: 300px;
  584. // width: auto;
  585. overflow: auto;
  586. /*定义滚动条高宽及背景
  587. 高宽分别对应横竖滚动条的尺寸*/
  588. &::-webkit-scrollbar {
  589. width: 10px;
  590. height: 10px;
  591. background-color: #F5F5F5;
  592. display: none;
  593. }
  594. /*定义滚动条轨道
  595. 内阴影+圆角*/
  596. &::-webkit-scrollbar-track {
  597. -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  598. border-radius: 10px;
  599. background-color: #F5F5F5;
  600. }
  601. /*定义滑块
  602. 内阴影+圆角*/
  603. &::-webkit-scrollbar-thumb {
  604. border-radius: 10px;
  605. -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  606. background-color: #DDDEE0;
  607. }
  608. &:hover {
  609. &::-webkit-scrollbar {
  610. display: block;
  611. }
  612. }
  613. }
  614. .knowledge-base {
  615. position: fixed;
  616. top: 50%;
  617. left: 50%;
  618. width: 80vw;
  619. height: 90vh;
  620. background-color: white;
  621. display: flex;
  622. flex-direction: column;
  623. // overflow: auto;
  624. transform: translate(-50%, -50%);
  625. border-radius: 5px;
  626. min-width: 800px;
  627. .topbar {
  628. padding: 5px;
  629. flex: 0 0 auto;
  630. .close-knowledge-base {
  631. margin-left: calc(100% - 24px);
  632. cursor: pointer;
  633. }
  634. }
  635. .knowledge-base-content {
  636. flex: 1 1 auto;
  637. // overflow: auto;
  638. display: flex;
  639. flex-wrap: nowrap;
  640. min-height: 0;
  641. .knowledge-base-list {
  642. flex: 0 0 auto;
  643. width: 300px;
  644. // background-color: skyblue;
  645. border-right: 1px solid #E4E4E4;
  646. .knowledge-base-item {
  647. display: flex;
  648. border-bottom: 1px solid #FAFAFA;
  649. padding: 10px 5px;
  650. gap: 0px 20px;
  651. align-items: center;
  652. &:first-child {
  653. background-color: #F2F5F9;
  654. padding: 10px 5px;
  655. // font-size: 17px;
  656. }
  657. &:hover,
  658. &:is(.active) {
  659. background-color: #F2F5F9;
  660. }
  661. .name {
  662. flex: 1 1 auto;
  663. display: flex;
  664. flex-wrap: nowrap;
  665. align-items: center;
  666. min-width: 0px;
  667. .icon {
  668. flex: 0 0 auto;
  669. padding: 2px;
  670. background-color: #7733FF;
  671. border-radius: 50%;
  672. display: inline-block;
  673. margin-right: 10px;
  674. }
  675. .text {
  676. flex: 1 1 auto;
  677. white-space: nowrap;
  678. text-overflow: ellipsis;
  679. overflow: hidden;
  680. width: 100%;
  681. display: flex;
  682. flex-direction: column;
  683. justify-content: center;
  684. &>div {
  685. width: 100%;
  686. white-space: nowrap;
  687. text-overflow: ellipsis;
  688. overflow: hidden;
  689. display: inline-block;
  690. line-height: 1.4;
  691. }
  692. .name-text {
  693. font-weight: bold;
  694. font-size: 16px;
  695. }
  696. .description-text {
  697. font-size: 14px;
  698. color: #D8D8D8;
  699. }
  700. }
  701. }
  702. .number {
  703. flex: 0 0 70px;
  704. }
  705. }
  706. }
  707. .knowledge-base-detail {
  708. flex: 1 1 auto;
  709. min-width: 0px;
  710. overflow: auto;
  711. min-width: 400px;
  712. // background-color: orange;
  713. }
  714. }
  715. .management-content {
  716. flex: 1 1 auto;
  717. min-width: 0px; //让盒子可以压缩
  718. min-height: 0px;
  719. padding: 0px 5px;
  720. display: flex;
  721. height: 100%;
  722. flex-direction: column;
  723. .management-content-top {
  724. clear: both;
  725. flex: 0 0 auto;
  726. display: flex;
  727. .right {
  728. margin-left: auto;
  729. }
  730. .left {}
  731. .add-file {
  732. color: white;
  733. background-color: #169BD5;
  734. margin-left: auto;
  735. padding: 10px 25px;
  736. border-radius: 3px;
  737. display: inline-block;
  738. cursor: pointer;
  739. margin-right: 10px;
  740. .text {
  741. // vertical-align: middle;
  742. // margin-left: 10px;
  743. }
  744. &[disabled='true'] {
  745. opacity: 0.5;
  746. }
  747. }
  748. }
  749. .management-content-middle {
  750. flex: 1 1 auto;
  751. // overflow: auto;
  752. padding-right: 10px;
  753. :deep(.el-scrollbar) {
  754. .el-table__header-wrapper {
  755. position: sticky;
  756. top: 0;
  757. }
  758. }
  759. }
  760. .pagination {
  761. flex: 0 0 auto;
  762. }
  763. }
  764. &:deep(.el-table) {
  765. min-width: 0px;
  766. max-width: 100%;
  767. .document-download-icon {
  768. width: 28px;
  769. height: 28px;
  770. cursor: pointer;
  771. }
  772. .list-name {
  773. display: flex;
  774. flex-wrap: nowrap;
  775. align-items: center;
  776. .icon-area {
  777. flex: 0 0 auto;
  778. }
  779. .text-area {
  780. flex: 1 1 auto;
  781. }
  782. }
  783. .circle {
  784. border-radius: 50%;
  785. width: 10px;
  786. height: 10px;
  787. background-color: #8AD068;
  788. display: inline-block;
  789. margin-right: 10px;
  790. &:is([type='warning']) {
  791. background-color: #E6A23C;
  792. }
  793. &:is([type='danger']) {
  794. background-color: #F56C6C;
  795. }
  796. }
  797. .operation {
  798. display: flex;
  799. align-items: center;
  800. justify-content: space-evenly;
  801. .el-switch {
  802. &:is(.is-checked) {
  803. .el-switch__core {
  804. background-color: #155AEF;
  805. }
  806. }
  807. .el-switch__core {
  808. border-radius: 5px;
  809. width: 35px;
  810. min-width: 0px;
  811. // background-color: #155AEF;
  812. }
  813. .el-switch__action {
  814. border-radius: 3px;
  815. }
  816. }
  817. }
  818. }
  819. .search {
  820. // padding: 10px 20px;
  821. margin: 5px;
  822. background: white;
  823. display: inline-block;
  824. :deep(.el-input__wrapper) {
  825. background-color: #EFF1F5;
  826. border-radius: 10px;
  827. }
  828. .el-input {
  829. width: 250px;
  830. }
  831. }
  832. .pagination {
  833. // text-align: right;
  834. // background-color: @bgColor;
  835. display: flex;
  836. padding: 0px 5px;
  837. justify-content: end;
  838. margin: 10px 0px 5px;
  839. :deep(.el-pagination) {
  840. background-color: #fff;
  841. // justify-content: end;
  842. margin-left: auto;
  843. padding: 10px 0px 20px;
  844. // margin: 0px 20px;
  845. .page-box {
  846. border: 1px solid #E7E8EE;
  847. border-radius: 3px;
  848. box-sizing: border-box;
  849. }
  850. .el-pager {
  851. .is-active {
  852. color: #fff;
  853. background-color: #5780FC;
  854. }
  855. .number {
  856. .page-box();
  857. margin-left: 10px;
  858. &:last-child {
  859. margin-right: 10px;
  860. }
  861. }
  862. }
  863. .btn-next,
  864. .btn-quicknext,
  865. .btn-quickprev,
  866. .btn-prev {
  867. .page-box()
  868. }
  869. .btn-quicknext,
  870. .btn-quickprev {
  871. margin-left: 10px;
  872. }
  873. }
  874. }
  875. .document-icon {
  876. width: 32px;
  877. height: 32px;
  878. }
  879. }
  880. </style>