index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <template>
  2. <div class="counsel-container">
  3. <div class="counsel-header">
  4. <span>会话<i class="ai el-icon-chat-dot-round"></i></span>
  5. <i class="el-icon-close cursor" @click="closeMsg"></i>
  6. </div>
  7. <div class="counsel-chats" ref="scrollRef">
  8. <div class="counsel-left flex" v-if="nowType == 'selectLand'">
  9. <div class="header-img">
  10. <img src="/static/images/aiModel/kefu.png" />
  11. </div>
  12. <div class="content">
  13. <span>Hi,有问题您可以这样问我:</span>
  14. <span class="high" @click="sendDefault">{{ defaultMsg }}</span>
  15. </div>
  16. </div>
  17. <!--会话内容-->
  18. <div v-for="(item, index) in questionList" :key="index">
  19. <!--右侧-->
  20. <div class="counsel-right flex" v-if="item.type == 0">
  21. <i class="el-icon-loading" v-if="item.mstatus == 0"></i>
  22. <i
  23. class="el-icon-warning cursor"
  24. v-else-if="item.mstatus == 1"
  25. @click="sendToBackend(item.content, index)"
  26. ></i>
  27. <div class="content">
  28. <!-- autoplay -->
  29. <audio v-if="item.mp3url" controls :src="item.mp3url"></audio>
  30. <div v-if="item.content">
  31. <span>{{ item.content }}</span>
  32. <i class="el-icon-document-copy" @click="copy(item)"></i>
  33. </div>
  34. </div>
  35. <div class="header-img">
  36. <img src="/static/images/aiModel/default.png" />
  37. </div>
  38. </div>
  39. <!--左侧-->
  40. <div class="counsel-left flex" v-else>
  41. <div class="header-img">
  42. <img src="/static/images/aiModel/kefu.png" />
  43. </div>
  44. <div class="content">
  45. <!-- breaks:是否将连续的换行符转换为<br>标签,默认值为false。
  46. typographer:是否启用智能引号和破折号等Typographer功能,默认值为false。
  47. linkify:是否将URL转换为链接,默认值为false。
  48. highlight:是否启用代码高亮,默认值为true。 -->
  49. <VueMarkdown
  50. v-if="item.type == 'answer'"
  51. class="markdowm"
  52. :source="item.content"
  53. :breaks="true"
  54. :linkify="true"
  55. :highlight="false"
  56. ></VueMarkdown>
  57. <div v-else>
  58. <span v-if="item.data" class="tit">参考意见</span>
  59. <span :class="{ high: item.type }" @click="onClick(item)">
  60. {{ item.content }}
  61. </span>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. </div>
  67. <div class="consel-types">
  68. <div
  69. v-for="(titem, i) in types"
  70. :key="i"
  71. class="typeitem cursor"
  72. :class="{ heigType: nowType == titem.value }"
  73. @click="nowType = titem.value"
  74. >
  75. <i :class="titem.icon"></i>
  76. {{ titem.label }}
  77. </div>
  78. </div>
  79. <div class="voice" v-if="spdisabled">
  80. <div>正在识别</div>
  81. <div class="noise"></div>
  82. </div>
  83. <div class="searchBtom">
  84. <div
  85. class="icon"
  86. :class="
  87. spdisabled ? 'el-icon-turn-off-microphone' : 'el-icon-microphone'
  88. "
  89. title="按住说话"
  90. :disabled="spdisabled"
  91. @click="voice"
  92. ></div>
  93. <el-input
  94. clearable
  95. v-model="sendContent"
  96. @input="change_witch"
  97. @keyup.enter.native="send"
  98. class="search"
  99. placeholder="请输入问题"
  100. ></el-input>
  101. <div
  102. class="el-icon-s-promotion icon sendIcon"
  103. :class="sendContent ? 'ok' : 'no'"
  104. @click="send"
  105. ></div>
  106. </div>
  107. </div>
  108. </template>
  109. <script>
  110. import { AddFzxz } from "../../api/ghss/ghxz.js";
  111. import { GetMsg, uploadAudio, closeMsg } from "../../api/aiModel.js";
  112. import record from "./record.js";
  113. import VueMarkdown from "vue-markdown";
  114. let recognition = null;
  115. export default {
  116. props: {},
  117. components: {
  118. VueMarkdown,
  119. },
  120. data() {
  121. return {
  122. questionList: [],
  123. defaultMsg:
  124. "我要在抱坡区进行选址,用地面积30到50亩,选择居住用地,距离幼儿园200米,距离医院300米,距离火葬场大于5000米",
  125. sendContent: "",
  126. spdisabled: false,
  127. nowType: "selectLand",
  128. types: [
  129. { label: "智能选址", value: "selectLand", icon: "el-icon-s-home" },
  130. { label: "知识问答", value: "answer", icon: "el-icon-chat-line-round" },
  131. ],
  132. };
  133. },
  134. created() {},
  135. mounted() {},
  136. methods: {
  137. initSpeech() {
  138. // 检查浏览器是否支持SpeechRecognition
  139. const SpeechRecognition =
  140. window.SpeechRecognition || window.webkitSpeechRecognition;
  141. if (!SpeechRecognition) {
  142. alert(
  143. "你的浏览器不支持语音识别功能,请使用支持的浏览器,例如Google Chrome。"
  144. );
  145. } else {
  146. recognition = new SpeechRecognition();
  147. recognition.lang = "zh-CN"; // 设置识别语言为中文
  148. recognition.interimResults = false; // 不返回临时结果
  149. recognition.maxAlternatives = 1; // 返回的识别结果数量
  150. recognition.onstart = () => {
  151. this.$message.success("开始语音识别...");
  152. this.spdisabled = true;
  153. };
  154. recognition.onresult = (event) => {
  155. this.sendContent = event.results[0][0].transcript;
  156. // this.send(); // 将识别结果发送到后台
  157. };
  158. recognition.onerror = (event) => {
  159. this.$message.warning("语音识别出错: " + event.error);
  160. };
  161. recognition.onend = () => {
  162. // this.$message.success("语音识别结束:");
  163. console.log("语音识别结束");
  164. this.spdisabled = false;
  165. };
  166. }
  167. },
  168. /**初始化 */
  169. init() {
  170. let _this = this;
  171. record.getPermission(function (permiss) {
  172. if (permiss.status == "fail") {
  173. _this.$message.warning("语音识别出错: " + permiss.data);
  174. } else {
  175. record.startRecorder();
  176. _this.$message.success("开始语音识别...");
  177. _this.spdisabled = true;
  178. }
  179. });
  180. },
  181. /**结束录音 */
  182. stop() {
  183. let _this = this;
  184. record.stopRecorder(function (res) {
  185. _this.upload(res);
  186. });
  187. this.spdisabled = false;
  188. },
  189. copy(item) {
  190. this.sendContent = item.content;
  191. },
  192. sendDefault() {
  193. this.sendContent = this.defaultMsg;
  194. // this.send();
  195. },
  196. send() {
  197. // 咨询问题
  198. if (this.sendContent) {
  199. this.questionList.push({
  200. type: 0,
  201. content: this.sendContent,
  202. mstatus: 0,
  203. });
  204. this.sendToBackend(this.sendContent, this.questionList.length - 1);
  205. this.sendContent = "";
  206. }
  207. },
  208. voice() {
  209. console.log("---this.spdisabled-", this.spdisabled);
  210. if (!this.spdisabled) {
  211. // if (!recognition) this.initSpeech();
  212. // recognition.start(); // 开始语音识别
  213. this.init();
  214. } else {
  215. this.stop();
  216. }
  217. },
  218. upload({ blob, formData }) {
  219. this.questionList.push({
  220. type: 0,
  221. mp3url: (window.URL || webkitURL).createObjectURL(blob),
  222. mstatus: 0,
  223. });
  224. let mindex = this.questionList.length - 1;
  225. /**处理 */
  226. uploadAudio(formData)
  227. .then((ares) => {
  228. if (ares.status == 200) {
  229. this.questionList[mindex].content = ares.data.voiceMsg;
  230. this.sendToBackend(ares.data.voiceMsg, mindex);
  231. } else {
  232. this.$message.error(ares.data.msg);
  233. }
  234. })
  235. .catch(() => {});
  236. },
  237. sendToBackend(msg, mindex) {
  238. this.questionList[mindex].mstatus = 0;
  239. GetMsg({ msg, type: this.nowType })
  240. .then((mres) => {
  241. if (mres.status == 200) {
  242. this.questionList[mindex].mstatus = 2;
  243. if (mres.data.code == 200 && mres.data.data) {
  244. if (mres.data.type == "selectLand") {
  245. this.questionList.push({
  246. type: mres.data.type,
  247. content: "分析中...请稍等",
  248. });
  249. this.AddFzxz(mres.data.data, mres.data.type);
  250. } else {
  251. this.questionList.push({
  252. type: mres.data.type,
  253. content: mres.data.data,
  254. });
  255. }
  256. } else {
  257. this.questionList.push({
  258. content: mres.data.data || "抱歉!缺少分析数据",
  259. });
  260. }
  261. } else {
  262. this.questionList[mindex].mstatus = 1;
  263. // this.$message.error("发送到后台时发生错误:");
  264. }
  265. })
  266. .catch(() => {
  267. this.questionList[mindex].mstatus = 1;
  268. // this.$message.error("发送到后台时发生错误:");
  269. });
  270. },
  271. AddFzxz(xzdata, type) {
  272. const loading = this.$loading({
  273. lock: true,
  274. text: "分析中",
  275. spinner: "el-icon-loading",
  276. background: "rgba(0, 0, 0, 0.7)",
  277. });
  278. AddFzxz(xzdata)
  279. .then((res) => {
  280. console.log("xuanz", res);
  281. loading.close();
  282. this.questionList.push({
  283. type,
  284. content: "查看智能选址结果",
  285. data: res.data,
  286. });
  287. this.$message({
  288. message: res.message,
  289. type: res.success ? "success" : "warning",
  290. });
  291. })
  292. .catch((error) => {
  293. loading.close();
  294. this.$message.error(error);
  295. });
  296. },
  297. onClick(item) {
  298. if (item.data) return;
  299. if (item.type == "selectLand") {
  300. this.$router.push({
  301. path: "/siteselection",
  302. query: { rwbsm: item.data.rwbsm },
  303. });
  304. this.$emit("close");
  305. }
  306. },
  307. closeMsg() {
  308. this.$emit("close");
  309. closeMsg().then((ares) => {
  310. if (ares.status == 200) {
  311. this.questionList[mindex].content = ares.data.voiceMsg;
  312. this.sendToBackend(ares.data.voiceMsg, mindex);
  313. } else {
  314. this.$message.error(ares.data.msg);
  315. }
  316. });
  317. },
  318. },
  319. watch: {
  320. questionList() {
  321. this.$nextTick(() => {
  322. this.$refs.scrollRef.scrollTop = this.$refs.scrollRef.scrollHeight;
  323. });
  324. },
  325. },
  326. };
  327. </script>
  328. <style lang="scss" scoped>
  329. .flex {
  330. display: flex;
  331. }
  332. //智能咨询
  333. .counsel-container {
  334. padding: 15px;
  335. width: 100%;
  336. height: 100%;
  337. position: absolute;
  338. background: #222;
  339. .counsel-header {
  340. display: flex;
  341. align-items: center;
  342. padding: 10px 20px;
  343. font-size: 16px;
  344. color: #fff;
  345. border-bottom: 1px solid #1f4099;
  346. margin-bottom: 10px;
  347. position: relative;
  348. }
  349. .time {
  350. display: block;
  351. text-align: center;
  352. margin-bottom: 20px;
  353. color: #999999;
  354. line-height: 40px;
  355. }
  356. .counsel-chats {
  357. height: calc(100% - 190px);
  358. overflow-x: hidden;
  359. overflow-y: auto;
  360. .header-img {
  361. // flex: 0 0 50px;
  362. margin-left: 10px;
  363. width: 50px;
  364. height: 50px;
  365. border-radius: 50px;
  366. display: block;
  367. overflow: hidden;
  368. img {
  369. width: 50px;
  370. height: 50px;
  371. border-radius: 50px;
  372. object-fit: cover;
  373. }
  374. }
  375. .high {
  376. color: #b6e0ff !important;
  377. cursor: pointer;
  378. display: block;
  379. }
  380. .cursor {
  381. cursor: pointer;
  382. }
  383. .content {
  384. // flex: 1;
  385. margin-left: 10px;
  386. max-width: calc(100% - 100px);
  387. padding: 5px 20px;
  388. border-radius: 8px;
  389. background: #40a0ff6c;
  390. .tit {
  391. display: block;
  392. color: #f99f2b;
  393. margin-bottom: 10px;
  394. }
  395. span {
  396. color: #ffffff;
  397. line-height: 20px;
  398. }
  399. .markdowm {
  400. width: 100%;
  401. word-break: break-word; /* 超出容器宽度时自动换行 */
  402. white-space: pre-wrap; /* 保持空白符序列,但是正常地换行 */
  403. // overflow-wrap: break-word;
  404. /deep/ pre {
  405. white-space: pre-wrap;
  406. }
  407. }
  408. }
  409. .counsel-left {
  410. margin-bottom: 30px;
  411. align-items: flex-start;
  412. .content {
  413. background: #b6e1ff3b;
  414. }
  415. }
  416. .counsel-right {
  417. margin-bottom: 30px;
  418. justify-content: flex-end;
  419. align-items: flex-start;
  420. width: 100%;
  421. i {
  422. font-size: 20px;
  423. }
  424. .el-icon-warning {
  425. color: #ff6666;
  426. }
  427. }
  428. }
  429. audio {
  430. height: 40px;
  431. }
  432. audio::-webkit-media-controls-enclosure {
  433. // background-color: #40a0ff6c;
  434. // color: #fff;
  435. }
  436. }
  437. .consel-types {
  438. position: absolute;
  439. bottom: 100px;
  440. width: calc(100% - 20px);
  441. display: flex;
  442. .typeitem {
  443. border: 1px solid #ffffff;
  444. border-radius: 8px;
  445. padding: 5px 10px;
  446. margin-right: 20px;
  447. }
  448. .heigType {
  449. border-color: #f0c472;
  450. color: #f0c472;
  451. }
  452. }
  453. .voice {
  454. width: 75%;
  455. // height: 100px;
  456. position: absolute;
  457. left: 15%;
  458. bottom: 92px;
  459. text-align: center;
  460. border-radius: 8px;
  461. background: #40a0ff6c;
  462. padding: 5px 20px;
  463. .noise {
  464. width: 110px;
  465. height: 40px;
  466. background-image: url("/static/images/aiModel/noise.png");
  467. background-size: 100% 100%;
  468. margin: 10px auto;
  469. }
  470. }
  471. .searchBtom {
  472. position: absolute;
  473. bottom: 20px;
  474. width: calc(100% - 20px);
  475. height: 70px;
  476. background: #44444e !important;
  477. border: 1px solid rgba(0, 0, 0, 0.12);
  478. box-shadow: #0003 0 0 10px;
  479. border-radius: 8px;
  480. display: flex;
  481. padding-top: 15px;
  482. .search {
  483. width: calc(100% - 120px);
  484. margin: 0 10px;
  485. }
  486. .icon {
  487. width: 40px;
  488. height: 40px;
  489. font-size: 40px;
  490. cursor: pointer;
  491. }
  492. .no {
  493. color: #2f2f39;
  494. }
  495. .ok {
  496. color: #0f7ac8;
  497. }
  498. }
  499. /deep/ .el-icon-close:before {
  500. position: absolute;
  501. top: 10px;
  502. right: 10px;
  503. font-size: larger;
  504. font-weight: bold;
  505. &:hover {
  506. color: aqua;
  507. }
  508. }
  509. </style>