index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. <template>
  2. <div class="interpretation">
  3. <progress-bar :num="processNum" />
  4. <div class="header">
  5. <home-header sub-title="自然资源大模型" />
  6. </div>
  7. <div class="content-box">
  8. <div class="left-panel">
  9. <div class="content-top">
  10. <div class="left">
  11. <a-input :value="fileDetail?.name" style="width: 400px; margin-right: 10px" />
  12. <a-upload
  13. class="upload"
  14. action="/aisKnowledge/infra/file/upload"
  15. @change="handleChange"
  16. :showUploadList="false"
  17. :data="{ type: 'temp' }"
  18. accept=".pdf"
  19. >
  20. <img src="/images/zcjd/upload-icon.png" />上传本地文件
  21. </a-upload>
  22. <a-button class="ai-btn">AI解读</a-button>
  23. </div>
  24. <div class="right" v-if="false">
  25. <span @click="downloadFile"><img src="/images/zcjd/download-icon.png" />下载</span>
  26. <span><img src="/images/zcjd/star-icon.png" />收藏</span>
  27. </div>
  28. </div>
  29. <div class="content-detail">
  30. <div class="file-preview">
  31. <!-- <img :src="fileDetail?.content" alt="" v-if="fileDetail?.content" /> -->
  32. <!-- <div class="spin" v-if="loading"><a-spin></a-spin></div> -->
  33. <PDFViewer :src="fileUrl" ref="ifRef"></PDFViewer>
  34. </div>
  35. </div>
  36. </div>
  37. <div class="affix_box" :offsetTop="20">
  38. <div class="right-panel">
  39. <div class="ai-interpretation">
  40. <div class="check-type">
  41. <div class="check">
  42. <a-radio-group v-model:value="checkType" button-style="solid">
  43. <a-radio-button value="interpretation">AI解读</a-radio-button>
  44. <a-radio-button value="question">AI问答</a-radio-button>
  45. </a-radio-group>
  46. </div>
  47. </div>
  48. <div class="result">
  49. <div class="content" id="scrollArea" v-if="checkType == 'interpretation'">
  50. <a-spin class="spin" v-if="aiLoading"></a-spin>
  51. <template v-else>
  52. <div class="desc">
  53. <vue-markdown-it
  54. :source="content"
  55. v-if="content"
  56. :options="{
  57. html: true,
  58. linkify: true
  59. }"
  60. />
  61. </div>
  62. </template>
  63. </div>
  64. <div class="question" v-else>
  65. <div class="chat_box" id="chat_box">
  66. <div class="item">
  67. <div class="value-panel">
  68. <div class="name">
  69. 文件内容太多,读起来太累了?可以试着让我来帮您解读。<span
  70. >你可以试着这样问我:</span
  71. >
  72. </div>
  73. <div
  74. class="citem"
  75. v-for="(item, index) in question"
  76. :key="index"
  77. @click="toggleQuestion(index)"
  78. >
  79. <div class="value">{{ item }}</div>
  80. </div>
  81. </div>
  82. </div>
  83. <div class="chat">
  84. <div class="item" v-for="(item, index) in chatDesc" :key="index">
  85. <div class="user-panel">
  86. <div class="name">{{ item.name }}</div>
  87. <img src="/images/zczk/user.png" />
  88. </div>
  89. <div class="ai-panel">
  90. <img src="/images/zczk/icon-ai-title.png" />
  91. <div class="desc" v-if="item.content" id="scrollArea-1">
  92. <vue-markdown-it
  93. :source="item.content"
  94. :options="{
  95. html: true,
  96. linkify: true
  97. }"
  98. />
  99. </div>
  100. <div class="desc animation" v-else>正在思考中<span class="dots"></span></div>
  101. </div>
  102. </div>
  103. </div>
  104. </div>
  105. <div class="input-panel">
  106. <a-textarea
  107. v-model:value="keyword"
  108. placeholder="请输入"
  109. @keydown.enter="keydownEnter"
  110. >
  111. </a-textarea>
  112. <div class="send" @click="send">
  113. 发送<img src="/images/zczk/icon-white-send.png" />
  114. </div>
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. </template>
  124. <script setup>
  125. /**
  126. * @description 解读详情
  127. */
  128. import { ref, onMounted } from 'vue';
  129. import { useRoute } from 'vue-router';
  130. import HomeHeader from '@/views/home/components/HomeHeader.vue';
  131. import PDFViewer from '@/components/pdf/PDFViewerSearch.vue';
  132. import ProgressBar from '@/components/ProgressBar.vue';
  133. import api from '@/utils/policy-api';
  134. import { getNumAll } from '@/utils/common';
  135. import { message } from 'ant-design-vue';
  136. import { fetchEventSource } from '@microsoft/fetch-event-source';
  137. import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
  138. const route = useRoute();
  139. const chatDesc = ref([]);
  140. const keyword = ref('');
  141. const ctr = new AbortController();
  142. const fileDetail = ref(null);
  143. const fileUrl = ref('');
  144. const aiLoading = ref(false);
  145. const queryText = ref('分别给出该文章的全文综述和要点总结');
  146. const content = ref('');
  147. const loading = ref(false);
  148. const ifRef = ref(null);
  149. const docs = ref([]);
  150. const question = [
  151. '帮我总结一下核心内容?',
  152. '有没有类似的政策文件?',
  153. '文件中提到了哪些信息化内容?'
  154. ];
  155. const checkType = ref('interpretation');
  156. onMounted(async () => {
  157. const id = route.query.id;
  158. const url = route.query.url;
  159. //政策跳转 请求文件
  160. if (id) {
  161. getFileByID(id);
  162. }
  163. //政策解读 请求文件
  164. if (url) {
  165. fileUrl.value = url;
  166. var name = url.substring(url.lastIndexOf('/') + 1);
  167. var file = await fetchPdfFileStream(fileUrl.value, name);
  168. if (file) {
  169. setTimeout(() => {
  170. fileDetail.value = file;
  171. query();
  172. }, 100);
  173. }
  174. }
  175. });
  176. const getFileByID = (id) => {
  177. loading.value = true;
  178. var url = '/policy/query/' + id;
  179. api.get(url, {}, this, false).then(async (res) => {
  180. if (res && res.code == 200) {
  181. if (res.data.item) {
  182. var fileName = res.data.file_name;
  183. fileName = res.data.file_fullname;
  184. fileUrl.value = '/knowledge/file/' + fileName;
  185. loading.value = false;
  186. var file = await fetchPdfFileStream(fileUrl.value, fileName);
  187. if (file) {
  188. setTimeout(() => {
  189. fileDetail.value = file;
  190. query();
  191. }, 100);
  192. }
  193. }
  194. }
  195. });
  196. };
  197. import axios from 'axios';
  198. //在线地址转文件
  199. const fetchPdfFileStream = (pdfUrl, name) => {
  200. return new Promise((resolve, reject) => {
  201. var type = 'application/pdf';
  202. if (name.indexOf('pdf') > -1) {
  203. type = 'application/pdf';
  204. } else {
  205. type = 'application/docx';
  206. }
  207. axios
  208. .get(pdfUrl, { responseType: 'blob' })
  209. .then((response) => {
  210. const blob = new Blob([response.data], { type: type });
  211. const file = new File([blob], name, { type: type });
  212. resolve(file);
  213. // 接下来可以使用URL.createObjectURL(blob)创建一个可以用于<iframe>的URL
  214. })
  215. .catch((error) => {
  216. console.error('Error loading PDF:', error)
  217. reject(new Error('网络请求失败,请检查网络连接')); // 处理网络错误
  218. });
  219. });
  220. };
  221. const send = () => {
  222. if (!keyword.value) {
  223. message.info('请输入!');
  224. return;
  225. }
  226. queryText.value = keyword.value;
  227. keyword.value = '';
  228. var item = { name: queryText.value, content: '' };
  229. chatDesc.value.push(item);
  230. query();
  231. scrollToBottom('chat_box')
  232. };
  233. const toggleQuestion = (index) => {
  234. keyword.value = question[index];
  235. };
  236. const keydownEnter = () => {
  237. send();
  238. };
  239. const processNum = ref(0)
  240. //文件上传
  241. const handleChange = (info) => {
  242. loading.value = true;
  243. fileDetail.value = null;
  244. const status = info.file.status;
  245. processNum.value = 1
  246. if (status === 'done') {
  247. processNum.value = 100;
  248. fileDetail.value = info.file.originFileObj;
  249. fileUrl.value = 'https://ai.zrzyt.zj.gov.cn/aisKnowledge' + info.file.response.data;
  250. chatDesc.value = [];
  251. loading.value = false;
  252. content.value = '';
  253. checkType.value = 'interpretation';
  254. uploadFile();
  255. } else if (status === 'error') {
  256. message.error(`${info.file.name} file upload failed.`);
  257. }
  258. };
  259. const downloadFile = () => {
  260. let a = document.createElement('a');
  261. var name = fileDetail.value.name;
  262. var type = fileUrl.value.indexOf('temp') > -1 ? 'temp' : '1';
  263. a.href = '/knowledge/policy/download/' + name + '/' + type;
  264. a.click();
  265. };
  266. const uploadFile = () => {
  267. queryText.value = '分别给出该文章的全文综述和要点总结';
  268. query();
  269. };
  270. const scrollToBottom = async (idDom) => {
  271. // 滚动到底部
  272. if (!idDom) return;
  273. await nextTick(() => {
  274. const scrollEle = document.getElementById(idDom);
  275. if (scrollEle != null) {
  276. scrollEle.scrollTo({
  277. top: scrollEle.scrollHeight - scrollEle.clientHeight + 50,
  278. behavior: 'smooth'
  279. });
  280. }
  281. });
  282. };
  283. const goLocation = (content) => {
  284. nextTick(() => {
  285. ifRef.value.goSourceLocation(content, 2);
  286. });
  287. };
  288. const openDocByIndex = (ind) => {
  289. var flag = false;
  290. docs.value.forEach((t) => {
  291. if (t.index == ind) {
  292. flag = true;
  293. goLocation(t.content);
  294. }
  295. });
  296. if (!flag) {
  297. message.info('未找到出处!');
  298. }
  299. };
  300. window.openDocByIndex = openDocByIndex;
  301. const handleDocs = (data) => {
  302. if (data) {
  303. docs.value = data.map((v, i) => {
  304. if (v.indexOf('.pdf') > 0) {
  305. return {
  306. index: v.substring(v.indexOf('[[') + 2, v.indexOf(']]')),
  307. doc: v.substring(v.indexOf('] [') + 3, v.indexOf('.pdf]') + 4),
  308. link: v.substring(v.indexOf('.pdf]') + 6, v.indexOf('.pdf)') + 4),
  309. content: v.substring(v.indexOf('.pdf]') + 6),
  310. showContent: false,
  311. type: 'pdf'
  312. };
  313. } else if (v.indexOf('.txt') > 0) {
  314. return {
  315. index: i++,
  316. doc: v.substring(v.indexOf('] [') + 3, v.indexOf('.txt]') + 4),
  317. link: v.substring(v.indexOf('.txt]') + 6, v.indexOf('.txt)') + 4),
  318. content: v.substring(v.indexOf('.txt)') + 6),
  319. showContent: false,
  320. type: 'txt'
  321. };
  322. }
  323. });
  324. }
  325. };
  326. const timers = ref([]) //缓存定时器
  327. const timerIndex = ref(0)
  328. const query = async () => {
  329. var params = new FormData();
  330. aiLoading.value = true;
  331. params.append('files', fileDetail.value);
  332. params.append('stream', true);
  333. params.append('query', queryText.value);
  334. timers.value = []
  335. timerIndex.value = 0
  336. await fetchEventSource(window.AppGlobalConfig.aiServer + '/chat/complete_file_chat', {
  337. method: 'POST',
  338. openWhenHidden: true,
  339. body: params,
  340. signal: ctr.signal,
  341. async onmessage(msg) {
  342. if (msg) {
  343. try {
  344. aiLoading.value = false;
  345. const rData = JSON.parse(msg.data);
  346. if (rData) {
  347. var con = rData.choices[0].delta.content;
  348. if (rData.status == 3) {
  349. timers.value.forEach((timer) => {
  350. clearTimeout(timer);
  351. })
  352. let nums = getNumAll(con);
  353. nums.forEach(num => {
  354. con = con.replace(
  355. `[[${num}]]`,
  356. `<span onclick="window.openDocByIndex(${num})" class="poi" style=" cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 14px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 4px;background: #fff;color:#435C80;border: 1px solid #BACAE3">${num}</span>`
  357. )
  358. })
  359. if (checkType.value == 'interpretation') {
  360. content.value = con;
  361. scrollToBottom('scrollArea');
  362. } else {
  363. chatDesc.value[chatDesc.value.length - 1].content = con + '\n\n #### 以上是我思考的内容!\n\n';
  364. scrollToBottom('chat_box');
  365. }
  366. if (rData.docs) {
  367. handleDocs(rData.docs);
  368. }
  369. } else if (rData.status == 2) {
  370. let timer = null;
  371. if (checkType.value == 'interpretation') {
  372. timer = setTimeout(() => {
  373. content.value += con;
  374. scrollToBottom('scrollArea');
  375. clearTimeout(timer)
  376. }, timerIndex.value * 15)
  377. } else {
  378. timer = setTimeout(() => {
  379. chatDesc.value[chatDesc.value.length - 1].content += con;
  380. scrollToBottom('chat_box');
  381. clearTimeout(timer)
  382. }, timerIndex.value * 15)
  383. }
  384. timers.value.push(timer)
  385. timerIndex.value++;
  386. }
  387. }
  388. } catch (e) {
  389. console.log('===' + e.message);
  390. }
  391. }
  392. },
  393. onclose() {
  394. aiLoading.value = false;
  395. },
  396. onerror(err) {
  397. aiLoading.value = false;
  398. throw err;
  399. },
  400. onopen() {}
  401. });
  402. };
  403. </script>
  404. <style scoped lang="scss">
  405. .interpretation {
  406. width: 100%;
  407. height: 100%;
  408. position: relative;
  409. z-index: 1;
  410. background: #f0f4f7;
  411. .content-box {
  412. width: calc(100% - 300px);
  413. height: calc(100% - 75px);
  414. margin: auto;
  415. margin-top: 15px;
  416. overflow-y: auto;
  417. box-shadow: 0px 1px 8px 0px #e4e4e4;
  418. display: flex;
  419. .left-panel {
  420. border-radius: 6px;
  421. padding: 10px 0px 0px 0px;
  422. background-color: #fff;
  423. width: calc(100% - 600px);
  424. }
  425. .affix_box {
  426. position: fixed;
  427. right: 150px;
  428. top: 75px;
  429. }
  430. .right-panel {
  431. width: 580px;
  432. height: calc(100vh - 86px) !important;
  433. border-radius: 6px;
  434. background-color: #fff;
  435. margin-left: 20px;
  436. .ai-interpretation {
  437. width: 100%;
  438. height: 100%;
  439. .check-type {
  440. width: 100%;
  441. display: flex;
  442. align-items: center;
  443. height: 50px;
  444. .check {
  445. height: 50px;
  446. line-height: 50px;
  447. padding-left: 10px;
  448. }
  449. justify-content: space-between;
  450. }
  451. .icon {
  452. width: 30px;
  453. cursor: pointer;
  454. height: 30px;
  455. margin-right: 12px;
  456. background: #e0efff;
  457. display: flex;
  458. justify-content: center;
  459. align-items: center;
  460. border-radius: 5px 5px 5px 5px;
  461. img {
  462. width: 17px;
  463. height: 17px;
  464. }
  465. }
  466. .result {
  467. width: 100%;
  468. height: calc(100% - 50px);
  469. padding: 0px 0px 20px 0px;
  470. .content {
  471. padding: 0px 27px 0px 23px;
  472. height: 100%;
  473. background: transparent;
  474. box-shadow: none;
  475. overflow-y: scroll;
  476. .title {
  477. font-family: PingFang SC;
  478. font-weight: bold;
  479. font-size: 16px;
  480. margin-bottom: 23px;
  481. color: #000;
  482. }
  483. .desc {
  484. font-family: PingFang SC;
  485. font-weight: normal;
  486. font-size: 14px;
  487. //color: #85909e;
  488. line-height: 28px;
  489. margin-bottom: 36px;
  490. }
  491. .spin {
  492. height: 100%;
  493. display: flex;
  494. align-items: center;
  495. justify-content: center;
  496. }
  497. }
  498. .question {
  499. height: 100%;
  500. padding: 0px 0px 6px 0px;
  501. position: relative;
  502. display: flex;
  503. flex-direction: column;
  504. .item {
  505. display: flex;
  506. width: 100%;
  507. background: #fff;
  508. padding: 4px 20px 15px 20px;
  509. img {
  510. width: 40px;
  511. height: 29px;
  512. margin-right: 13px;
  513. }
  514. .value-panel {
  515. flex: 1;
  516. .name {
  517. font-family: PingFang SC, PingFang SC;
  518. font-weight: 400;
  519. font-size: 14px;
  520. color: #2c2c2c;
  521. line-height: 21px;
  522. span {
  523. color: #5a5b5e;
  524. }
  525. }
  526. .citem {
  527. margin-top: 0px;
  528. display: flex;
  529. cursor: pointer;
  530. align-items: center;
  531. padding: 16px 0px 0px 0px;
  532. img {
  533. width: 18px;
  534. height: 18px;
  535. margin-right: 9px;
  536. }
  537. .value {
  538. flex: 1;
  539. font-family: PingFang SC, PingFang SC;
  540. font-weight: 400;
  541. font-size: 14px;
  542. color: #ef7217;
  543. line-height: 21px;
  544. }
  545. }
  546. }
  547. }
  548. .scale-panel {
  549. display: flex;
  550. justify-content: flex-end;
  551. font-family: PingFang SC, PingFang SC;
  552. font-weight: 400;
  553. margin-top: 7px;
  554. margin-bottom: 27px;
  555. cursor: pointer;
  556. width: calc(100% - 30px);
  557. font-size: 13px;
  558. color: #778490;
  559. span {
  560. font-family: PingFang SC, PingFang SC;
  561. font-weight: 500;
  562. font-size: 13px;
  563. color: #3c7bff;
  564. }
  565. }
  566. .chat_box {
  567. flex: 1;
  568. height: 0px;
  569. overflow-y: auto;
  570. }
  571. .chat {
  572. font-family: PingFang SC;
  573. font-weight: 500;
  574. margin: 10px 0px;
  575. line-height: 28px;
  576. font-size: 16px;
  577. color: #333333;
  578. .item {
  579. display: flex;
  580. flex-direction: column;
  581. padding: 0px;
  582. background: transparent;
  583. width: unset;
  584. .user-panel {
  585. display: flex;
  586. margin: 20px 0px;
  587. justify-content: flex-end;
  588. img {
  589. width: 36px;
  590. margin-left: 7px;
  591. height: 36px;
  592. }
  593. .name {
  594. max-width: calc(100% - 43px);
  595. background: #3c7bff;
  596. border-radius: 5px 5px 5px 5px;
  597. padding: 7px 24px 7px 11px;
  598. font-family: PingFang SC, PingFang SC;
  599. font-weight: 500;
  600. font-size: 14px;
  601. color: #ffffff;
  602. line-height: 21px;
  603. }
  604. }
  605. .ai-panel {
  606. display: flex;
  607. img {
  608. width: 32px;
  609. margin-left: 13px;
  610. height: 32px;
  611. }
  612. .desc {
  613. max-width: calc(100% - 95px);
  614. background: #f5f8fc;
  615. border-radius: 6px 6px 6px 6px;
  616. padding: 10px 18px 10px 13px;
  617. font-weight: 500;
  618. overflow-y: auto;
  619. font-size: 14px;
  620. color: #333333;
  621. line-height: 28px;
  622. }
  623. .animation {
  624. @keyframes dot {
  625. 0%,
  626. 20% {
  627. content: '';
  628. }
  629. 40% {
  630. content: '.';
  631. }
  632. 60% {
  633. content: '..';
  634. }
  635. 80%,
  636. 100% {
  637. content: '...';
  638. }
  639. }
  640. .dots::after {
  641. display: inline-block;
  642. animation: dot 1.5s steps(1, end) infinite;
  643. content: '';
  644. }
  645. }
  646. }
  647. }
  648. }
  649. .input-panel {
  650. position: relative;
  651. bottom: 0px;
  652. background: #f5f8fc;
  653. border-radius: 6px;
  654. height: 111px;
  655. padding: 2px;
  656. width: calc(100% - 54px);
  657. margin-left: 25px;
  658. textarea {
  659. border: none;
  660. padding: 16px 22px;
  661. height: 100%;
  662. background: #f5f8fc;
  663. font-weight: 400;
  664. font-size: 16px;
  665. color: #333333;
  666. &:focus-visible {
  667. border: none !important;
  668. outline: none;
  669. box-shadow: none;
  670. }
  671. &:focus {
  672. border: none !important;
  673. outline: none;
  674. box-shadow: none;
  675. }
  676. }
  677. .send {
  678. right: 17px;
  679. bottom: 14px;
  680. cursor: pointer;
  681. width: 79px;
  682. height: 32px;
  683. background: linear-gradient(134deg, #1ba3fb 0%, #0745f6 100%);
  684. border-radius: 16px 16px 16px 16px;
  685. z-index: 2;
  686. position: absolute;
  687. display: flex;
  688. align-items: center;
  689. justify-content: center;
  690. img {
  691. width: 13px;
  692. height: 13px;
  693. margin-left: 6px;
  694. }
  695. font-family: PingFang SC, PingFang SC;
  696. font-weight: 400;
  697. font-size: 14px;
  698. color: #ffffff;
  699. }
  700. }
  701. }
  702. }
  703. }
  704. }
  705. .zm {
  706. width: 640px;
  707. }
  708. }
  709. .content-top {
  710. width: 100%;
  711. height: 80px;
  712. padding: 0 30px;
  713. display: flex;
  714. justify-content: space-between;
  715. align-items: center;
  716. border-bottom: 1px solid #e9e9e9;
  717. .left {
  718. .upload {
  719. font-size: 16px;
  720. color: #2185f2;
  721. cursor: pointer;
  722. img {
  723. margin-right: 5px;
  724. }
  725. }
  726. .ai-btn {
  727. width: 120px;
  728. height: 40px;
  729. background: url('/images/zcjd/ai-btn-icon.png');
  730. background-repeat: no-repeat;
  731. background-size: 100% 100%;
  732. border-radius: 12px;
  733. padding-left: 35px;
  734. margin-left: 20px;
  735. font-size: 18px;
  736. font-weight: 600;
  737. color: #ffffff;
  738. }
  739. }
  740. .right {
  741. height: 80px;
  742. line-height: 80px;
  743. color: #666666;
  744. span {
  745. margin-left: 20px;
  746. cursor: pointer;
  747. }
  748. span:hover {
  749. box-shadow: 1px 1px 1px 1px rgba(140, 140, 140, 0.2);
  750. }
  751. img {
  752. margin-right: 4px;
  753. width: 20px;
  754. height: 20px;
  755. }
  756. }
  757. }
  758. .content-detail {
  759. width: 100%;
  760. min-height: calc(100% - 80px);
  761. display: flex;
  762. .file-preview {
  763. flex: 1;
  764. text-align: center;
  765. .spin {
  766. display: flex;
  767. align-items: center;
  768. height: 100%;
  769. justify-content: center;
  770. }
  771. }
  772. }
  773. ::-webkit-scrollbar {
  774. width: 3px !important;
  775. }
  776. }
  777. </style>