AIRobot.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. <template>
  2. <div class="ai-layout" :class="{ 'is-mobile': isMobile }">
  3. <div
  4. class="open-chat"
  5. :class="{
  6. 'toggle-off': isOpen,
  7. 'toggle-on': !isOpen
  8. }"
  9. @click="toggleChatOpen('open')"
  10. >
  11. <img
  12. src="./img/layout/ai-figure-base.png"
  13. alt=""
  14. :class="{ 'is-drag': isDrag }"
  15. @dragstart.prevent
  16. />
  17. <div class="open-tip">
  18. <div class="hi"> HI,我是AI机器人,有什么想要了解的可以问我哦 </div>
  19. </div>
  20. </div>
  21. <div class="chat-container" :class="[isOpen ? 'show' : 'hide', isExpand ? 'is-expand' : '']">
  22. <div class="chat-bg"></div>
  23. <div class="chat-topbar">
  24. <div class="chat-topbar-text" v-if="!isMobile" @dragstart.prevent>AI机器人</div>
  25. <!-- 移动端下顶部的ai图片部分 -->
  26. <template v-if="isMobile">
  27. <!-- web是右上角关闭按钮 H5是左上角回退按钮 -->
  28. <img
  29. src="./img/icon/back.png"
  30. class="close-back"
  31. alt=""
  32. @click="toggleChatOpen('close')"
  33. />
  34. </template>
  35. <div class="chat-topbar-operates">
  36. <div class="chat-topbar-operates_icon newChat" @click="handleNewChat">+新对话</div>
  37. <div class="chat-topbar-operates_icon" @click="handleExpand">
  38. <img alt="" v-if="isExpand" src="./img/icon/contract.png" />
  39. <img alt="" v-else src="./img/icon/expand.png" />
  40. </div>
  41. <div v-if="!isMobile" class="chat-topbar-operates_icon" @click="toggleChatOpen">
  42. <img alt="" src="./img/icon/close.png" />
  43. </div>
  44. </div>
  45. </div>
  46. <div class="chat-main">
  47. <div class="chat-figure">
  48. <img src="./img/layout/ai-figure-base.png" alt="" @dragstart.prevent />
  49. </div>
  50. <div class="chat-window">
  51. <AiChat
  52. class="chat-content"
  53. ref="AiChatRef"
  54. :is-open="isOpen"
  55. @handle-send="handleSend"
  56. />
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. </template>
  62. <script setup lang="ts">
  63. import AiChat from './components/AiChat.vue'
  64. import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
  65. import {Recordable} from "vite-plugin-mock";
  66. const props = defineProps({
  67. a: {
  68. type: Boolean,
  69. default: false
  70. }
  71. })
  72. console.log('props', props)
  73. const isOpen = ref(false)
  74. const isExpand = ref(false)
  75. const isMobile = computed(() => /Mobile/i.test(navigator.userAgent))
  76. const isDrag = ref(false) // 用于在拖拽过程中 隐藏logo tip
  77. // 记录聊天框的鼠标点击处的位置,用于在下面move方法中处理拖拽边界问题上的计算
  78. const chatBarClickPos: Recordable = ref({})
  79. onMounted(() => {
  80. nextTick(() => {
  81. const aiLogoBox = document.querySelector('.open-chat') as HTMLElement
  82. if (isMobile.value) {
  83. // 防冒泡
  84. aiLogoBox.addEventListener('touchstart', (e) => {
  85. const pageX = e.touches[0].pageX
  86. const pageY = e.touches[0].pageY
  87. chatBarClickPos.value = {
  88. right: aiLogoBox.offsetWidth - (pageX - aiLogoBox.offsetLeft),
  89. bottom: aiLogoBox.offsetHeight - (pageY - aiLogoBox.offsetTop)
  90. }
  91. isDrag.value = false
  92. aiLogoBox.addEventListener('touchmove', mouseMove)
  93. })
  94. aiLogoBox.addEventListener('touchend', () => {
  95. aiLogoBox.removeEventListener('touchmove', mouseMove)
  96. })
  97. isDrag.value = false
  98. } else {
  99. aiLogoBox.addEventListener('mousedown', (e) => {
  100. chatBarClickPos.value = {
  101. right: aiLogoBox.offsetWidth - e.offsetX,
  102. bottom: aiLogoBox.offsetHeight - e.offsetY
  103. }
  104. isDrag.value = false
  105. document.addEventListener('mousemove', mouseMove)
  106. })
  107. document.addEventListener('mouseup', () => {
  108. document.removeEventListener('mousemove', mouseMove)
  109. setTimeout(() => {
  110. isDrag.value = false
  111. }, 100)
  112. })
  113. }
  114. })
  115. })
  116. onUnmounted(() => {
  117. const aiLogoBox = document.querySelector('.open-chat img') as HTMLElement
  118. if (isMobile.value) {
  119. aiLogoBox && aiLogoBox.removeEventListener('touchend', () => {})
  120. // document.body.removeEventListener('touchend', () => {})
  121. } else {
  122. document.removeEventListener('mouseup', () => {})
  123. }
  124. })
  125. const mouseMove = (e) => {
  126. // 可视区宽度
  127. const clientWidth = document.body.clientWidth
  128. const clientHeight = document.body.clientHeight
  129. const pageX = isMobile.value ? e.touches[0].pageX : e.pageX
  130. const pageY = isMobile.value ? e.touches[0].pageY : e.pageY
  131. // 拖拽过程中 隐藏tip,并点击抬起后不打开聊天框
  132. isDrag.value = true
  133. const aiLogoBox = document.querySelector('.open-chat') as HTMLElement
  134. // 处理边界问题
  135. const r = clientWidth - pageX - chatBarClickPos.value.right
  136. aiLogoBox.style.right = `${r < 0 ? 0 : r > clientWidth - aiLogoBox.offsetWidth ? clientWidth - aiLogoBox.offsetWidth : r}px`
  137. const b = clientHeight - pageY - chatBarClickPos.value.bottom
  138. aiLogoBox.style.bottom = `${b < 0 ? 0 : b > clientHeight - aiLogoBox.offsetHeight ? clientHeight - aiLogoBox.offsetHeight : b}px`
  139. }
  140. // 打开关闭弹窗
  141. const toggleChatOpen = (type) => {
  142. if (isDrag.value) return
  143. isOpen.value = type === 'open'
  144. }
  145. // 展开收起
  146. const handleExpand = () => {
  147. isExpand.value = !isExpand.value
  148. }
  149. const AiChatRef = ref()
  150. const handleNewChat = () => {
  151. AiChatRef.value.newChat()
  152. }
  153. // 发送消息
  154. const emit = defineEmits(['onMessage'])
  155. const handleSend = (message) => {
  156. emit('onMessage', message)
  157. }
  158. // 接受消息
  159. const handleResponse = (message) => {
  160. AiChatRef.value?.handleMessageResponse(message)
  161. }
  162. defineExpose({ handleResponse })
  163. </script>
  164. <style lang="less">
  165. @import './styles/variables.less';
  166. @import './styles/index.less';
  167. // 移动端下点击的高亮
  168. .ai-layout * {
  169. -webkit-tap-highlight-color: transparent;
  170. }
  171. .chat-container {
  172. position: fixed;
  173. bottom: 0;
  174. right: 0;
  175. width: 680px;
  176. height: 680px;
  177. max-height: 90%;
  178. z-index: 101;
  179. font-size: 14px;
  180. transform: scale(0);
  181. transform-origin: right bottom;
  182. display: flex;
  183. flex-direction: column;
  184. align-items: center;
  185. transition:
  186. width 0.5s,
  187. height 0.5s;
  188. // 移动端默认字体16px
  189. //.is-mobile & {
  190. // font-size: 16px;
  191. // line-height: 24px;
  192. //}
  193. .chat-bg {
  194. width: 100%;
  195. height: 100%;
  196. position: absolute;
  197. left: 0;
  198. top: 0;
  199. background-image: url('img/layout/chat-bg.jpg');
  200. background-color: transparent;
  201. background-repeat: no-repeat;
  202. background-size: 100% auto;
  203. box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 0.1);
  204. overflow: hidden;
  205. border-radius: 12px;
  206. }
  207. .chat-topbar {
  208. height: 52px;
  209. width: 100%;
  210. flex-shrink: 0;
  211. z-index: 1;
  212. display: flex;
  213. justify-content: space-between;
  214. align-items: center;
  215. &-text {
  216. font-size: 20px;
  217. font-weight: bold;
  218. color: #fff;
  219. margin-left: 100px;
  220. }
  221. &-operates {
  222. display: flex;
  223. margin-right: 10px;
  224. &_icon {
  225. margin-right: 8px;
  226. width: 26px;
  227. height: 26px;
  228. background-color: #fff;
  229. border-radius: 6px;
  230. box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08);
  231. display: flex;
  232. justify-content: center;
  233. align-items: center;
  234. cursor: pointer;
  235. color: #666;
  236. &.newChat {
  237. width: auto;
  238. padding: 0 8px;
  239. }
  240. }
  241. }
  242. }
  243. .chat-main {
  244. flex: 1;
  245. width: 100%;
  246. height: calc(100% - 52px);
  247. display: flex;
  248. .chat-figure {
  249. width: 90px;
  250. height: 100px;
  251. position: absolute;
  252. left: 0;
  253. top: -30px;
  254. overflow: hidden;
  255. text-align: center;
  256. img {
  257. width: 80%;
  258. }
  259. }
  260. .chat-window {
  261. flex: 1;
  262. display: flex;
  263. flex-direction: column;
  264. width: 100%;
  265. height: 100%;
  266. border-radius: 8px;
  267. overflow: hidden;
  268. .chat-content {
  269. position: relative;
  270. overflow: auto;
  271. width: 100%;
  272. flex: 1;
  273. }
  274. }
  275. }
  276. &:not(.is-expand) {
  277. .chat-content-ai {
  278. background: linear-gradient(180deg, rgba(235, 239, 248, 0.56), #ebeff8);
  279. border: 1px solid;
  280. border-image: linear-gradient(180deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0)) 1 1;
  281. backdrop-filter: blur(4px);
  282. }
  283. }
  284. &.is-expand {
  285. width: 90%;
  286. max-width: 1200px;
  287. height: 90%;
  288. max-height: 900px;
  289. // 展开时 样式
  290. //overflow: hidden;
  291. border-radius: 16px;
  292. .chat-topbar {
  293. &-ai {
  294. display: none;
  295. }
  296. &-text {
  297. margin-left: 30px;
  298. font-size: 28px;
  299. }
  300. }
  301. .chat-main {
  302. .chat-figure {
  303. width: 376px;
  304. display: flex;
  305. justify-content: center;
  306. align-items: center;
  307. position: static;
  308. height: auto;
  309. margin-bottom: 100px;
  310. z-index: 99;
  311. img {
  312. width: 100%;
  313. }
  314. }
  315. }
  316. }
  317. &.show {
  318. animation: scaleIn 0.15s ease-in-out 0s 1 normal forwards;
  319. }
  320. &.hide {
  321. display: none;
  322. }
  323. }
  324. .is-mobile .chat-container {
  325. bottom: 0;
  326. right: 0;
  327. width: 100%;
  328. height: 100%;
  329. max-height: 100%;
  330. .chat-bg {
  331. background-image: url('img/layout/chat-bg-h5.jpg');
  332. }
  333. .chat-topbar {
  334. height: 56px;
  335. position: absolute;
  336. left: 0;
  337. top: 12px;
  338. z-index: 100;
  339. display: flex;
  340. justify-content: space-between;
  341. .close-back {
  342. margin-left: 22px;
  343. }
  344. }
  345. .chat-main {
  346. position: relative;
  347. width: 100%;
  348. height: 100%;
  349. .chat-figure {
  350. width: 100%;
  351. display: flex;
  352. justify-content: center;
  353. align-items: flex-start;
  354. position: static;
  355. height: auto;
  356. img {
  357. width: 24%;
  358. transition: width 0.5s;
  359. margin-top: 20px;
  360. }
  361. }
  362. .chat-window {
  363. position: absolute;
  364. bottom: 0;
  365. left: 0;
  366. z-index: 99;
  367. width: 100%;
  368. height: calc(100% - 130px);
  369. transition: height 0.5s;
  370. border-radius: 16px 16px 0 0;
  371. box-shadow: 0px -3px 10px 0px rgba(0, 0, 0, 0.06);
  372. overflow: hidden;
  373. .chat-content-ai {
  374. border: none;
  375. }
  376. }
  377. }
  378. &.is-expand {
  379. .chat-figure {
  380. img {
  381. width: 64%;
  382. }
  383. }
  384. .chat-window {
  385. height: calc(100% - 354px);
  386. min-height: 66%;
  387. backdrop-filter: blur(10px);
  388. }
  389. }
  390. }
  391. .open-chat {
  392. position: fixed;
  393. width: 60px;
  394. right: 30px;
  395. bottom: 200px;
  396. cursor: pointer;
  397. z-index: 101;
  398. transform: scale(0);
  399. .is-mobile & {
  400. width: 60px;
  401. right: 10px;
  402. bottom: 100px;
  403. .open-tip {
  404. display: none !important;
  405. }
  406. }
  407. img {
  408. width: 100%;
  409. transition: all 0.5s;
  410. position: relative;
  411. z-index: 99;
  412. &:not(.is-drag):hover + .open-tip {
  413. display: block;
  414. animation: scaleIn 0.15s ease-in-out 0.15s 1 normal forwards;
  415. }
  416. // 拖动过程中隐藏
  417. &:has(.is-drag) {
  418. display: none;
  419. }
  420. }
  421. .open-tip {
  422. position: absolute;
  423. right: -4px;
  424. top: 22px;
  425. z-index: 98;
  426. width: 236px;
  427. font-size: 14px;
  428. font-weight: 600;
  429. color: #315e97;
  430. padding: 12px 64px 12px 16px;
  431. background: linear-gradient(180deg, #dbf1ff, #ffffff);
  432. border: 1px solid;
  433. border-image: linear-gradient(360deg, rgba(255, 255, 255, 0), #ffffff) 1 1;
  434. border-radius: 8px;
  435. box-shadow: 0px 4px 20px 0px rgba(76, 107, 148, 0.15);
  436. display: none;
  437. transform: scale(0);
  438. transform-origin: top right;
  439. &:hover {
  440. display: block;
  441. animation: scaleIn 0.15s ease-in-out 0.15s 1 normal forwards;
  442. }
  443. .hi {
  444. color: #1d80ff;
  445. line-height: 26px;
  446. & + div {
  447. width: 120px;
  448. text-align: justify;
  449. text-align-last: justify;
  450. }
  451. }
  452. }
  453. &.toggle-off {
  454. display: none;
  455. }
  456. &.toggle-on {
  457. animation: scaleIn 0.15s ease-in-out 0.15s 1 normal forwards;
  458. }
  459. }
  460. @keyframes scaleIn {
  461. 0% {
  462. transform: scale(0);
  463. }
  464. 100% {
  465. transform: scale(1);
  466. }
  467. }
  468. </style>