OCRDialog.vue 22 KB

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