index.vue 25 KB

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