chatWindow.vue 35 KB

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