index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. <template>
  2. <div class="chat-page">
  3. <div class="chat-wrap">
  4. <div class="chat-title">飞鸟种植大脑</div>
  5. <div class="chat-box" ref="chatBox">
  6. <div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.type">
  7. <div v-if="msg.type === 'user'" class="bubble">{{ msg.text }}</div>
  8. <div v-if="msg.type === 'system'" class="bubble answer">
  9. <div class="think">
  10. 思考中<el-icon><ArrowDown /></el-icon>
  11. </div>
  12. <div class="header" v-html="msg.text.header" :class="{'main-text': !msg.text.content}"></div>
  13. <div class="divider" v-if="msg.text.content"></div>
  14. <div class="content" v-html="msg.text.content"></div>
  15. <div class="table-wrap" v-if="msg.text.name === '2024年广东省各市的稻谷播种面积?' && msg.loadEnd">
  16. <el-table :data="tableData" border style="width: 100%">
  17. <el-table-column prop="city" label="市" width="100" />
  18. <el-table-column prop="area" label="2024年稻谷播种面积">
  19. <template #default="scope">
  20. {{ scope.row.area }}亩
  21. </template>
  22. </el-table-column>
  23. </el-table>
  24. </div>
  25. <div class="table-wrap" v-if="msg.text.name === '花期统防统治报表,有没有与农资对接' && msg.loadEnd">
  26. <el-table :data="tableData2" border style="width: 100%">
  27. <el-table-column prop="name" label="企业名称" />
  28. <el-table-column prop="area" label="覆盖范围" show-overflow-tooltip />
  29. <el-table-column prop="step" label="完成进度" show-overflow-tooltip />
  30. <el-table-column prop="rate" label="系统/信用评分" show-overflow-tooltip />
  31. <el-table-column prop="farmRate" label="农户评价" show-overflow-tooltip />
  32. <el-table-column prop="result" label="成效" show-overflow-tooltip />
  33. </el-table>
  34. </div>
  35. </div>
  36. <div v-if="msg.type === 'real'" class="bubble answer">
  37. <div class="think" v-if="!msg.text.name">
  38. 思考中<el-icon><ArrowDown /></el-icon>
  39. </div>
  40. <div class="header" v-html="deepSeekAsk.markdownToHtml(msg.text.header)"></div>
  41. <div class="divider"></div>
  42. <div class="content" v-html="deepSeekAsk.markdownToHtml(msg.text.content)"></div>
  43. </div>
  44. <div v-if="msg.type === 'auto'" class="system auto">
  45. <div class="bubble">
  46. {{ msg.text.header }}
  47. <div>
  48. <div class="ask-title">你可以试着问我</div>
  49. <div class="ask-list">
  50. <li
  51. class="ask-item cursor-pointer"
  52. @click="askText('2024年广东省各市的稻谷播种面积?')"
  53. >
  54. 2024年广东省各市的稻谷播种面积?
  55. </li>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. <!-- 猜你想问 -->
  61. <div v-if="msg.type === 'ask'" class="system ask">
  62. <div class="bubble">
  63. <div class="ask-title">你可以试着问我</div>
  64. <div class="ask-list">
  65. <div class="to-map" v-for="(ask, askI) in msg.text.content" :key="askI" @click="toMapLayer(ask)">
  66. <li>
  67. {{ ask }}
  68. </li>
  69. <div class="go-icon"><el-icon><ArrowRight /></el-icon></div>
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. </div>
  76. <div class="bottom-send">
  77. <div class="input-box">
  78. <el-input
  79. v-model="userInput"
  80. :autosize="{ minRows: 2, maxRows: 8 }"
  81. type="textarea"
  82. placeholder="给 飞鸟种植大脑 提问吧~"
  83. @keyup.enter="sendMessage"
  84. />
  85. <div class="bottom-group">
  86. <div class="btn-l">
  87. <div class="l-item">
  88. <img src="@/assets/images/warningHome/chat/think.png" />
  89. 深度思考(R1)
  90. </div>
  91. <div class="l-item">
  92. <img src="@/assets/images/warningHome/chat/net.png" />
  93. 联网搜索
  94. </div>
  95. </div>
  96. <div class="btn-r">
  97. <img class="file-icon" src="@/assets/images/warningHome/chat/file.png" />
  98. <img class="send-icon" @click="sendMessage" src="@/assets/images/warningHome/chat/send.png" alt="send">
  99. </div>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. </div>
  105. </template>
  106. <script setup>
  107. import { nextTick, onMounted, ref } from "vue";
  108. import { useRoute, useRouter } from "vue-router";
  109. import chat from "./chat.json";
  110. import DeepSeekAsk from "./deepSeekAsk";
  111. import { useStore } from "vuex";
  112. import eventBus from "@/api/eventBus";
  113. const store = useStore();
  114. let userId = 12321;
  115. let deepSeekAsk = new DeepSeekAsk(userId);
  116. const router = useRouter();
  117. const userInput = ref("");
  118. const messages = ref([]);
  119. const isProcessing = ref(false); // 控制是否在处理消息
  120. const tableData = ref([
  121. { city: '广州', area: 360091 },
  122. { city: '深圳', area: 14726 },
  123. { city: '珠海', area: 66670 },
  124. { city: '汕头', area: 690567 },
  125. { city: '佛山', area: 104095 },
  126. { city: '韶关', area: 1534904 },
  127. { city: '河源', area: 1836469 },
  128. { city: '梅州', area: 2428360 },
  129. { city: '惠州', area: 1275493 },
  130. { city: '汕尾', area: 1033786 },
  131. { city: '东莞', area: 24106 },
  132. { city: '中山', area: 37709 },
  133. { city: '江门', area: 2523802 },
  134. { city: '阳江', area: 1604316 },
  135. { city: '湛江', area: 3357751 },
  136. { city: '茂名', area: 3151002 },
  137. { city: '肇庆', area: 2521029 },
  138. { city: '清远', area: 1832838 },
  139. { city: '潮州', area: 481617 },
  140. { city: '揭阳', area: 1217258 },
  141. { city: '云浮', area: 1317616 },
  142. ])
  143. const tableData2 = ref([
  144. {name: "天合股份", area: "广东省及周边省份,100家配送中心", step: "2024年上半年服务243.71万亩,带动增收1.11亿元;2025年目标完成率约24%", rate: "未明确评分", farmRate: "服务超6万户,节本增收显著", result: "绿色农资全覆盖,建立“耕、种、管、收”全程服务体系"},
  145. {name: "大炎农业", area: "广东省及周边", step: "承担17.84万亩水稻、6.17万亩蔬菜物化补贴;带动654户农户销售226.1吨", rate: "绿色补贴覆盖率100%", farmRate: "合作社带动增收,农户组织化程度提高", result: "助农销售1.3亿元,建立稳定增收渠道"},
  146. {name: "美荔公司", area: "广东省及周边", step: "完成30万亩次测土配方施肥目标", rate: "A级信用评定", farmRate: "守信经营,服务标准化受认可", result: "保障农资质量安全,推动化肥减量增效,覆盖本地10家A级企业"},
  147. ])
  148. const steps = [
  149. { type: "auto", text: { header: "您好,飞鸟智慧种植大脑是您的私人管家" }, loadEnd: false },
  150. {
  151. type: "ask",
  152. text: {
  153. askHeader: "当前高州的作物分布",
  154. askContent: ["当前高州的作物分布",],
  155. },
  156. loadEnd: false
  157. },
  158. {
  159. type: "ask",
  160. text: {
  161. askHeader: "当前区域荔枝有什么生长风险?",
  162. askContent: ["当前区域荔枝有什么生长风险?"],
  163. },
  164. loadEnd: false
  165. },
  166. {
  167. type: "ask",
  168. text: {
  169. askHeader: "当前区域哪些有荔枝地块有病虫害",
  170. askContent: ["当前区域哪些有荔枝地块有病虫害"],
  171. },
  172. loadEnd: false
  173. },
  174. {
  175. type: "ask",
  176. text: {
  177. askHeader: "现在哪些地方有农情需求",
  178. askContent: ["现在哪些地方有农情需求"],
  179. },
  180. loadEnd: false
  181. },
  182. {
  183. type: "ask",
  184. text: {
  185. askHeader: "哪些地方有植保机",
  186. askContent: ["哪些地方有植保机"],
  187. },
  188. loadEnd: false
  189. },
  190. {
  191. type: "ask",
  192. text: {
  193. askHeader: "花期统防统治报表,有没有与农资对接",
  194. askContent: ["花期统防统治报表,有没有与农资对接"],
  195. },
  196. loadEnd: false
  197. },
  198. ];
  199. const stepIndex = ref(0);
  200. const triggerNextStep = () => {
  201. if (stepIndex.value < steps.length) {
  202. addSystemReply(
  203. steps[stepIndex.value].type,
  204. steps[stepIndex.value].text,
  205. () => {
  206. isProcessing.value = false;
  207. // steps[stepIndex.value].loadEnd = true
  208. }
  209. );
  210. stepIndex.value++;
  211. saveState();
  212. scrollToBottom();
  213. }
  214. };
  215. const loadState = () => {
  216. const storedMessages = localStorage.getItem(STORAGE_KEY);
  217. const storedDate = localStorage.getItem(DATE_KEY);
  218. const storedStep = localStorage.getItem(STEP_KEY);
  219. const today = new Date().toISOString().split("T")[0];
  220. if (storedDate === today && storedMessages) {
  221. messages.value = JSON.parse(storedMessages);
  222. stepIndex.value = storedStep ? parseInt(storedStep, 10) : 0;
  223. nextTick(() => scrollToBottom());
  224. } else {
  225. localStorage.removeItem(STORAGE_KEY);
  226. localStorage.removeItem(DATE_KEY);
  227. localStorage.removeItem(STEP_KEY);
  228. stepIndex.value = 0;
  229. }
  230. };
  231. const toMapLayer = (name) => {
  232. eventBus.emit("chat:showMapLayer", name)
  233. askText(name)
  234. // addSystemReply('system', {header: name, content: '', name}, () => {
  235. // steps[stepIndex.value].loadEnd = true
  236. // });
  237. }
  238. const askText = (val) => {
  239. userInput.value = val;
  240. sendMessage();
  241. };
  242. const sendMessage = () => {
  243. if (!userInput.value.trim()) return;
  244. // 先保存用户输入的内容
  245. const userText = userInput.value;
  246. // 添加用户消息
  247. messages.value.push({ text: userInput.value, type: "user" });
  248. saveMessages();
  249. scrollToBottom();
  250. // 模拟系统回复
  251. // setTimeout(() => {
  252. // console.log("userInput", userText);
  253. // messages.value.push({ text: "系统回复: " + userText, type: "system" });
  254. // }, 500);
  255. // 模拟 AI 逐字回复
  256. let isSearch = true;
  257. console.log("userText", userText);
  258. chat.map((item) => {
  259. if (userText.indexOf(item.name) !== -1) {
  260. addSystemReply('system', {header: item.header, content: item.content, name: item.name}, () => {
  261. // steps[stepIndex.value].loadEnd = true
  262. messages.value[messages.value.length - 1].loadEnd = true
  263. setTimeout(triggerNextStep, 2000);
  264. });
  265. isSearch = false;
  266. }
  267. });
  268. if (isSearch) {
  269. messages.value.push({ type: "real", text: { header: "", content: "" } });
  270. deepSeekAsk.ask(userText, function (code) {
  271. if (code == 0) {
  272. let intervalId = setInterval(() => {
  273. if (deepSeekAsk.end) {
  274. clearInterval(intervalId);
  275. }
  276. for (let content of deepSeekAsk.contents) {
  277. console.log(content.content);
  278. messages.value[messages.value.length - 1].text.content = content.content;
  279. messages.value[messages.value.length - 1].text.header = content.header;
  280. scrollToBottom();
  281. saveMessages();
  282. }
  283. }, 1000);
  284. }
  285. });
  286. }
  287. // 清空输入框
  288. userInput.value = "";
  289. };
  290. const loadEnd = ref(false);
  291. // **逐字显示系统回复(header 和 content)**
  292. const addSystemReply = (type = "system", textObject, callback) => {
  293. isProcessing.value = true
  294. let currentHeader = "";
  295. let currentContent = "";
  296. if (type === "ask") {
  297. messages.value.push({ text: { header: textObject.askHeader, content: textObject.askContent, }, type });
  298. } else {
  299. messages.value.push({ text: { header: currentHeader, content: currentContent, name: textObject.name }, type });
  300. }
  301. saveMessages();
  302. scrollToBottom();
  303. const content = textObject.content
  304. const header = textObject.header
  305. // **逐字显示 content**
  306. const showContent = () => {
  307. let contentIndex = 0;
  308. if (content && content.length > 0) {
  309. const contentInterval = setInterval(() => {
  310. if (contentIndex < content.length) {
  311. currentContent += content[contentIndex];
  312. if (type !== "ask") {
  313. messages.value[messages.value.length - 1].text.content = currentContent;
  314. }
  315. saveMessages();
  316. scrollToBottom();
  317. contentIndex++;
  318. } else {
  319. clearInterval(contentInterval);
  320. messages.value[messages.value.length - 1].loadEnd = true
  321. scrollToBottom();
  322. callback && callback(); // 回复完成后解锁输入
  323. }
  324. }, 5);
  325. } else {
  326. callback && callback(); // 如果 content 为空,直接解锁输入
  327. }
  328. };
  329. let headerIndex = 0;
  330. if (header && header.length > 0) {
  331. const headerInterval = setInterval(() => {
  332. if (headerIndex < header.length) {
  333. currentHeader += header[headerIndex];
  334. messages.value[messages.value.length - 1].text.header = currentHeader;
  335. saveMessages();
  336. scrollToBottom();
  337. headerIndex++;
  338. } else {
  339. clearInterval(headerInterval);
  340. showContent();
  341. }
  342. }, 5); // 50ms 逐字显示 header
  343. } else {
  344. showContent();
  345. }
  346. };
  347. const saveState = () => {
  348. const today = new Date().toISOString().split("T")[0];
  349. localStorage.setItem(STORAGE_KEY, JSON.stringify(messages.value));
  350. localStorage.setItem(DATE_KEY, today);
  351. localStorage.setItem(STEP_KEY, stepIndex.value);
  352. };
  353. // **存入缓存**
  354. const STORAGE_KEY = "chatMessages";
  355. const DATE_KEY = "chatDate";
  356. const STEP_KEY = "chatStep";
  357. const saveMessages = () => {
  358. const today = new Date().toISOString().split("T")[0]; // 仅保存 "YYYY-MM-DD"
  359. localStorage.setItem(STORAGE_KEY, JSON.stringify(messages.value));
  360. localStorage.setItem(DATE_KEY, today);
  361. };
  362. // **恢复缓存**
  363. const loadMessages = () => {
  364. const storedMessages = localStorage.getItem(STORAGE_KEY);
  365. const storedDate = localStorage.getItem(DATE_KEY);
  366. const today = new Date().toISOString().split("T")[0]; // 获取今天的日期
  367. if (storedDate === today && storedMessages) {
  368. messages.value = JSON.parse(storedMessages);
  369. } else {
  370. // 清除过期数据
  371. localStorage.removeItem(STORAGE_KEY);
  372. localStorage.removeItem(DATE_KEY);
  373. }
  374. };
  375. // 路由跳转
  376. const toOtherPage = (val) => {
  377. router.push(val);
  378. };
  379. const chatBox = ref();
  380. onMounted(() => {
  381. // loadMessages()
  382. // scrollToBottom()
  383. loadState();
  384. nextTick(() => {
  385. setTimeout(triggerNextStep, 1000);
  386. });
  387. // setTimeout(() => {
  388. // chatBox.value.scrollTo({top: chatBox.value.scrollHeight, behavior: 'smooth' }); // 滚动到页面顶部
  389. // }, 200)
  390. });
  391. eventBus.on("chat:hideMapLayer", () => {
  392. setTimeout(triggerNextStep, 2000);
  393. })
  394. // **滚动到底部**
  395. const scrollToBottom = () => {
  396. nextTick(() => {
  397. if (chatBox.value) {
  398. chatBox.value.scrollTo({ top: chatBox.value.scrollHeight, behavior: "smooth" }); // 滚动到页面底部
  399. }
  400. });
  401. };
  402. </script>
  403. <style lang="scss" scoped>
  404. .chat-page {
  405. height: 100%;
  406. padding-top: 34px;
  407. box-sizing: border-box;
  408. .chat-wrap {
  409. height: 100%;
  410. background: #232323;
  411. border-radius: 8px;
  412. border: 1px solid #777777;
  413. display: flex;
  414. flex-direction: column;
  415. justify-content: space-between;
  416. box-sizing: border-box;
  417. .chat-title {
  418. border-radius: 8px 8px 0 0;
  419. background: #2F2F2F;
  420. text-align: center;
  421. padding: 10px 0;
  422. }
  423. }
  424. }
  425. .chat-container {
  426. display: flex;
  427. flex-direction: column;
  428. height: 100vh;
  429. justify-content: space-between;
  430. padding: 10px;
  431. box-sizing: border-box;
  432. background: #fff;
  433. }
  434. .chat-box {
  435. flex: 1;
  436. overflow-y: auto;
  437. padding: 16px;
  438. }
  439. .message {
  440. display: flex;
  441. margin: 10px 0;
  442. .avatar {
  443. width: 28px;
  444. height: 28px;
  445. margin-right: 8px;
  446. }
  447. }
  448. .table-wrap {
  449. padding-top: 12px;
  450. width: 100%;
  451. overflow: auto;
  452. ::v-deep {
  453. .el-table .el-table__header th.el-table__cell {
  454. background: #3B3B3B !important;
  455. border-bottom-color: #555555;
  456. border-right-color: #555555;
  457. }
  458. .el-table {
  459. color: #fff;
  460. }
  461. .el-table tr {
  462. background: #2F2F2F;
  463. pointer-events: none;
  464. }
  465. .el-table thead {
  466. color: #999999;
  467. }
  468. .el-table td.el-table__cell {
  469. border-color: #555555;
  470. }
  471. .el-table--border .el-table__inner-wrapper:after, .el-table--border:after, .el-table--border:before, .el-table__inner-wrapper:before,
  472. .el-table__border-bottom-patch, .el-table__border-left-patch{
  473. background-color: #555555;
  474. }
  475. }
  476. }
  477. .ask {
  478. width: 100%;
  479. display: flex;
  480. .bubble {
  481. width: 100%;
  482. }
  483. }
  484. .ask-title {
  485. color: #999999;
  486. padding-bottom: 10px;
  487. }
  488. .ask-list {
  489. color: #FFD489;
  490. }
  491. .to-map {
  492. display: flex;
  493. justify-content: space-between;
  494. background: #3C3C3C;
  495. padding: 6px 8px;
  496. border-radius: 6px;
  497. cursor: pointer;
  498. }
  499. .to-map + .to-map {
  500. margin-top: 8px;
  501. }
  502. .ask-item + .ask-item {
  503. padding-top: 2px;
  504. }
  505. .route-wrap {
  506. display: flex;
  507. .route-img {
  508. width: 100%;
  509. padding-top: 8px;
  510. }
  511. }
  512. .user {
  513. justify-content: flex-end;
  514. }
  515. .system {
  516. justify-content: flex-start;
  517. }
  518. .link {
  519. display: flex;
  520. .header-link {
  521. color: #FFD489;
  522. display: flex;
  523. align-items: baseline;
  524. text-decoration: underline;
  525. .icon {
  526. margin-right: 5px;
  527. }
  528. }
  529. }
  530. .bubble {
  531. padding: 16px 12px;
  532. box-sizing: border-box;
  533. max-width: 100%;
  534. border-radius: 8px;
  535. // max-width: 60%;
  536. background: rgba(255, 212, 137, 0.1);
  537. color: #FFD489;
  538. border-radius: 16px 2px 16px 16px;
  539. line-height: 24px;
  540. font-size: 14px;
  541. .header {
  542. color: #999999;
  543. // padding-bottom: 8px;
  544. }
  545. .main-text {
  546. color: #ffffff;
  547. }
  548. .content {
  549. color: #ffffff;
  550. }
  551. .divider {
  552. width: 100%;
  553. height: 1px;
  554. background: rgba(153, 153, 153, 0.2);
  555. margin: 12px 0;
  556. }
  557. .think {
  558. color: #999999;
  559. }
  560. .expert-img {
  561. margin-top: 12px;
  562. width: 100%;
  563. }
  564. }
  565. .system .bubble,
  566. .real .bubble {
  567. background: #2F2F2F;
  568. color: #fff;
  569. border-radius: 2px 16px 16px 16px;
  570. }
  571. .uploader {
  572. width: 100%;
  573. display: flex;
  574. justify-content: center;
  575. img {
  576. width: 174px;
  577. height: 55px;
  578. }
  579. }
  580. .bottom-send {
  581. padding: 6px 16px 16px;
  582. }
  583. .input-box {
  584. display: flex;
  585. flex-direction: column;
  586. padding: 10px;
  587. background: #2F2F2F;
  588. border-radius: 16px;
  589. /* border-top: 1px solid #ddd; */
  590. ::v-deep {
  591. .el-textarea__inner {
  592. background: transparent;
  593. box-shadow: none;
  594. color: #fff;
  595. }
  596. }
  597. .bottom-group {
  598. display: flex;
  599. justify-content: space-between;
  600. align-items: center;
  601. padding-top: 6px;
  602. .btn-l {
  603. display: flex;
  604. .l-item {
  605. display: flex;
  606. align-items: center;
  607. border-radius: 20px;
  608. border: 1px solid #555555;
  609. padding: 6px 8px;
  610. }
  611. .l-item + .l-item {
  612. margin-left: 12px;
  613. }
  614. img {
  615. width: 16px;
  616. padding-right: 2px;
  617. }
  618. }
  619. .file-icon {
  620. // width: 16px;
  621. height: 16px;
  622. }
  623. .send-icon {
  624. margin-left: 8px;
  625. width: 28px;
  626. }
  627. }
  628. }
  629. input {
  630. flex: 1;
  631. padding: 10px;
  632. border: 1px solid #ddd;
  633. border-radius: 4px;
  634. }
  635. button {
  636. margin-left: 10px;
  637. padding: 10px 15px;
  638. background: #007bff;
  639. color: white;
  640. border: none;
  641. cursor: pointer;
  642. }
  643. </style>