OCRDialog.vue 27 KB

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