chatWindow.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  1. <template>
  2. <div class="chat-container">
  3. <!-- 聊天消息区域 -->
  4. <div class="chat-messages" ref="messagesContainer">
  5. <div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.sender">
  6. <!-- 对方消息 -->
  7. <template v-if="msg.sender === 'received'">
  8. <!-- <div class="avatar">{{ msg.receiverName.charAt(0) }}</div> -->
  9. <el-avatar
  10. class="avatar"
  11. :size="40"
  12. :src="
  13. msg.receiverIcon ||
  14. 'https://birdseye-img.sysuimars.com/birdseye-look-mini/Group%201321316260.png'
  15. "
  16. />
  17. <img src="" alt="" />
  18. <div class="bubble" :class="{ 'no-bubble': msg.messageType === 'image' }">
  19. <!-- 文本消息 -->
  20. <div v-if="msg.messageType === 'text'" class="content">{{ msg.content }}</div>
  21. <!-- 图片消息 -->
  22. <div v-if="msg.messageType === 'image'" class="image-message">
  23. <img
  24. :src="msg.content + resize"
  25. @click="showImagePreview(msg.content)"
  26. @load="handleImageLoad"
  27. alt="图片"
  28. />
  29. </div>
  30. <!-- 语音消息 -->
  31. <div v-if="msg.messageType === 'audio'" class="audio-message" @click="playAudio(msg.content)">
  32. <span class="audio-icon">🎵</span>
  33. <span class="duration">{{ msg.duration }}"</span>
  34. </div>
  35. <!-- 对话样式消息 -->
  36. <div v-if="msg.messageType === 'dialog'" class="dialog-message">
  37. <template v-if="msg.content.type === 'farm_report'">
  38. <div class="report-title">{{ msg.content.title }}</div>
  39. <div class="dialog-title">{{ msg.content.content }}</div>
  40. <img src="@/assets/img/monitor/aaa.png" alt="" class="monitor-image" />
  41. </template>
  42. <template v-else>
  43. <div class="dialog-title">{{ msg.content.title }}</div>
  44. <img src="@/assets/img/monitor/image.png" alt="" class="monitor-image" />
  45. </template>
  46. </div>
  47. <!-- 对话样式消息 -->
  48. <div v-if="msg.messageType === 'card'" class="card-message" @click="handleCardClick(msg)">
  49. <div class="card-title">{{ msg.title || msg.content.title }}</div>
  50. <img :src="base_img_url2 + (msg.coverUrl || msg.content.coverUrl) + resize" alt="" />
  51. </div>
  52. <!-- <div class="time">{{ msg.time }}</div> -->
  53. </div>
  54. </template>
  55. <!-- 我方消息 -->
  56. <template v-else>
  57. <div class="bubble" :class="{ 'no-bubble': msg.messageType === 'image'}">
  58. <!-- 文本消息 -->
  59. <div v-if="msg.messageType === 'text'" class="content">{{ msg.content }}</div>
  60. <!-- 图片消息 -->
  61. <div v-if="msg.messageType === 'image'" class="image-message">
  62. <img
  63. :src="msg.content + resize"
  64. @click="showImagePreview(msg.content)"
  65. @load="handleImageLoad"
  66. alt="图片"
  67. />
  68. </div>
  69. <!-- 语音消息 -->
  70. <div v-if="msg.messageType === 'audio'" class="audio-message" @click="playAudio(msg.content)">
  71. <span class="audio-icon">🎵</span>
  72. <span class="duration">{{ msg.duration }}"</span>
  73. </div>
  74. <!-- 对话样式消息 -->
  75. <div v-if="msg.messageType === 'dialog'" class="dialog-message">
  76. <template v-if="msg.content.type === 'farm_report'">
  77. <div class="report-title">{{ msg.content.title }}</div>
  78. <div class="dialog-title">{{ msg.content.content }}</div>
  79. <img src="@/assets/img/monitor/aaa.png" alt="" class="monitor-image" />
  80. </template>
  81. <template v-else>
  82. <div class="dialog-title">{{ msg.content.title }}</div>
  83. <img src="@/assets/img/monitor/image.png" alt="" class="monitor-image" />
  84. </template>
  85. </div>
  86. <!-- 对话样式消息 -->
  87. <div v-if="msg.messageType === 'card'" class="card-message" @click="handleCardClick(msg)">
  88. <div class="card-title">{{ msg.title || msg.content.title }}</div>
  89. <img :src="base_img_url2 + (msg.coverUrl || msg.content.coverUrl) + resize" alt="" />
  90. </div>
  91. <!-- <div class="time">{{ msg.time }}</div> -->
  92. </div>
  93. <!-- <div class="avatar avatar-r">{{ msg.senderName.charAt(0) }}</div> -->
  94. <el-avatar
  95. class="avatar avatar-r"
  96. :size="40"
  97. :src="
  98. msg.senderIcon ||
  99. 'https://birdseye-img.sysuimars.com/birdseye-look-mini/Group%201321316260.png'
  100. "
  101. />
  102. </template>
  103. </div>
  104. </div>
  105. <!-- 功能按钮区域 -->
  106. <div class="function-buttons">
  107. <el-select v-model="farmVal" size="large" @change="handleFarmChange()">
  108. <el-option
  109. v-for="item in options"
  110. :key="item.id"
  111. :label="item.name"
  112. :value="item.id"
  113. />
  114. </el-select>
  115. <div v-for="(btn, index) in functionButtons" :key="index" class="function-btn" @click="btn.handler">
  116. <span class="btn-text">{{ btn.text }}</span>
  117. </div>
  118. </div>
  119. <!-- 输入框区域 -->
  120. <div class="input-area">
  121. <div class="toolbar">
  122. <!-- <button @click="toggleEmojiPicker">😊</button> -->
  123. <!-- <button @click="startImageUpload">📷</button> -->
  124. <!-- <button
  125. @mousedown="startRecording"
  126. @mouseup="stopRecording"
  127. @touchstart="startRecording"
  128. @touchend="stopRecording"
  129. >
  130. 🎤
  131. </button> -->
  132. <el-icon class="link" @click="startImageUpload"><Link /></el-icon>
  133. <input type="file" ref="fileInput" accept="image/*" style="display: none" @change="handleImageUpload" />
  134. </div>
  135. <input type="text" v-model="inputMessage" placeholder="请输入你想说的话~" @keyup.enter="sendTextMessage" />
  136. <div class="send" @click="sendTextMessage">发送</div>
  137. <!-- 录音指示器 -->
  138. <div v-if="isRecording" class="recording-indicator">
  139. <div class="pulse"></div>
  140. 录音中... {{ recordingDuration }}s
  141. </div>
  142. </div>
  143. <!-- Emoji 选择器 -->
  144. <div v-if="showEmojiPicker" class="emoji-picker">
  145. <span v-for="emoji in emojis" :key="emoji" @click="addEmoji(emoji)">{{ emoji }}</span>
  146. </div>
  147. <!-- 图片预览模态框 -->
  148. <div v-if="previewImage" class="image-preview" @click="previewImage = null">
  149. <img :src="previewImage" alt="预览" />
  150. </div>
  151. </div>
  152. </template>
  153. <script setup>
  154. import { ref, onUnmounted, nextTick, watch, onActivated, onDeactivated } from "vue";
  155. import { useRouter ,useRoute} from "vue-router";
  156. import { base_img_url2 } from "@/api/config";
  157. import { getFileExt } from "@/utils/util";
  158. import UploadFile from "@/utils/upliadFile";
  159. import MqttClient from "@/plugins/MqttClient";
  160. const resize = "?x-oss-process=image/resize,p_120/format,webp/quality,q_100";
  161. const router = useRouter();
  162. const props = defineProps({
  163. text: {
  164. type: String,
  165. defalut: "",
  166. },
  167. img: {
  168. type: String,
  169. defalut: "",
  170. },
  171. userId: {
  172. type: [String, Number],
  173. defalut: "",
  174. },
  175. });
  176. const defalutMsg = ref([
  177. {
  178. sender: "sent",
  179. senderIcon: "王",
  180. messageType: "text",
  181. content:
  182. "你好,我叫陈晓晓。有100亩荔枝,30亩桂味,70亩妃子笑,位置在广州市番禺区大学城110号,希望您可以来指导。",
  183. time: "上午10:32",
  184. },
  185. ]);
  186. const curUserId = Number(localStorage.getItem("MINI_USER_ID"));
  187. const senderIcon = ref("");
  188. const receiverIcon = ref("");
  189. const receiverIdVal = ref(null);
  190. // 本地用户头像
  191. const localUserInfoIcon = (() => {
  192. try {
  193. const info = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
  194. return info?.icon || "";
  195. } catch (e) {
  196. return "";
  197. }
  198. })();
  199. // 初始化本地头像为默认发送者头像
  200. senderIcon.value = localUserInfoIcon;
  201. //聊天会话
  202. const createSession = (targetUserId, callback) => {
  203. // 先保存当前的对话样式消息 要注释
  204. // const dialogMessages = messages.value.filter((msg) => msg.messageType === "dialog");
  205. VE_API.bbs.createSession({ farmId: farmVal.value, targetUserId }).then(({ data, code }) => {
  206. if (code === 0) {
  207. senderIcon.value = localUserInfoIcon;
  208. receiverIcon.value = data.session.targetUserAvatar;
  209. receiverIdVal.value = data.session.targetUserId;
  210. messages.value = data.messages.map((item) => {
  211. let content = item.content;
  212. if (item.messageType === "image") {
  213. // 优先读取后端的 image 字段,其次兼容旧的 content(JSON)
  214. if (item.image && (item.image.url || item.image.originUrl)) {
  215. content = item.image.url || item.image.originUrl;
  216. } else if (item.content) {
  217. try {
  218. const imgObj = JSON.parse(item.content);
  219. content = imgObj.url || imgObj.originUrl;
  220. } catch (e) {
  221. console.error(e, "e");
  222. }
  223. }
  224. }else if(item.messageType === 'card'){
  225. content = JSON.parse(item.content);
  226. }
  227. return {
  228. ...item,
  229. content,
  230. sender: item.senderId === curUserId ? "sent" : "received",
  231. senderIcon: item.senderId === curUserId ? localUserInfoIcon : data.session.targetUserAvatar,
  232. receiverIcon: data.session.targetUserAvatar,
  233. };
  234. });
  235. // // 重新添加对话样式消息 要注释
  236. // if (dialogMessages.length > 0) {
  237. // messages.value = [...messages.value, ...dialogMessages];
  238. // }
  239. setTimeout(() => {
  240. scrollToBottom();
  241. }, 300);
  242. callback && callback();
  243. }
  244. });
  245. };
  246. const handleFarmChange = (e) => {
  247. createSession(userId.value);
  248. initMqtt();
  249. };
  250. //发送消息接口
  251. //类型 text ,file,image
  252. const sendMsg = (messageType = "text", content = "", obj = {}) => {
  253. const params = {
  254. farmId: farmVal.value,
  255. senderId: curUserId,
  256. receiverId: userId.value,
  257. content,
  258. [messageType]:obj,
  259. messageType,
  260. };
  261. VE_API.bbs.sendMsg(params);
  262. };
  263. const userId = ref(null);
  264. const handleCardClick = (msg) => {
  265. router.push(msg.linkUrl || msg.content.linkUrl);
  266. }
  267. watch(
  268. () => props.userId,
  269. async (newValue) => {
  270. if (newValue) {
  271. await getFarmList();
  272. userId.value = newValue;
  273. createSession(newValue, () => {
  274. if(route.query.pageParams) {
  275. const params = JSON.parse(route.query.pageParams);
  276. const imgArr = JSON.parse(params.executeEvidence);
  277. const message = {
  278. sender: "sent",
  279. messageType: "card",
  280. senderIcon: senderIcon.value,
  281. title: params.farmWorkName + ' 农事已完成,请您确认',
  282. coverUrl: imgArr[0],
  283. cardType:'farm_work',
  284. linkUrl:`/completed_work?json=${JSON.stringify({id:params.id})}`,
  285. time: getCurrentTime(),
  286. };
  287. sendMessage(message)
  288. }
  289. if (props.text) {
  290. sendMsg("text", props.text);
  291. messages.value.push({
  292. sender: "sent",
  293. senderIcon: senderIcon.value,
  294. messageType: "text",
  295. content: props.text,
  296. });
  297. if (props.img) {
  298. const imgArr = JSON.parse(props.img);
  299. if (imgArr.length) {
  300. imgArr.forEach((item) => {
  301. sendMsg("image", "", { url: item, thumbnailUrl: item + resize });
  302. messages.value.push({
  303. sender: "sent",
  304. senderIcon: senderIcon.value,
  305. messageType: "image",
  306. content: item,
  307. });
  308. });
  309. }
  310. }
  311. }
  312. });
  313. initMqtt();
  314. }
  315. }
  316. );
  317. onDeactivated(() => {
  318. // mqttClient.value.client.end(true);
  319. });
  320. // mqtt 连接
  321. const mqttClient = ref(null);
  322. const messagesContainer = ref(null);
  323. // 消息数据
  324. const messages = ref([]);
  325. // 输入相关
  326. const inputMessage = ref("");
  327. const fileInput = ref(null);
  328. const showEmojiPicker = ref(false);
  329. const emojis = ["😀", "😂", "😍", "👍", "👋", "🎉", "❤️", "🙏"];
  330. // 录音相关
  331. const isRecording = ref(false);
  332. const recordingDuration = ref(0);
  333. const audioChunks = ref([]);
  334. const mediaRecorder = ref(null);
  335. const audioContext = ref(null);
  336. function handleImageLoad() {
  337. scrollToBottom();
  338. }
  339. // 图片预览
  340. const previewImage = ref(null);
  341. // 初始化 mqtt
  342. const initMqtt = () => {
  343. const topics = [`user/chat/message/${farmVal.value}/${curUserId}`]; // 订阅的主题数组
  344. mqttClient.value = new MqttClient(topics, (topic, message) => {
  345. if (message && message.length > 10) {
  346. const obj = JSON.parse(message);
  347. console.log("message有值", obj);
  348. if(obj.senderId === curUserId){
  349. return;
  350. }
  351. if (obj.senderId === receiverIdVal.value) {
  352. if (obj.messageType === "image") {
  353. if (obj.image && (obj.image.url || obj.image.originUrl)) {
  354. obj.content = obj.image.url || obj.image.originUrl;
  355. } else if (obj.content) {
  356. try {
  357. const img = JSON.parse(obj.content);
  358. obj.content = img.url || img.originUrl;
  359. } catch (e) {
  360. console.error(e, "e");
  361. }
  362. }
  363. }
  364. obj.receiverId = curUserId;
  365. (obj.sender = obj.senderId === curUserId ? "sent" : "received"), (obj.senderIcon = senderIcon.value);
  366. obj.receiverIcon = receiverIcon.value;
  367. messages.value.push(obj);
  368. scrollToBottom();
  369. }
  370. }
  371. });
  372. mqttClient.value.connect();
  373. };
  374. // 发送消息
  375. const sendMessage = (message) => {
  376. if (message.messageType === "text") {
  377. sendMsg("text", message.content);
  378. } else if (message.messageType === "image") {
  379. // 按新协议:不传 content,传 image 对象
  380. sendMsg("image", "", { url: message.content, thumbnailUrl: message.content + resize });
  381. } else if (message.messageType === "dialog") {
  382. // 对话样式消息不发送到服务器,只显示在本地
  383. console.log("发送对话样式消息:", message.content);
  384. }else{
  385. sendMsg('card','',{
  386. title: message.title,
  387. coverUrl: message.coverUrl,
  388. cardType: message.cardType,
  389. linkUrl: message.linkUrl
  390. });
  391. }
  392. messages.value.push(message);
  393. scrollToBottom();
  394. };
  395. // 发送文本消息
  396. const sendTextMessage = () => {
  397. if (inputMessage.value.trim()) {
  398. const message = {
  399. sender: "sent",
  400. messageType: "text",
  401. senderIcon: senderIcon.value,
  402. content: inputMessage.value,
  403. time: getCurrentTime(),
  404. };
  405. sendMessage(message);
  406. inputMessage.value = "";
  407. }
  408. };
  409. // 发送图片消息
  410. const sendImageMessage = (imageUrl) => {
  411. const message = {
  412. sender: "sent",
  413. messageType: "image",
  414. senderIcon: senderIcon.value,
  415. content: imageUrl,
  416. time: getCurrentTime(),
  417. };
  418. sendMessage(message);
  419. };
  420. // 发送语音消息
  421. const sendAudioMessage = (audioUrl, duration) => {
  422. const message = {
  423. sender: "sent",
  424. messageType: "audio",
  425. senderIcon: senderIcon.value,
  426. content: audioUrl,
  427. duration: duration,
  428. time: getCurrentTime(),
  429. };
  430. sendMessage(message);
  431. };
  432. // 图片处理
  433. const startImageUpload = () => {
  434. fileInput.value.click();
  435. };
  436. const uploadFileObj = new UploadFile();
  437. const handleImageUpload = (event) => {
  438. const file = event.target.files[0];
  439. if (file) {
  440. // 实际项目中应该上传到服务器,这里使用本地URL模拟
  441. const miniUserId = localStorage.getItem("MINI_USER_ID");
  442. let ext = getFileExt(file.name);
  443. let key = `birdseye-look-mini/${miniUserId}/${new Date().getTime()}.${ext}`;
  444. let imageUrl = "";
  445. uploadFileObj.put(key, file).then((resFilename) => {
  446. imageUrl = base_img_url2 + resFilename;
  447. sendImageMessage(imageUrl);
  448. });
  449. // const imageUrl = URL.createObjectURL(file);
  450. }
  451. };
  452. const showImagePreview = (imageUrl) => {
  453. previewImage.value = imageUrl;
  454. };
  455. // 语音处理
  456. const startRecording = async () => {
  457. try {
  458. audioChunks.value = [];
  459. recordingDuration.value = 0;
  460. const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  461. mediaRecorder.value = new MediaRecorder(stream);
  462. audioContext.value = new (window.AudioContext || window.webkitAudioContext)();
  463. mediaRecorder.value.ondataavailable = (event) => {
  464. if (event.data.size > 0) {
  465. audioChunks.value.push(event.data);
  466. }
  467. };
  468. mediaRecorder.value.onstop = async () => {
  469. const audioBlob = new Blob(audioChunks.value, { type: "audio/wav" });
  470. const audioUrl = URL.createObjectURL(audioBlob);
  471. // 计算录音时长
  472. const duration = Math.round(recordingDuration.value);
  473. // 实际项目中应该上传到服务器,这里使用本地URL模拟
  474. sendAudioMessage(audioUrl, duration);
  475. // 释放资源
  476. stream.getTracks().forEach((track) => track.stop());
  477. };
  478. mediaRecorder.value.start();
  479. isRecording.value = true;
  480. // 更新录音计时器
  481. const timer = setInterval(() => {
  482. recordingDuration.value += 0.1;
  483. if (!isRecording.value) {
  484. clearInterval(timer);
  485. }
  486. }, 100);
  487. } catch (error) {
  488. console.error("录音失败:", error);
  489. alert("无法访问麦克风,请检查权限设置");
  490. }
  491. };
  492. const stopRecording = () => {
  493. if (mediaRecorder.value && isRecording.value) {
  494. mediaRecorder.value.stop();
  495. isRecording.value = false;
  496. }
  497. };
  498. const playAudio = (audioUrl) => {
  499. const audio = new Audio(audioUrl);
  500. audio.play();
  501. };
  502. // Emoji 处理
  503. const toggleEmojiPicker = () => {
  504. showEmojiPicker.value = !showEmojiPicker.value;
  505. };
  506. const addEmoji = (emoji) => {
  507. inputMessage.value += emoji;
  508. showEmojiPicker.value = false;
  509. };
  510. // 功能按钮配置
  511. const functionButtons = ref([
  512. {
  513. text: "农场报告",
  514. handler: () => {
  515. console.log("点击农场报告,农场ID:", farmVal.value);
  516. // 发送农场报告对话框消息
  517. sendFarmReportDialog();
  518. },
  519. },
  520. {
  521. text: "农事卡片",
  522. handler: () => {
  523. // 跳转到农事卡片页面
  524. router.push(`/farm_card?farmId=${farmVal.value}`);
  525. },
  526. },
  527. // {
  528. // text: '农场相册',
  529. // handler: () => {
  530. // // 跳转到农场相册页面
  531. // router.push(`/farm_photo`);
  532. // }
  533. // }
  534. ]);
  535. // 辅助函数
  536. const getCurrentTime = () => {
  537. return new Date().toLocaleTimeString("zh-CN", {
  538. hour: "2-digit",
  539. minute: "2-digit",
  540. });
  541. };
  542. const scrollToBottom = () => {
  543. nextTick(() => {
  544. setTimeout(() => {
  545. if (messagesContainer.value) {
  546. messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
  547. }
  548. }, 300);
  549. });
  550. };
  551. const farmVal = ref("");
  552. const options = ref([]);
  553. const route = useRoute();
  554. // 获取农场列表
  555. function getFarmList() {
  556. return VE_API.farm.userFarmSelectOption().then(({ data }) => {
  557. options.value = data || [];
  558. if (data && data.length > 0) {
  559. const defaultOption = data.find((item) => item.defaultOption === true);
  560. if(route.query.farmId) {
  561. farmVal.value = Number(route.query.farmId);
  562. }else{
  563. farmVal.value = defaultOption ? defaultOption.id : data[0].id;
  564. }
  565. }
  566. });
  567. }
  568. onActivated(() => {
  569. if (props.userId) {
  570. scrollToBottom();
  571. }
  572. // 检查是否有选中的农事工作数据
  573. checkSelectedFarmWork();
  574. });
  575. // 检查选中的农事工作数据
  576. const checkSelectedFarmWork = () => {
  577. const selectedFarmWork = localStorage.getItem("selectedFarmWork");
  578. if (selectedFarmWork) {
  579. const data = JSON.parse(selectedFarmWork);
  580. // 发送对话样式的消息
  581. sendDialogMessage(data.dialogMessage);
  582. // 清除localStorage中的数据
  583. localStorage.removeItem("selectedFarmWork");
  584. }
  585. };
  586. // 发送对话样式的消息
  587. const sendDialogMessage = (dialogData) => {
  588. const message = {
  589. sender: "sent",
  590. messageType: "dialog",
  591. senderIcon: senderIcon.value,
  592. content: dialogData,
  593. time: getCurrentTime(),
  594. };
  595. sendMessage(message);
  596. };
  597. // 发送农场报告对话框
  598. const sendFarmReportDialog = () => {
  599. const currentFarmName = options.value.find((opt) => opt.id === farmVal.value)?.name || "当前农场";
  600. const farmReportData = {
  601. type: "farm_report",
  602. title: currentFarmName,
  603. content: "这是我的果园情况,请查看~",
  604. reportDetails: {
  605. farmId: farmVal.value,
  606. farmName: currentFarmName,
  607. reportDate: new Date().toLocaleDateString("zh-CN"),
  608. reportType: "综合报告",
  609. status: "正常",
  610. },
  611. };
  612. const message = {
  613. sender: "sent",
  614. messageType: "dialog",
  615. senderIcon: senderIcon.value,
  616. title: currentFarmName,
  617. content: farmReportData,
  618. time: getCurrentTime(),
  619. };
  620. sendMessage(message);
  621. };
  622. </script>
  623. <style scoped lang="scss">
  624. /* 基础样式(保持之前的不变) */
  625. .chat-container {
  626. display: flex;
  627. flex-direction: column;
  628. height: 100%;
  629. width: 100%;
  630. margin: 0 auto;
  631. border: 1px solid #e6e6e6;
  632. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  633. position: relative;
  634. box-sizing: border-box;
  635. .chat-messages {
  636. flex: 1;
  637. padding: 12px;
  638. overflow-y: auto;
  639. background-color: #f5f5f5;
  640. box-sizing: border-box;
  641. .message {
  642. display: flex;
  643. margin-bottom: 15px;
  644. }
  645. .received {
  646. justify-content: flex-start;
  647. .bubble {
  648. background-color: white;
  649. border-radius: 0 10px 10px 10px;
  650. padding: 10px 12px;
  651. box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
  652. }
  653. }
  654. .sent {
  655. justify-content: flex-end;
  656. .bubble {
  657. background-color: #07c160;
  658. border-radius: 10px 0 10px 10px;
  659. padding: 10px 15px;
  660. color: #fff;
  661. box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
  662. }
  663. }
  664. .avatar {
  665. width: 40px;
  666. height: 40px;
  667. border-radius: 50%;
  668. background-color: #07c160;
  669. color: white;
  670. display: flex;
  671. align-items: center;
  672. justify-content: center;
  673. margin-right: 10px;
  674. font-weight: bold;
  675. }
  676. .avatar-r {
  677. margin: 0 0 0 10px;
  678. }
  679. .bubble {
  680. max-width: 70%;
  681. }
  682. }
  683. }
  684. .content {
  685. font-size: 16px;
  686. line-height: 1.4;
  687. }
  688. .time {
  689. font-size: 12px;
  690. color: #fff;
  691. margin-top: 5px;
  692. text-align: right;
  693. }
  694. .input-area {
  695. display: flex;
  696. align-items: center;
  697. padding: 15px 10px;
  698. border-top: 1px solid #e6e6e6;
  699. background-color: white;
  700. position: relative;
  701. width: 100%;
  702. box-sizing: border-box;
  703. input {
  704. flex: 1;
  705. padding: 10px;
  706. border: 1px solid #e6e6e6;
  707. border-radius: 20px;
  708. outline: none;
  709. }
  710. .send {
  711. margin-left: 10px;
  712. padding: 8px 20px;
  713. background-color: #07c160;
  714. color: white;
  715. border: none;
  716. border-radius: 20px;
  717. cursor: pointer;
  718. }
  719. }
  720. // .input-area button:hover {
  721. // background-color: #06ad56;
  722. // }
  723. /* 新增的多媒体消息样式 */
  724. .image-message {
  725. img {
  726. max-width: 200px;
  727. max-height: 200px;
  728. border-radius: 8px;
  729. cursor: pointer;
  730. }
  731. }
  732. /* 图片消息不使用对话气泡样式 */
  733. .no-bubble {
  734. background: transparent !important;
  735. border-radius: 0 !important;
  736. padding: 0 !important;
  737. box-shadow: none !important;
  738. color: inherit !important;
  739. }
  740. .audio-message {
  741. display: flex;
  742. align-items: center;
  743. padding: 10px 15px;
  744. background-color: #f0f0f0;
  745. border-radius: 20px;
  746. cursor: pointer;
  747. }
  748. .audio-message .audio-icon {
  749. margin-right: 8px;
  750. font-size: 20px;
  751. }
  752. .audio-message .duration {
  753. font-size: 14px;
  754. color: #666;
  755. }
  756. .message.sent .audio-message {
  757. background-color: #d8f1cb;
  758. }
  759. /* 工具栏样式 */
  760. .toolbar {
  761. display: flex;
  762. align-items: center;
  763. button {
  764. background: none;
  765. border: none;
  766. font-size: 20px;
  767. margin-right: 10px;
  768. cursor: pointer;
  769. padding: 5px;
  770. }
  771. .link {
  772. font-size: 24px;
  773. margin-right: 10px;
  774. }
  775. }
  776. /* 录音指示器 */
  777. .recording-indicator {
  778. position: absolute;
  779. top: -40px;
  780. left: 0;
  781. right: 0;
  782. background-color: #ff4d4f;
  783. color: white;
  784. padding: 8px;
  785. text-align: center;
  786. border-radius: 4px;
  787. font-size: 14px;
  788. }
  789. .recording-indicator .pulse {
  790. display: inline-block;
  791. width: 10px;
  792. height: 10px;
  793. border-radius: 50%;
  794. background: white;
  795. margin-right: 8px;
  796. animation: pulse 1.5s infinite;
  797. }
  798. @keyframes pulse {
  799. 0% {
  800. transform: scale(0.95);
  801. opacity: 1;
  802. }
  803. 50% {
  804. transform: scale(1.1);
  805. opacity: 0.7;
  806. }
  807. 100% {
  808. transform: scale(0.95);
  809. opacity: 1;
  810. }
  811. }
  812. /* Emoji 选择器 */
  813. .emoji-picker {
  814. position: absolute;
  815. bottom: 60px;
  816. right: 10px;
  817. background: white;
  818. border: 1px solid #e6e6e6;
  819. border-radius: 8px;
  820. padding: 10px;
  821. display: grid;
  822. grid-template-columns: repeat(4, 1fr);
  823. gap: 8px;
  824. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  825. z-index: 100;
  826. }
  827. .emoji-picker span {
  828. font-size: 24px;
  829. text-align: center;
  830. }
  831. .emoji-picker span:hover {
  832. transform: scale(1.2);
  833. }
  834. /* 功能按钮样式 */
  835. .function-buttons {
  836. display: flex;
  837. gap: 5px;
  838. padding: 5px 10px;
  839. box-sizing: border-box;
  840. .function-btn {
  841. background-color: white;
  842. border-radius: 8px;
  843. padding: 10px 10px;
  844. min-width: 60px;
  845. text-align: center;
  846. .btn-text {
  847. font-size: 14px;
  848. }
  849. }
  850. }
  851. /* 图片预览 */
  852. .image-preview {
  853. position: fixed;
  854. top: 0;
  855. left: 0;
  856. right: 0;
  857. bottom: 0;
  858. background: rgba(0, 0, 0, 0.8);
  859. display: flex;
  860. align-items: center;
  861. justify-content: center;
  862. z-index: 1000;
  863. }
  864. .image-preview img {
  865. max-width: 90%;
  866. max-height: 90%;
  867. object-fit: contain;
  868. }
  869. /* 对话样式消息 */
  870. .dialog-message {
  871. max-width: 100%;
  872. background: #fff !important;
  873. padding: 10px;
  874. border-radius: 10px;
  875. .report-title {
  876. font-size: 16px;
  877. font-weight: 600;
  878. color: #000;
  879. margin-bottom: 5px;
  880. }
  881. .dialog-title {
  882. font-size: 12px;
  883. color: rgba(0, 0, 0, 0.6);
  884. margin-bottom: 10px;
  885. }
  886. .monitor-image {
  887. width: 222px;
  888. height: 180px;
  889. object-fit: cover;
  890. }
  891. .farm-report-content,
  892. .farm-work-content {
  893. .report-details,
  894. .work-details {
  895. background: #f8f9fa;
  896. border-radius: 8px;
  897. padding: 12px;
  898. margin-top: 10px;
  899. .detail-item {
  900. display: flex;
  901. margin-bottom: 6px;
  902. font-size: 13px;
  903. &:last-child {
  904. margin-bottom: 0;
  905. }
  906. .detail-label {
  907. color: #666;
  908. min-width: 80px;
  909. }
  910. .detail-value {
  911. color: #333;
  912. flex: 1;
  913. }
  914. }
  915. }
  916. }
  917. }
  918. .card-message{
  919. .card-title{
  920. font-size: 15px;
  921. font-weight: 600;
  922. color: #fff;
  923. margin-bottom: 5px;
  924. }
  925. img{
  926. width: 222px;
  927. height: 180px;
  928. object-fit: cover;
  929. }
  930. }
  931. /* 我方消息中的对话样式 */
  932. .message.sent .dialog-message {
  933. background: #e3f2fd;
  934. .work-details {
  935. background: #f0f8ff;
  936. }
  937. }
  938. </style>