123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 |
- <template>
- <div class="chat-page">
- <div class="chat-wrap">
- <div class="chat-title">飞鸟种植大脑</div>
- <div class="chat-box" ref="chatBox">
- <div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.type">
- <div v-if="msg.type === 'user'" class="bubble">{{ msg.text }}</div>
- <div v-if="msg.type === 'system'" class="bubble answer">
- <div class="think">
- 思考中<el-icon><ArrowDown /></el-icon>
- </div>
- <div class="header" v-html="msg.text.header" :class="{'main-text': !msg.text.content}"></div>
- <div class="divider" v-if="msg.text.content"></div>
- <div class="content" v-html="msg.text.content"></div>
- <div class="table-wrap" v-if="msg.text.name === '2024年广东省各市的稻谷播种面积?' && msg.loadEnd">
- <el-table :data="tableData" border style="width: 100%">
- <el-table-column prop="city" label="市" width="100" />
- <el-table-column prop="area" label="2024年稻谷播种面积">
- <template #default="scope">
- {{ scope.row.area }}亩
- </template>
- </el-table-column>
- </el-table>
- </div>
- <div class="table-wrap" v-if="msg.text.name === '花期统防统治报表,有没有与农资对接' && msg.loadEnd">
- <el-table :data="tableData2" border style="width: 100%">
- <el-table-column prop="name" label="企业名称" />
- <el-table-column prop="area" label="覆盖范围" show-overflow-tooltip />
- <el-table-column prop="step" label="完成进度" show-overflow-tooltip />
- <el-table-column prop="rate" label="系统/信用评分" show-overflow-tooltip />
- <el-table-column prop="farmRate" label="农户评价" show-overflow-tooltip />
- <el-table-column prop="result" label="成效" show-overflow-tooltip />
- </el-table>
- </div>
- </div>
- <div v-if="msg.type === 'real'" class="bubble answer">
- <div class="think" v-if="!msg.text.name">
- 思考中<el-icon><ArrowDown /></el-icon>
- </div>
- <div class="header" v-html="deepSeekAsk.markdownToHtml(msg.text.header)"></div>
- <div class="divider"></div>
- <div class="content" v-html="deepSeekAsk.markdownToHtml(msg.text.content)"></div>
- </div>
- <div v-if="msg.type === 'auto'" class="system auto">
- <div class="bubble">
- {{ msg.text.header }}
- <div>
- <div class="ask-title">你可以试着问我</div>
- <div class="ask-list">
- <li
- class="ask-item cursor-pointer"
- @click="askText('2024年广东省各市的稻谷播种面积?')"
- >
- 2024年广东省各市的稻谷播种面积?
- </li>
- </div>
- </div>
- </div>
- </div>
- <!-- 猜你想问 -->
- <div v-if="msg.type === 'ask'" class="system ask">
- <div class="bubble">
- <div class="ask-title">你可以试着问我</div>
- <div class="ask-list">
- <div class="to-map" v-for="(ask, askI) in msg.text.content" :key="askI" @click="toMapLayer(ask)">
- <li>
- {{ ask }}
- </li>
- <div class="go-icon"><el-icon><ArrowRight /></el-icon></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="bottom-send">
- <div class="input-box">
- <el-input
- v-model="userInput"
- :autosize="{ minRows: 2, maxRows: 8 }"
- type="textarea"
- placeholder="给 飞鸟种植大脑 提问吧~"
- @keyup.enter="sendMessage"
- />
- <div class="bottom-group">
- <div class="btn-l">
- <div class="l-item">
- <img src="@/assets/images/warningHome/chat/think.png" />
- 深度思考(R1)
- </div>
- <div class="l-item">
- <img src="@/assets/images/warningHome/chat/net.png" />
- 联网搜索
- </div>
- </div>
- <div class="btn-r">
- <img class="file-icon" src="@/assets/images/warningHome/chat/file.png" />
- <img class="send-icon" @click="sendMessage" src="@/assets/images/warningHome/chat/send.png" alt="send">
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { nextTick, onMounted, ref } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import chat from "./chat.json";
- import DeepSeekAsk from "./deepSeekAsk";
- import { useStore } from "vuex";
- import eventBus from "@/api/eventBus";
- const store = useStore();
- let userId = 12321;
- let deepSeekAsk = new DeepSeekAsk(userId);
- const router = useRouter();
- const userInput = ref("");
- const messages = ref([]);
- const isProcessing = ref(false); // 控制是否在处理消息
- const tableData = ref([
- { city: '广州', area: 360091 },
- { city: '深圳', area: 14726 },
- { city: '珠海', area: 66670 },
- { city: '汕头', area: 690567 },
- { city: '佛山', area: 104095 },
- { city: '韶关', area: 1534904 },
- { city: '河源', area: 1836469 },
- { city: '梅州', area: 2428360 },
- { city: '惠州', area: 1275493 },
- { city: '汕尾', area: 1033786 },
- { city: '东莞', area: 24106 },
- { city: '中山', area: 37709 },
- { city: '江门', area: 2523802 },
- { city: '阳江', area: 1604316 },
- { city: '湛江', area: 3357751 },
- { city: '茂名', area: 3151002 },
- { city: '肇庆', area: 2521029 },
- { city: '清远', area: 1832838 },
- { city: '潮州', area: 481617 },
- { city: '揭阳', area: 1217258 },
- { city: '云浮', area: 1317616 },
- ])
- const tableData2 = ref([
- {name: "天合股份", area: "广东省及周边省份,100家配送中心", step: "2024年上半年服务243.71万亩,带动增收1.11亿元;2025年目标完成率约24%", rate: "未明确评分", farmRate: "服务超6万户,节本增收显著", result: "绿色农资全覆盖,建立“耕、种、管、收”全程服务体系"},
- {name: "大炎农业", area: "广东省及周边", step: "承担17.84万亩水稻、6.17万亩蔬菜物化补贴;带动654户农户销售226.1吨", rate: "绿色补贴覆盖率100%", farmRate: "合作社带动增收,农户组织化程度提高", result: "助农销售1.3亿元,建立稳定增收渠道"},
- {name: "美荔公司", area: "广东省及周边", step: "完成30万亩次测土配方施肥目标", rate: "A级信用评定", farmRate: "守信经营,服务标准化受认可", result: "保障农资质量安全,推动化肥减量增效,覆盖本地10家A级企业"},
- ])
- const steps = [
- { type: "auto", text: { header: "您好,飞鸟智慧种植大脑是您的私人管家" }, loadEnd: false },
- {
- type: "ask",
- text: {
- askHeader: "当前高州的作物分布",
- askContent: ["当前高州的作物分布",],
- },
- loadEnd: false
- },
- {
- type: "ask",
- text: {
- askHeader: "当前区域荔枝有什么生长风险?",
- askContent: ["当前区域荔枝有什么生长风险?"],
- },
- loadEnd: false
- },
- {
- type: "ask",
- text: {
- askHeader: "当前区域哪些有荔枝地块有病虫害",
- askContent: ["当前区域哪些有荔枝地块有病虫害"],
- },
- loadEnd: false
- },
- {
- type: "ask",
- text: {
- askHeader: "现在哪些地方有农情需求",
- askContent: ["现在哪些地方有农情需求"],
- },
- loadEnd: false
- },
- {
- type: "ask",
- text: {
- askHeader: "哪些地方有植保机",
- askContent: ["哪些地方有植保机"],
- },
- loadEnd: false
- },
- {
- type: "ask",
- text: {
- askHeader: "花期统防统治报表,有没有与农资对接",
- askContent: ["花期统防统治报表,有没有与农资对接"],
- },
- loadEnd: false
- },
- ];
- const stepIndex = ref(0);
- const triggerNextStep = () => {
- if (stepIndex.value < steps.length) {
- addSystemReply(
- steps[stepIndex.value].type,
- steps[stepIndex.value].text,
- () => {
- isProcessing.value = false;
- // steps[stepIndex.value].loadEnd = true
- }
- );
- stepIndex.value++;
- saveState();
- scrollToBottom();
- }
- };
- const loadState = () => {
- const storedMessages = localStorage.getItem(STORAGE_KEY);
- const storedDate = localStorage.getItem(DATE_KEY);
- const storedStep = localStorage.getItem(STEP_KEY);
- const today = new Date().toISOString().split("T")[0];
- if (storedDate === today && storedMessages) {
- messages.value = JSON.parse(storedMessages);
- stepIndex.value = storedStep ? parseInt(storedStep, 10) : 0;
- nextTick(() => scrollToBottom());
- } else {
- localStorage.removeItem(STORAGE_KEY);
- localStorage.removeItem(DATE_KEY);
- localStorage.removeItem(STEP_KEY);
- stepIndex.value = 0;
- }
- };
- const toMapLayer = (name) => {
- eventBus.emit("chat:showMapLayer", name)
- askText(name)
- // addSystemReply('system', {header: name, content: '', name}, () => {
- // steps[stepIndex.value].loadEnd = true
- // });
- }
- const askText = (val) => {
- userInput.value = val;
- sendMessage();
- };
- const sendMessage = () => {
- if (!userInput.value.trim()) return;
- // 先保存用户输入的内容
- const userText = userInput.value;
- // 添加用户消息
- messages.value.push({ text: userInput.value, type: "user" });
- saveMessages();
- scrollToBottom();
- // 模拟系统回复
- // setTimeout(() => {
- // console.log("userInput", userText);
- // messages.value.push({ text: "系统回复: " + userText, type: "system" });
- // }, 500);
- // 模拟 AI 逐字回复
- let isSearch = true;
- console.log("userText", userText);
- chat.map((item) => {
- if (userText.indexOf(item.name) !== -1) {
- addSystemReply('system', {header: item.header, content: item.content, name: item.name}, () => {
- // steps[stepIndex.value].loadEnd = true
- messages.value[messages.value.length - 1].loadEnd = true
- setTimeout(triggerNextStep, 2000);
- });
- isSearch = false;
- }
- });
- if (isSearch) {
- messages.value.push({ type: "real", text: { header: "", content: "" } });
- deepSeekAsk.ask(userText, function (code) {
- if (code == 0) {
- let intervalId = setInterval(() => {
- if (deepSeekAsk.end) {
- clearInterval(intervalId);
- }
- for (let content of deepSeekAsk.contents) {
- console.log(content.content);
- messages.value[messages.value.length - 1].text.content = content.content;
- messages.value[messages.value.length - 1].text.header = content.header;
- scrollToBottom();
- saveMessages();
- }
- }, 1000);
- }
- });
- }
- // 清空输入框
- userInput.value = "";
- };
- const loadEnd = ref(false);
- // **逐字显示系统回复(header 和 content)**
- const addSystemReply = (type = "system", textObject, callback) => {
- isProcessing.value = true
- let currentHeader = "";
- let currentContent = "";
- if (type === "ask") {
- messages.value.push({ text: { header: textObject.askHeader, content: textObject.askContent, }, type });
- } else {
- messages.value.push({ text: { header: currentHeader, content: currentContent, name: textObject.name }, type });
- }
- saveMessages();
- scrollToBottom();
- const content = textObject.content
- const header = textObject.header
- // **逐字显示 content**
- const showContent = () => {
- let contentIndex = 0;
- if (content && content.length > 0) {
- const contentInterval = setInterval(() => {
- if (contentIndex < content.length) {
- currentContent += content[contentIndex];
- if (type !== "ask") {
- messages.value[messages.value.length - 1].text.content = currentContent;
- }
- saveMessages();
- scrollToBottom();
- contentIndex++;
- } else {
- clearInterval(contentInterval);
- messages.value[messages.value.length - 1].loadEnd = true
- scrollToBottom();
- callback && callback(); // 回复完成后解锁输入
- }
- }, 5);
- } else {
- callback && callback(); // 如果 content 为空,直接解锁输入
- }
- };
- let headerIndex = 0;
- if (header && header.length > 0) {
- const headerInterval = setInterval(() => {
- if (headerIndex < header.length) {
- currentHeader += header[headerIndex];
- messages.value[messages.value.length - 1].text.header = currentHeader;
- saveMessages();
- scrollToBottom();
- headerIndex++;
- } else {
- clearInterval(headerInterval);
- showContent();
- }
- }, 5); // 50ms 逐字显示 header
- } else {
- showContent();
- }
- };
- const saveState = () => {
- const today = new Date().toISOString().split("T")[0];
- localStorage.setItem(STORAGE_KEY, JSON.stringify(messages.value));
- localStorage.setItem(DATE_KEY, today);
- localStorage.setItem(STEP_KEY, stepIndex.value);
- };
- // **存入缓存**
- const STORAGE_KEY = "chatMessages";
- const DATE_KEY = "chatDate";
- const STEP_KEY = "chatStep";
- const saveMessages = () => {
- const today = new Date().toISOString().split("T")[0]; // 仅保存 "YYYY-MM-DD"
- localStorage.setItem(STORAGE_KEY, JSON.stringify(messages.value));
- localStorage.setItem(DATE_KEY, today);
- };
- // **恢复缓存**
- const loadMessages = () => {
- const storedMessages = localStorage.getItem(STORAGE_KEY);
- const storedDate = localStorage.getItem(DATE_KEY);
- const today = new Date().toISOString().split("T")[0]; // 获取今天的日期
- if (storedDate === today && storedMessages) {
- messages.value = JSON.parse(storedMessages);
- } else {
- // 清除过期数据
- localStorage.removeItem(STORAGE_KEY);
- localStorage.removeItem(DATE_KEY);
- }
- };
- // 路由跳转
- const toOtherPage = (val) => {
- router.push(val);
- };
- const chatBox = ref();
- onMounted(() => {
- // loadMessages()
- // scrollToBottom()
- loadState();
- nextTick(() => {
- setTimeout(triggerNextStep, 1000);
- });
- // setTimeout(() => {
- // chatBox.value.scrollTo({top: chatBox.value.scrollHeight, behavior: 'smooth' }); // 滚动到页面顶部
- // }, 200)
- });
- eventBus.on("chat:hideMapLayer", () => {
- setTimeout(triggerNextStep, 2000);
- })
- // **滚动到底部**
- const scrollToBottom = () => {
- nextTick(() => {
- if (chatBox.value) {
- chatBox.value.scrollTo({ top: chatBox.value.scrollHeight, behavior: "smooth" }); // 滚动到页面底部
- }
- });
- };
- </script>
- <style lang="scss" scoped>
- .chat-page {
- height: 100%;
- padding-top: 34px;
- box-sizing: border-box;
- .chat-wrap {
- height: 100%;
- background: #232323;
- border-radius: 8px;
- border: 1px solid #777777;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- box-sizing: border-box;
- .chat-title {
- border-radius: 8px 8px 0 0;
- background: #2F2F2F;
- text-align: center;
- padding: 10px 0;
- }
- }
- }
- .chat-container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- justify-content: space-between;
- padding: 10px;
- box-sizing: border-box;
- background: #fff;
- }
- .chat-box {
- flex: 1;
- overflow-y: auto;
- padding: 16px;
-
- }
- .message {
- display: flex;
- margin: 10px 0;
- .avatar {
- width: 28px;
- height: 28px;
- margin-right: 8px;
- }
- }
- .table-wrap {
- padding-top: 12px;
- width: 100%;
- overflow: auto;
- ::v-deep {
- .el-table .el-table__header th.el-table__cell {
- background: #3B3B3B !important;
- border-bottom-color: #555555;
- border-right-color: #555555;
- }
- .el-table {
- color: #fff;
- }
- .el-table tr {
- background: #2F2F2F;
- pointer-events: none;
- }
- .el-table thead {
- color: #999999;
- }
- .el-table td.el-table__cell {
- border-color: #555555;
- }
- .el-table--border .el-table__inner-wrapper:after, .el-table--border:after, .el-table--border:before, .el-table__inner-wrapper:before,
- .el-table__border-bottom-patch, .el-table__border-left-patch{
- background-color: #555555;
- }
- }
- }
- .ask {
- width: 100%;
- display: flex;
- .bubble {
- width: 100%;
- }
- }
- .ask-title {
- color: #999999;
- padding-bottom: 10px;
- }
- .ask-list {
- color: #FFD489;
- }
- .to-map {
- display: flex;
- justify-content: space-between;
- background: #3C3C3C;
- padding: 6px 8px;
- border-radius: 6px;
- cursor: pointer;
- }
- .to-map + .to-map {
- margin-top: 8px;
- }
- .ask-item + .ask-item {
- padding-top: 2px;
- }
- .route-wrap {
- display: flex;
- .route-img {
- width: 100%;
- padding-top: 8px;
- }
- }
- .user {
- justify-content: flex-end;
- }
- .system {
- justify-content: flex-start;
- }
- .link {
- display: flex;
- .header-link {
- color: #FFD489;
- display: flex;
- align-items: baseline;
- text-decoration: underline;
- .icon {
- margin-right: 5px;
- }
- }
- }
- .bubble {
- padding: 16px 12px;
- box-sizing: border-box;
- max-width: 100%;
- border-radius: 8px;
- // max-width: 60%;
- background: rgba(255, 212, 137, 0.1);
- color: #FFD489;
- border-radius: 16px 2px 16px 16px;
- line-height: 24px;
- font-size: 14px;
- .header {
- color: #999999;
- // padding-bottom: 8px;
- }
- .main-text {
- color: #ffffff;
- }
- .content {
- color: #ffffff;
- }
- .divider {
- width: 100%;
- height: 1px;
- background: rgba(153, 153, 153, 0.2);
- margin: 12px 0;
- }
- .think {
- color: #999999;
- }
- .expert-img {
- margin-top: 12px;
- width: 100%;
- }
- }
- .system .bubble,
- .real .bubble {
- background: #2F2F2F;
- color: #fff;
- border-radius: 2px 16px 16px 16px;
- }
- .uploader {
- width: 100%;
- display: flex;
- justify-content: center;
- img {
- width: 174px;
- height: 55px;
- }
- }
- .bottom-send {
- padding: 6px 16px 16px;
- }
- .input-box {
- display: flex;
- flex-direction: column;
- padding: 10px;
- background: #2F2F2F;
- border-radius: 16px;
- /* border-top: 1px solid #ddd; */
- ::v-deep {
- .el-textarea__inner {
- background: transparent;
- box-shadow: none;
- color: #fff;
- }
- }
- .bottom-group {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 6px;
- .btn-l {
- display: flex;
- .l-item {
- display: flex;
- align-items: center;
- border-radius: 20px;
- border: 1px solid #555555;
- padding: 6px 8px;
- }
- .l-item + .l-item {
- margin-left: 12px;
- }
- img {
- width: 16px;
- padding-right: 2px;
- }
- }
- .file-icon {
- // width: 16px;
- height: 16px;
- }
- .send-icon {
- margin-left: 8px;
- width: 28px;
- }
- }
- }
- input {
- flex: 1;
- padding: 10px;
- border: 1px solid #ddd;
- border-radius: 4px;
- }
- button {
- margin-left: 10px;
- padding: 10px 15px;
- background: #007bff;
- color: white;
- border: none;
- cursor: pointer;
- }
- </style>
|