consult.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. <template>
  2. <div class="consult">
  3. <custom-header name="咨询专家"></custom-header>
  4. <div class="consult-content">
  5. <!-- 聊天消息区域 -->
  6. <div class="chat-messages" ref="messagesContainer">
  7. <div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.sender">
  8. <!-- 对方消息 -->
  9. <template v-if="msg.sender === 'received'">
  10. <!-- <div class="avatar">{{ msg.receiverName.charAt(0) }}</div> -->
  11. <el-avatar
  12. class="avatar"
  13. :size="40"
  14. :src="
  15. msg.receiverIcon ||
  16. 'https://birdseye-img.sysuimars.com/dinggou-mini/defalut-icon.png'
  17. "
  18. />
  19. <div class="bubble" :class="{ 'no-bubble': msg.messageType === 'image' ,'card-bubble': msg.messageType === 'card'}">
  20. <!-- 文本消息 -->
  21. <div v-if="msg.messageType === 'text'" class="content">{{ msg.content }}</div>
  22. <!-- 图片消息 -->
  23. <div v-if="msg.messageType === 'image'" class="image-message">
  24. <img
  25. :src="msg.content + resize"
  26. @click="showImagePreview(msg.content)"
  27. @load="handleImageLoad"
  28. alt="图片"
  29. />
  30. </div>
  31. <!-- 对话样式消息 -->
  32. <div v-if="msg.messageType === 'report'" class="dialog-message" @click="handleReportClick(msg)">
  33. <template v-if="(msg.reportType || msg.content.reportType) === 'farm_report'">
  34. <div class="report-title">{{ msg.title ||msg.content.title }}</div>
  35. <div class="dialog-title">这是{{curRole == 2 ? '该农场' : '我'}}的果园情况,请查看~</div>
  36. <img src="https://birdseye-img.sysuimars.com/birdseye-look-mini/share-report-bg.png" alt="" class="monitor-image" />
  37. </template>
  38. <template v-else>
  39. <div class="dialog-title">{{ msg.title || msg.content.title}}</div>
  40. <img src="@/assets/img/monitor/image.png" alt="" class="monitor-image" />
  41. </template>
  42. </div>
  43. <!-- 对话样式消息 -->
  44. <div v-if="msg.messageType === 'question'" class="question-message" @click="handleCardClick(msg)">
  45. <div class="question-title">{{ msg.content }}</div>
  46. <div class="image-wrap">
  47. <img src="@/assets/img/monitor/image.png" alt="" />
  48. <img src="@/assets/img/monitor/image.png" alt="" />
  49. <img src="@/assets/img/monitor/image.png" alt="" />
  50. </div>
  51. <div class="btn-detail" @click="handleDetailClick">查看详情</div>
  52. </div>
  53. </div>
  54. </template>
  55. <!-- 我方消息 -->
  56. <template v-else>
  57. <div class="bubble" :class="{ 'no-bubble': msg.messageType === 'image','card-bubble': msg.messageType === 'card' || msg.messageType === 'report' }">
  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 === 'report'" class="dialog-message" @click="handleReportClick(msg)">
  71. <template v-if="(msg.reportType || msg.content.reportType) === 'farm_report'">
  72. <div class="report-title">{{ msg.title ||msg.content.title }}</div>
  73. <div class="dialog-title">这是{{curRole == 2 ? '该农场' : '我'}}果园情况,请查看~</div>
  74. <img src="https://birdseye-img.sysuimars.com/birdseye-look-mini/share-report-bg.png" alt="" class="monitor-image" />
  75. </template>
  76. <template v-else>
  77. <div class="dialog-title">{{ msg.title || msg.content.title}}</div>
  78. <img src="@/assets/img/monitor/image.png" alt="" class="monitor-image" />
  79. </template>
  80. </div>
  81. <!-- 对话样式消息 -->
  82. <div v-if="msg.messageType === 'card'" class="card-message" @click="handleCardClick(msg)">
  83. <template v-if="(msg.cardType || msg.content.cardType) === 'quotation'">
  84. <div class="card-title">向您发送了一张 服务报价单</div>
  85. <img src="https://birdseye-img.sysuimars.com/temp/price.png" alt="" />
  86. </template>
  87. <template v-else>
  88. <div class="card-title">{{ msg.title || msg.content.title }}</div>
  89. <img :src="handleImgUrl(msg.coverUrl || msg.content.coverUrl)" alt="" />
  90. </template>
  91. </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/dinggou-mini/defalut-icon.png'
  100. "
  101. />
  102. </template>
  103. </div>
  104. </div>
  105. <!-- 输入框区域 -->
  106. <div class="input-area">
  107. <!-- <div class="toolbar">
  108. <el-icon class="link" @click="startImageUpload"><Link /></el-icon>
  109. <input type="file" ref="fileInput" accept="image/*" style="display: none" @change="handleImageUpload" />
  110. </div> -->
  111. <input type="text" v-model="inputMessage" placeholder="给 专家 发送消息" @keyup.enter="sendTextMessage" />
  112. <div class="send" @click="sendTextMessage">发送</div>
  113. </div>
  114. <!-- 图片预览模态框 -->
  115. <div v-if="previewImage" class="image-preview" @click="previewImage = null">
  116. <img :src="previewImage" alt="预览" />
  117. </div>
  118. </div>
  119. </div>
  120. </template>
  121. <script setup>
  122. import { ref, nextTick, onDeactivated, onMounted } from "vue";
  123. import { useRouter ,useRoute} from "vue-router";
  124. import { base_img_url2 } from "@/api/config";
  125. import { getFileExt } from "@/utils/util";
  126. import UploadFile from "@/utils/upliadFile";
  127. import MqttClient from "@/plugins/MqttClient";
  128. import customHeader from "@/components/customHeader.vue";
  129. const resize = "?x-oss-process=image/resize,p_120/format,webp/quality,q_100";
  130. const router = useRouter();
  131. const route = useRoute();
  132. const props = defineProps({
  133. text: {
  134. type: String,
  135. defalut: "",
  136. },
  137. img: {
  138. type: String,
  139. defalut: "",
  140. },
  141. userId: {
  142. type: [String, Number],
  143. defalut: "",
  144. },
  145. });
  146. const curUserId = Number(localStorage.getItem("MINI_USER_ID"));
  147. const senderIcon = ref("");
  148. const receiverIcon = ref("");
  149. const receiverIdVal = ref(null);
  150. // 本地用户头像
  151. const localUserInfoIcon = (() => {
  152. try {
  153. const info = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
  154. return info?.icon || "";
  155. } catch (e) {
  156. return "";
  157. }
  158. })();
  159. // 初始化本地头像为默认发送者头像
  160. senderIcon.value = localUserInfoIcon;
  161. // mqtt 连接
  162. const mqttClient = ref(null);
  163. const messagesContainer = ref(null);
  164. // 消息数据
  165. const messages = ref([]);
  166. // 输入相关
  167. const inputMessage = ref("");
  168. const fileInput = ref(null);
  169. function handleImageLoad() {
  170. scrollToBottom();
  171. }
  172. const handleDetailClick = () => {
  173. router.push('/interaction_list');
  174. }
  175. // 图片预览
  176. const previewImage = ref(null);
  177. const userId = ref(null);
  178. const handleCardClick = (msg) => {
  179. router.push(msg.linkUrl || msg.content.linkUrl);
  180. }
  181. const handleImgUrl = (url) => {
  182. if (url && url.includes('https://')) {
  183. return url;
  184. } else {
  185. return base_img_url2 + url + resize;
  186. }
  187. }
  188. // 图片处理
  189. const startImageUpload = () => {
  190. fileInput.value.click();
  191. };
  192. const uploadFileObj = new UploadFile();
  193. const handleImageUpload = (event) => {
  194. const file = event.target.files[0];
  195. if (file) {
  196. // 实际项目中应该上传到服务器,这里使用本地URL模拟
  197. const miniUserId = localStorage.getItem("MINI_USER_ID");
  198. let ext = getFileExt(file.name);
  199. let key = `birdseye-look-mini/${miniUserId}/${new Date().getTime()}.${ext}`;
  200. let imageUrl = "";
  201. uploadFileObj.put(key, file).then((resFilename) => {
  202. imageUrl = base_img_url2 + resFilename;
  203. sendImageMessage(imageUrl);
  204. });
  205. }
  206. };
  207. const showImagePreview = (imageUrl) => {
  208. previewImage.value = imageUrl;
  209. };
  210. // 发送图片消息
  211. const sendImageMessage = (thumbnailUrl) => {
  212. const message = {
  213. sender: "sent",
  214. messageType: "image",
  215. senderIcon: senderIcon.value,
  216. content: thumbnailUrl,
  217. };
  218. sendMessage(message);
  219. };
  220. //发送消息接口
  221. //类型 text ,file,image
  222. const sendMsg = (messageType = "text", content = "", obj = {}) => {
  223. const params = {
  224. farmId: farmVal.value,
  225. senderId: curUserId,
  226. receiverId: userId.value,
  227. content,
  228. [messageType]:obj,
  229. messageType,
  230. };
  231. VE_API.bbs.sendMsg(params);
  232. };
  233. // 发送消息
  234. const sendMessage = (message) => {
  235. if (message.messageType === "text") {
  236. sendMsg("text", message.content);
  237. } else if (message.messageType === "image") {
  238. // 按新协议:不传 content,传 image 对象
  239. sendMsg("image", "", { url: message.content, thumbnailUrl: message.content + resize });
  240. } else if (message.messageType === "report") {
  241. // 对话样式消息不发送到服务器,只显示在本地
  242. console.log("发送对话样式消息:", message);
  243. if(message.reportType === 'farm_report'){
  244. sendMsg('report','',{
  245. title: message.title,
  246. reportId: message.reportId,
  247. reportType: message.reportType,
  248. });
  249. }else{
  250. sendMsg('report','',{
  251. title: message.title,
  252. reportId: message.reportId,
  253. reportType: message.reportType,
  254. });
  255. console.log('其他文件1');
  256. }
  257. }else{
  258. sendMsg('card','',{
  259. title: message.title,
  260. coverUrl: message.coverUrl,
  261. cardType: message.cardType,
  262. linkUrl: message.linkUrl
  263. });
  264. }
  265. messages.value.push(message);
  266. scrollToBottom();
  267. };
  268. // 发送文本消息
  269. const sendTextMessage = () => {
  270. if (inputMessage.value.trim()) {
  271. const message = {
  272. sender: "sent",
  273. messageType: "text",
  274. senderIcon: senderIcon.value,
  275. content: inputMessage.value,
  276. };
  277. sendMessage(message);
  278. inputMessage.value = "";
  279. }
  280. };
  281. const scrollToBottom = () => {
  282. nextTick(() => {
  283. setTimeout(() => {
  284. if (messagesContainer.value) {
  285. messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
  286. }
  287. }, 300);
  288. });
  289. };
  290. const farmVal = ref("");
  291. const curRole = ref(null);
  292. // 点击农场报告对话框
  293. const handleReportClick = (msg) => {
  294. if(msg.reportType === 'farm_report' || msg.messageType === 'report'){
  295. const params = {
  296. farmId: msg.reportId || msg.content?.reportId,
  297. showFilter: true,
  298. }
  299. router.push(`/farm_report?miniJson=${JSON.stringify(params)}`);
  300. }else{
  301. console.log('其他文件');
  302. }
  303. }
  304. // 页面加载时自动添加欢迎消息
  305. onMounted(() => {
  306. const welcomeMessage = {
  307. sender: "received",
  308. messageType: "text",
  309. content: "您好,我叫冼继东。我是种植专家,介绍专家介绍专家",
  310. receiverIcon: receiverIcon.value || 'https://birdseye-img.sysuimars.com/dinggou-mini/defalut-icon.png',
  311. };
  312. const questionMessage = {
  313. sender: "received",
  314. messageType: "question",
  315. content: "为了更方便分析农场问题,请先采集农情互动信息",
  316. receiverIcon: receiverIcon.value || 'https://birdseye-img.sysuimars.com/dinggou-mini/defalut-icon.png',
  317. };
  318. messages.value.push(welcomeMessage,questionMessage);
  319. scrollToBottom();
  320. });
  321. onDeactivated(() => {
  322. mqttClient.value && mqttClient.value.client.end(true);
  323. });
  324. </script>
  325. <style scoped lang="scss">
  326. .consult {
  327. width: 100%;
  328. height: calc(100vh - 40px);
  329. box-sizing: border-box;
  330. .consult-content {
  331. width: 100%;
  332. height: 100%;
  333. display: flex;
  334. flex-direction: column;
  335. position: relative;
  336. }
  337. }
  338. /* 聊天消息区域样式 */
  339. .chat-messages {
  340. flex: 1;
  341. padding: 12px;
  342. overflow-y: auto;
  343. background-color: #fff;
  344. box-sizing: border-box;
  345. .message {
  346. display: flex;
  347. margin-bottom: 15px;
  348. }
  349. .received {
  350. justify-content: flex-start;
  351. .bubble {
  352. background-color: #F4F5F8;
  353. border-radius: 0 10px 10px 10px;
  354. padding: 10px 12px;
  355. box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
  356. }
  357. }
  358. .sent {
  359. justify-content: flex-end;
  360. .bubble {
  361. background-color: #07c160;
  362. border-radius: 10px 0 10px 10px;
  363. padding: 10px 15px;
  364. color: #fff;
  365. box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
  366. }
  367. }
  368. .avatar {
  369. width: 40px;
  370. height: 40px;
  371. border-radius: 50%;
  372. background-color: #07c160;
  373. color: white;
  374. display: flex;
  375. align-items: center;
  376. justify-content: center;
  377. margin-right: 10px;
  378. font-weight: bold;
  379. }
  380. .avatar-r {
  381. margin: 0 0 0 10px;
  382. }
  383. .bubble {
  384. max-width: 70%;
  385. }
  386. }
  387. .content {
  388. font-size: 16px;
  389. color: #666666;
  390. }
  391. .input-area {
  392. display: flex;
  393. align-items: center;
  394. padding: 15px 10px;
  395. border-top: 1px solid #e6e6e6;
  396. background-color: white;
  397. position: relative;
  398. width: 100%;
  399. box-sizing: border-box;
  400. input {
  401. flex: 1;
  402. padding: 10px;
  403. border: 1px solid #e6e6e6;
  404. border-radius: 20px;
  405. outline: none;
  406. }
  407. .send {
  408. margin-left: 10px;
  409. padding: 8px 20px;
  410. background-color: #07c160;
  411. color: white;
  412. border: none;
  413. border-radius: 20px;
  414. cursor: pointer;
  415. }
  416. }
  417. /* 新增的多媒体消息样式 */
  418. .image-message {
  419. img {
  420. max-width: 200px;
  421. max-height: 200px;
  422. border-radius: 8px;
  423. cursor: pointer;
  424. }
  425. }
  426. /* 图片消息不使用对话气泡样式 */
  427. .no-bubble {
  428. background: transparent !important;
  429. border-radius: 0 !important;
  430. padding: 0 !important;
  431. box-shadow: none !important;
  432. color: inherit !important;
  433. }
  434. .card-bubble{
  435. background: #fff !important;
  436. }
  437. /* 工具栏样式 */
  438. .toolbar {
  439. display: flex;
  440. align-items: center;
  441. button {
  442. background: none;
  443. border: none;
  444. font-size: 20px;
  445. margin-right: 10px;
  446. cursor: pointer;
  447. padding: 5px;
  448. }
  449. .link {
  450. font-size: 24px;
  451. margin-right: 10px;
  452. }
  453. }
  454. /* 图片预览 */
  455. .image-preview {
  456. position: fixed;
  457. top: 0;
  458. left: 0;
  459. right: 0;
  460. bottom: 0;
  461. background: rgba(0, 0, 0, 0.8);
  462. display: flex;
  463. align-items: center;
  464. justify-content: center;
  465. z-index: 1000;
  466. }
  467. .image-preview img {
  468. max-width: 90%;
  469. max-height: 90%;
  470. object-fit: contain;
  471. }
  472. /* 对话样式消息 */
  473. .dialog-message {
  474. max-width: 100%;
  475. background: #fff !important;
  476. border-radius: 10px;
  477. .report-title {
  478. font-size: 16px;
  479. font-weight: 600;
  480. color: #000;
  481. margin-bottom: 5px;
  482. }
  483. .dialog-title {
  484. font-size: 12px;
  485. color: rgba(0, 0, 0, 0.6);
  486. margin-bottom: 10px;
  487. }
  488. .monitor-image {
  489. width: 222px;
  490. height: 180px;
  491. object-fit: cover;
  492. }
  493. .farm-report-content,
  494. .farm-work-content {
  495. .report-details,
  496. .work-details {
  497. background: #f8f9fa;
  498. border-radius: 8px;
  499. padding: 12px;
  500. margin-top: 10px;
  501. .detail-item {
  502. display: flex;
  503. margin-bottom: 6px;
  504. font-size: 13px;
  505. &:last-child {
  506. margin-bottom: 0;
  507. }
  508. .detail-label {
  509. color: #666;
  510. min-width: 80px;
  511. }
  512. .detail-value {
  513. color: #333;
  514. flex: 1;
  515. }
  516. }
  517. }
  518. }
  519. }
  520. .question-message{
  521. .question-title{
  522. font-size: 16px;
  523. color: #666666;
  524. margin-bottom: 6px;
  525. }
  526. .image-wrap{
  527. display: flex;
  528. // flex-wrap: wrap;
  529. gap: 10px;
  530. img{
  531. flex: 1;
  532. width: 75px;
  533. height: 70px;
  534. border-radius: 8px;
  535. object-fit: cover;
  536. }
  537. }
  538. .btn-detail{
  539. font-size: 14px;
  540. margin-top: 8px;
  541. background: #FFFFFF;
  542. border-radius: 6px;
  543. text-align: center;
  544. padding: 6px;
  545. }
  546. }
  547. /* 我方消息中的对话样式 */
  548. .message.sent .dialog-message {
  549. background: #e3f2fd;
  550. .work-details {
  551. background: #f0f8ff;
  552. }
  553. }
  554. </style>