Przeglądaj źródła

Merge branch 'master' of http://www.sysuimars.cn:3000/feiniao/feiniao-pc-vue

shuhao 1 miesiąc temu
rodzic
commit
cd120a9958

BIN
src/assets/images/warningHome/chat/file.png


BIN
src/assets/images/warningHome/chat/msg.png


BIN
src/assets/images/warningHome/chat/net.png


BIN
src/assets/images/warningHome/chat/send.png


BIN
src/assets/images/warningHome/chat/think.png


+ 77 - 0
src/views/warningHome/components/chat_components/chat.json

@@ -0,0 +1,77 @@
+[
+    {
+        "name": "气象风险",
+        "header": "嗯,要回答这个问题,用户可能想问荔枝受到气象的影响,首先需要调取一下用户当前位置的过去30天与未来7天预报的气象数据,需要注意的是,山坡的阴面和阳面温差可能比较大,然后通过海拔、坡度和坡向等地形因子进行降尺度分析,得出当前的温度,同样我们可以得出土壤水分和太阳辐射强度的信息,这样我们就可以调取荔枝生长风险模型,输入得到的气象精细化因子,然后计算这里荔枝的风险,还需要注意,调取应该是这个地区的荔枝物候期,是处于花蕾抽出期,然后开始计算这个物候期的风险,最后得出来每个风险的概率,输出给用户当前最关键的风险,并且说明一下这个气象风险是什么含义,对荔枝有什么影响。",
+        "content": "依据空天地一体化气象预警系统计算,您的果园物候期应处于 花蕾抽出期,果园坡度约为 14.14度 ,属于 阳坡 ,当日最高温度为 23° ,最低气温为 13° ,近三日有 2mm 降雨,尤其需要注意 叶芽冲梢,当前属于 叶芽冲梢 二级风险,风险概率为 67%,暖湿天气会促使荔枝抽发新梢或形成花带叶,影响花芽的 正常分化 ,进而导致 分化停滞或受阻 ,导致坐果率下降。"
+    },
+    {
+        "name": "荔枝农事",
+        "header": "用户问最近需要做什么荔枝农事,当前时间为2月13日,立春刚过,属于早春阶段,根据各监测点的农情结果反馈,当前各产区的荔枝普遍处于花穗期(83.2%),部分早熟品种处于开花期。因此,当前荔枝的管理重点在于培育健壮花穗,提高花穗质量,为后续开花坐果打好基础。",
+        "content": "在过去两周内,经历和多次寒潮和升温的交替过程,天气变化情况较大,且该时间段处于花穗抽出关键阶段,因此抽出的花穗中,有较高的花带叶风险。天气的升温幅度较大,最大的温度浮动在5天内超过10度,使得叶芽抽出影响花穗的质量,导致目前大部分处于花穗期的荔枝花带叶情况较为严重,需要尽快进行杀除小叶的农事,提高花穗质量。<br/>杀除小叶需精准执行农事,对于花带叶情况较轻或未出现花带叶的果树,不可轻易喷药,以免影响花穗发育。对于花带叶比例超过10%的树体,可采用乙氧氟草醚3000倍喷施处理,喷施不可过多,只需喷施到叶芽轻微滴水即可。若采用无人机喷施,则可采用乙氧氟草醚300倍喷撒。对于执行了农事的区域,需在7天后及时复核效果,若花带叶情况未明显改善,需进一步执行农事,或人工摘除小叶,密集观察。"
+    },
+    {
+        "name": "推荐专家",
+        "header": "用户想找值得信赖的荔枝专家。我需要看看搜索结果和飞鸟智慧大脑信息库里的专家名字和相关机构。首先,在飞鸟智慧大脑的种植专家联盟里进行寻找,用户寻找的是荔枝种植专家,检索荔枝品类的种植专家,其次,用户提到了有经验的,有经验的可能代表着有多年种植经验的专家,因此在飞鸟智慧大脑中找到有较长时间种植经验的韦帮稳、冼继东等专家推荐给用户。网页3提到了陈厚彬,是国家荔枝龙眼产业技术体系首席专家,华南农业大学的教授。还有胡桂兵,也是华南农业大学的园艺学院院长,国家荔枝良种重大科研联合攻关的首席专家。这两个人应该很重要,多次被不同的网页提到。 所以,整理下来,主要专家有韦帮稳、冼继东、陈厚彬、胡桂兵等专家。",
+        "content": "根据搜索结果,以下几位在荔枝研究领域具有丰富经验和权威性的专家值得信赖,他们在荔枝品种选育、栽培技术、产业管理等方面有突出贡献。"
+    },
+    {
+        "name": "什么问题",
+        "header": "近期指的是离当前时间较近的一段时期,进一步为用户查询飞鸟管家动态中的资讯,发现荔枝果农们关心如下问题:最近,荔枝果农们关注的主要问题有几个。首先,天气变化对荔枝的影响越来越明显,尤其是暖冬、倒春寒和干旱等极端天气,这些因素会影响开花和结果,导致产量波动,比如2024年全国荔枝产量大幅下降了近46%。此外,荔枝的“大小年”现象也让果农头疼,每年丰产后,往往下一年减产,收入不稳定,给他们带来压力。为了解决这个问题,果农们需要加强管理,尤其是在“大年”时进行疏花疏果,避免过度消耗,确保产量更稳定。还有就是由于气候影响,市场供应减少,荔枝价格上涨,果农得更加关注市场动态,合理安排采收和销售。与此同时,荔枝的保鲜难度很大,很多果农都在关注新的保鲜技术,比如超低温“冻眠”技术,这可以大大延长荔枝的保鲜期,减少损耗。最后,花期管理也是一个重要问题,果农们在关心如何及时催肥、如何防治病虫害,以及一些细节上的操作,比如如何分辨花带叶,如何保花等等。这些问题是目前果农们讨论的重点。",
+        "content": "近期,荔枝果农们主要关注以下问题:<br/>1.气候变化对产量的影响 <br/>近年来,异常天气频发,如暖冬、倒春寒、干旱和持续降雨等,严重影响荔枝的开花和结果,导致产量大幅下降。<br/>2.“大小年”现象 <br/>荔枝树存在明显的“大小年”周期,即一年丰产,次年可能减产。这种现象给果农的收益带来不稳定性,影响其种植积极性。<br/>3.市场供需与价格波动<br/> 由于气候导致的减产,市场供应减少,荔枝价格上涨。例如,2024年早熟品种“妃子笑”价格较上年同期上涨了3.64元/公斤。<br/>4.保鲜与储运技术<br/> 荔枝易腐坏,保鲜难度大。为延长销售期,减少损耗,新的保鲜技术如超低温“冻眠”锁鲜技术正在推广,使荔枝保鲜期延长至一年。<br/>5.花期管理技术 <br/>如何在花期及时催化追肥,如何在花期防治病虫害,压低病虫基数,如何分辨花带叶,如何杀小叶保花,红黄叶是否要杀,以上是荔枝果农近期关注的花期管理基数问题。"
+    },
+    {
+        "name": "分析",
+        "header": "嗯,用户让我分析一张荔枝花穗期的照片,我检测到约20%的花带叶,这在荔枝栽培中不算少见,但可能与管理不当有关。过多氮肥或不合适的温度可能导致冲梢,影响开花结果,进而影响产量。因此,这20%的花带叶可能反映了果园的管理问题,需要进一步分析。管理措施上,花芽分化需要低温,如果冬季温度不足,可能导致花芽分化不良。水分和肥料管理也很关键,过多水分和氮肥会促进枝叶生长,而适当的磷钾肥有助于花芽分化。此外,花穗期容易遭受病虫害侵袭,虽然照片中未提到,但建议检查病虫害情况。最后,我建议用户进行更多观察,确认具体情况,因为仅凭一张照片描述可能有局限性。",
+        "content": "1.花穗发育阶段判断 <br/>花穗中20%新叶,存在冲梢风险,温度回升过快或氮肥过量可能影响花穗发育,增加落花落果风险。<br/>2.果园问题分析 <br/>环境因素:温度波动和光照不足可能影响花芽分化和花穗发育。 管理因素:过多氮肥、水分过多、磷钾不足及修剪不及时可能导致冲梢。<br/>3.产量影响 <br/>轻度冲梢(20%)若及时干预影响小,否则可能导致坐果率下降。<br/>4.管理建议 <br/>营养管理:人工摘叶,喷施乙烯利控梢。 水肥管理:控氮增钾,保持适度干旱。 环境调控:疏剪枝条,监测天气避免冲梢。<br/>5.后续注意事项<br/> 病虫害防控:使用氯氰菊酯或生物农药防治害虫。 保花保果:补充钙、镁肥,喷施赤霉素提高坐果率。"
+    },
+    {
+        "name": "小管家",
+        "index":"0",
+        "header": "您好,飞鸟智慧种植大脑是您的私人管家,请告诉我您的作物类别,并授权我读取您的位置,飞鸟感知脑自动读取大数据,推理脑将评估您的作物种植风险,决策脑将自动生成智能处方。",
+        "content": ""
+    },
+    {
+        "name": "品种",
+        "index":"1",
+        "header": "请选择您种植的荔枝品种",
+        "content": ""
+    },
+    {
+        "name": "物候期",
+        "index":"2",
+        "header": "请选择您种植的品种物候期",
+        "content": ""
+    },
+    {
+        "name": "农场",
+        "link":true,
+        "index":"3",
+        "header": "飞鸟感知脑提示,请问您是否需要飞鸟私人管家持续守护您的农场,请添加您的农场",
+        "content": ""
+    },
+    {
+        "name": "感知风险",
+        "header": "1、飞息鸟感知脑正在感知当前地区环境信息<br/>(1)正在读取基础地形信息,用户当下",
+        "content": "您的果园气象风险预警报告如下<br/>(空天地气象预警系统测算)<br>一、物候与地理信息<br/>(1)当前物候期:花蕾"
+    },
+    {
+        "name": "什么农事",
+        "header": "飞鸟决策脑正在生成农事处方<br/>(1)正在调取用户感知和推理数据,当前用户有荔枝叶",
+        "content": "定量农事建议与周期性农事复核方案<br/>1、推荐农事:高温控梢<br/>2、农事目的:在叶芽冲梢花"
+    },
+    {
+        "name": "果园总结",
+        "header": "好的,我现在需要帮助用户总结他的果园情况。<br/>1、飞鸟感知脑正在感知……<br/>(1)首先,调取用户的",
+        "content": "当前果园情况总结如下:<br/>一、整体概况<br/>(1)果园规模:生产树185棵(占比94%),休养树8棵(占比6%),果"
+    },
+    {
+        "name": "联系专家指导",
+        "header": "飞鸟决策脑推理中:<br/>(1)用户想要联系生态荔枝种植专家,可能是遇到了飞鸟标准化基本农事体",
+        "content": "根据您的要求生态荔枝种植,考虑了您所在区域和品种,为您寻找了最优秀的荔枝种植管家,可以邀请他来托管指导您的农场:"
+    },
+    {
+        "name": "种植面积",
+        "header": "以下是2024年广东省各市水稻种植面积",
+        "content": ""
+    }
+]

+ 111 - 0
src/views/warningHome/components/chat_components/deepSeekAsk.js

@@ -0,0 +1,111 @@
+import config from "@/api/config.js";
+
+
+
+/**
+ * DeepSeekAsk class
+ */
+class DeepSeekAsk {
+  constructor(userId) {
+    this.userId = userId;
+    this.topic = `ask/${userId}/${Date.now()}`;
+    this.contents = []
+    this.end = false
+    this.isStart= false
+  }
+
+  ask(userText, callback){
+    let that = this;
+    this.end = false
+    this.contents.push({type:1, content: userText,reasoning:""})
+    const message = {
+      topic: this.topic,
+      contents: this.contents
+    }
+    VE_API.deep_seek.ask(message).then(({code})=> {
+      console.log("用户发送消息",message)
+      callback(code)
+      if(code === 0){
+        that.res(that)
+      }
+    })
+  }
+
+  /**
+   * 用户主动停止
+   */
+  stop(){
+    this.end = true
+    this.isStart = false
+  }
+
+  res(that){
+    console.log("----------------------开始轮询结果------------")
+    VE_API.deep_seek.res({topic: this.topic}).then(({code,data})=> {
+      if(code === 0){
+        that.isStart = true
+        // console.log("接收到消息",data.msg)
+        if(data.index > -1){
+          if(that.contents[that.contents.length-1].type === 2){
+            that.contents[that.contents.length-1].header += data.reasoning
+            that.contents[that.contents.length-1].content += data.msg
+          }else{
+            that.contents.push({type:2, content: data.msg, header: data.reasoning})
+          }
+        }
+        if(!data.end){
+          setTimeout((that2)=> {
+            that2.res(that2)
+          },1000,that)
+        }else{
+          that.isStart = false
+          that.end = true
+        }
+      }
+    }).catch(e => {
+      that.isStart = false
+      that.end = true
+      console.log("轮询结果失败",e)
+    })
+  }
+
+  markdownToHtml(markdown) {
+    if (!markdown) {
+      return '';
+    }
+    // 转换标题(# 到 <h1>, ## 到 <h2> 等)
+    markdown = markdown.replace(/^# (.+)$/mg, '<h1>$1</h1>');
+    markdown = markdown.replace(/^## (.+)$/mg, '<h2>$1</h2>');
+    markdown = markdown.replace(/^### (.+)$/mg, '<h3>$1</h3>');
+    markdown = markdown.replace(/^#### (.+)$/mg, '<h4>$1</h4>');
+    markdown = markdown.replace(/^##### (.+)$/mg, '<h5>$1</h5>');
+    markdown = markdown.replace(/^###### (.+)$/mg, '<h6>$1</h6>');
+
+    // 转换段落(两个换行符分隔)
+    markdown = markdown.replace(/\n\s*\n/g, '</p><p>');
+
+    // 转换粗体(** 或 __)
+    markdown = markdown.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
+    markdown = markdown.replace(/__(.+?)__/g, '<strong>$1</strong>');
+
+    // 转换斜体(* 或 _)
+    markdown = markdown.replace(/\*(.+?)\*/g, '<em>$1</em>');
+    markdown = markdown.replace(/_(.+?)_/g, '<em>$1</em>');
+
+    // 转换无序列表(- 或 *)
+    markdown = markdown.replace(/^- (.+)$/mg, '<li>$1</li>');
+    markdown = markdown.replace(/^\* (.+)$/mg, '<li>$1</li>');
+    markdown = markdown.replace(/<li>(.+?)<\/li>/g, '<ul><li>$1</li></ul>');
+
+    // 将第一个段落元素包裹在 <p> 标签中
+    markdown = `<p>${markdown}</p>`;
+
+    // 移除多余的 <ul> 标签
+    markdown = markdown.replace(/<\/ul>\s*<ul>/g, '');
+
+    return markdown;
+  }
+
+}
+
+export default DeepSeekAsk;

+ 616 - 0
src/views/warningHome/components/chat_components/index.vue

@@ -0,0 +1,616 @@
+<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" v-if="!msg.text.name">
+                            思考中<el-icon><ArrowDown /></el-icon>
+                        </div>
+                        <div class="header" v-html="msg.text.header"></div>
+                        <div class="divider"></div>
+                        <div class="content" v-html="msg.text.content"></div>
+                        <div class="table-wrap" v-if="msg.text.name === '种植面积'">
+                            <el-table :data="tableData" border style="width: 100%">
+                                <el-table-column prop="city" label="市" width="100" />
+                                <el-table-column prop="area" label="2024年水稻种植面积" width="320">
+                                    <template #default="scope">
+                                        {{ scope.row.area  }}亩
+                                    </template>
+                                </el-table-column>
+                            </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 class="table-wrap" v-if="msg.text.name === '种植面积'">
+                            <el-table :data="tableData" border style="width: 100%">
+                                <el-table-column prop="city" label="市" width="100" />
+                                <el-table-column prop="area" label="2024年水稻种植面积" width="320">
+                                    <template #default="scope">
+                                        {{ scope.row.area  }}亩
+                                    </template>
+                                </el-table-column>
+                            </el-table>
+                        </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>
+                                    <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: 10000 },
+    { city: '佛山市', area: 900 },
+    { city: '惠州市', area: 856 },
+    { city: '茂名市', area: 789 },
+])
+
+const steps = [
+    { type: "auto", text: { header: "您好,飞鸟智慧种植大脑是您的私人管家" } },
+    {
+        type: "ask",
+        text: {
+            askHeader: "猜您想问",
+            askContent: ["2024年广东省各市的水稻种植面积", "2024年广东省各市的水稻种植面积变化"],
+        },
+    },
+    {
+        type: "ask",
+        text: {
+            askHeader: "猜您想问",
+            askContent: ["荔枝出现花带叶怎么办?", "荔枝抽梢具体做什么农事呢?"],
+        },
+    },
+];
+
+
+const stepIndex = ref(0);
+const triggerNextStep = () => {
+    if (stepIndex.value < steps.length) {
+        addSystemReply(
+            steps[stepIndex.value].type,
+            steps[stepIndex.value].text,
+            () => {
+                console.log("stepIndex.value === 76574", stepIndex.value);
+                isProcessing.value = false;
+            }
+        );
+        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)
+}
+
+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}, () => {
+                console.log("sendMessage eeeeee",);
+                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 = "";
+    console.log("stepIndex.value", type);
+    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);
+                    console.log("ddddddddone", stepIndex.value);
+                    // if (stepIndex.value === 2 || stepIndex.value === 6) {
+
+                    //     nextTick(() => {
+                    //                     setTimeout(triggerNextStep, 1000);
+                    //                 });
+                    // }
+                    callback && callback(); // 回复完成后解锁输入
+                }
+            }, 50);
+        } 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)
+});
+
+// **滚动到底部**
+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 {
+    ::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;
+    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;
+    }
+    .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 {
+    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;
+        }
+        .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>

+ 41 - 13
src/views/warningHome/index.vue

@@ -4,6 +4,10 @@
         <div class="content">
             <div class="warning-l left">
                 <div class="warning-top">
+                    <div class="back-icon yes-events" v-if="!hideChatMapLayer" @click="toggleChatMapLayer">
+                        <img src="@/assets/images/common/back-icon.png" />
+                        返回
+                    </div>
                     <div class="top-l yes-events">
                         <div>
                             <el-cascader
@@ -15,16 +19,6 @@
                                 @change="toggleArea"
                                 popper-class="area-cascader"
                             />
-                            <!-- <el-select
-                                v-model="areaVal"
-                                placeholder=""
-                                style="width: 184px"
-                                popper-class="area-select"
-                                @change="toggleArea"
-                                >
-                                <el-option label="广东省" :value="3" />
-                                <el-option label="广东省-从化" :value="3186" />
-                            </el-select> -->
                         </div>
                         <div class="type-box"><img src="@/assets/images/warningHome/lz.png" /></div>
                     </div>
@@ -48,7 +42,7 @@
                         </div>
                     </div>
                 </div>
-                <div class="warning-alarm yes-events">
+                <div class="warning-alarm yes-events" v-show="hideChatMapLayer">
                     <alarm-list></alarm-list>
                 </div>
                 <div class="time-wrap yes-events">
@@ -56,7 +50,8 @@
                 </div>
             </div>
             <div class="warning-r right yes-events">
-                <album></album>
+                <!-- <album></album> -->
+                <chat></chat>
 
                 <!-- 地图图例 -->
                 <div class="map-legend" v-if="legendImg">
@@ -103,6 +98,7 @@ import fnHeader from "@/components/fnHeader.vue";
 import WarningMap from "./warningMap";
 import AlarmLayer from "./map/alarmLayer";
 import album from "./components/album.vue";
+import chat from "./components/chat_components/index.vue";
 import alarmList from "./components/alarmList.vue";
 import timeLine from "./components/timeLine.vue";
 import { useRouter } from "vue-router";
@@ -137,8 +133,22 @@ onMounted(() => {
         legendImg.value = warningLayers.value[`${name}图例`];
       }
     });
+
+    // ai与地图交互
+    eventBus.on("chat:showMapLayer", handleMapLayer)
 });
 
+// ai与地图交互
+const hideChatMapLayer = ref(true)
+const handleMapLayer = (name) => {
+    hideChatMapLayer.value = false
+    console.log('name', name);
+}
+
+const toggleChatMapLayer = () => {
+    hideChatMapLayer.value = true
+}
+
 const destroyPopup = () => {
     eventBus.emit("map:destroyPopup");
 };
@@ -195,7 +205,8 @@ const toggleBox = (name) => {
             // display: flex;
         }
         .right {
-            width: 395px;
+            // width: 395px;
+            width: 500px;
             position: relative;
             .list {
                 width: 100%;
@@ -243,6 +254,23 @@ const toggleBox = (name) => {
         }
         .warning-top {
             display: flex;
+            width: max-content;
+            align-items: center;
+            .back-icon {
+                cursor: pointer;
+                margin-right: 20px;
+                height: 50px;
+                display: flex;
+                align-items: center;
+                padding: 0 20px;
+                border: 1px solid rgba(255, 255, 255, 0.6);
+                background: rgba(0, 0, 0, 0.2);
+                border-radius: 4px;
+                img {
+                    width: 17px;
+                    margin-right: 10px;
+                }
+            }
             .top-l {
                 display: flex;
                 flex-direction: column;