AIRobot.vue 12 KB

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