123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- <template>
- <div class="ai-layout" :class="{ 'is-mobile': isMobile }">
- <div
- class="open-chat"
- :class="{
- 'toggle-off': isOpen,
- 'toggle-on': !isOpen
- }"
- @click="toggleChatOpen('open')"
- >
- <img
- src="./img/layout/ai-figure-base.png"
- alt=""
- :class="{ 'is-drag': isDrag }"
- @dragstart.prevent
- />
- <div class="open-tip">
- <div class="hi"> HI,我是AI机器人,有什么想要了解的可以问我哦 </div>
- </div>
- </div>
- <div class="chat-container" :class="[isOpen ? 'show' : 'hide', isExpand ? 'is-expand' : '']">
- <div class="chat-bg"></div>
- <div class="chat-topbar">
- <div class="chat-topbar-text" v-if="!isMobile" @dragstart.prevent>AI机器人</div>
- <!-- 移动端下顶部的ai图片部分 -->
- <template v-if="isMobile">
- <!-- web是右上角关闭按钮 H5是左上角回退按钮 -->
- <img
- src="./img/icon/back.png"
- class="close-back"
- alt=""
- @click="toggleChatOpen('close')"
- />
- </template>
- <div class="chat-topbar-operates">
- <div class="chat-topbar-operates_icon newChat" @click="handleNewChat">+新对话</div>
- <div class="chat-topbar-operates_icon" @click="handleExpand">
- <img alt="" v-if="isExpand" src="./img/icon/contract.png" />
- <img alt="" v-else src="./img/icon/expand.png" />
- </div>
- <div v-if="!isMobile" class="chat-topbar-operates_icon" @click="toggleChatOpen">
- <img alt="" src="./img/icon/close.png" />
- </div>
- </div>
- </div>
- <div class="chat-main">
- <div class="chat-figure">
- <img src="./img/layout/ai-figure-base.png" alt="" @dragstart.prevent />
- </div>
- <div class="chat-window">
- <AiChat
- class="chat-content"
- ref="AiChatRef"
- :is-open="isOpen"
- @handle-send="handleSend"
- />
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import AiChat from './components/AiChat.vue'
- import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
- import {Recordable} from "vite-plugin-mock";
- const props = defineProps({
- a: {
- type: Boolean,
- default: false
- }
- })
- console.log('props', props)
- const isOpen = ref(false)
- const isExpand = ref(false)
- const isMobile = computed(() => /Mobile/i.test(navigator.userAgent))
- const isDrag = ref(false) // 用于在拖拽过程中 隐藏logo tip
- // 记录聊天框的鼠标点击处的位置,用于在下面move方法中处理拖拽边界问题上的计算
- const chatBarClickPos: Recordable = ref({})
- onMounted(() => {
- nextTick(() => {
- const aiLogoBox = document.querySelector('.open-chat') as HTMLElement
- if (isMobile.value) {
- // 防冒泡
- aiLogoBox.addEventListener('touchstart', (e) => {
- const pageX = e.touches[0].pageX
- const pageY = e.touches[0].pageY
- chatBarClickPos.value = {
- right: aiLogoBox.offsetWidth - (pageX - aiLogoBox.offsetLeft),
- bottom: aiLogoBox.offsetHeight - (pageY - aiLogoBox.offsetTop)
- }
- isDrag.value = false
- aiLogoBox.addEventListener('touchmove', mouseMove)
- })
- aiLogoBox.addEventListener('touchend', () => {
- aiLogoBox.removeEventListener('touchmove', mouseMove)
- })
- isDrag.value = false
- } else {
- aiLogoBox.addEventListener('mousedown', (e) => {
- chatBarClickPos.value = {
- right: aiLogoBox.offsetWidth - e.offsetX,
- bottom: aiLogoBox.offsetHeight - e.offsetY
- }
- isDrag.value = false
- document.addEventListener('mousemove', mouseMove)
- })
- document.addEventListener('mouseup', () => {
- document.removeEventListener('mousemove', mouseMove)
- setTimeout(() => {
- isDrag.value = false
- }, 100)
- })
- }
- })
- })
- onUnmounted(() => {
- const aiLogoBox = document.querySelector('.open-chat img') as HTMLElement
- if (isMobile.value) {
- aiLogoBox && aiLogoBox.removeEventListener('touchend', () => {})
- // document.body.removeEventListener('touchend', () => {})
- } else {
- document.removeEventListener('mouseup', () => {})
- }
- })
- const mouseMove = (e) => {
- // 可视区宽度
- const clientWidth = document.body.clientWidth
- const clientHeight = document.body.clientHeight
- const pageX = isMobile.value ? e.touches[0].pageX : e.pageX
- const pageY = isMobile.value ? e.touches[0].pageY : e.pageY
- // 拖拽过程中 隐藏tip,并点击抬起后不打开聊天框
- isDrag.value = true
- const aiLogoBox = document.querySelector('.open-chat') as HTMLElement
- // 处理边界问题
- const r = clientWidth - pageX - chatBarClickPos.value.right
- aiLogoBox.style.right = `${r < 0 ? 0 : r > clientWidth - aiLogoBox.offsetWidth ? clientWidth - aiLogoBox.offsetWidth : r}px`
- const b = clientHeight - pageY - chatBarClickPos.value.bottom
- aiLogoBox.style.bottom = `${b < 0 ? 0 : b > clientHeight - aiLogoBox.offsetHeight ? clientHeight - aiLogoBox.offsetHeight : b}px`
- }
- // 打开关闭弹窗
- const toggleChatOpen = (type) => {
- if (isDrag.value) return
- isOpen.value = type === 'open'
- }
- // 展开收起
- const handleExpand = () => {
- isExpand.value = !isExpand.value
- }
- const AiChatRef = ref()
- const handleNewChat = () => {
- AiChatRef.value.newChat()
- }
- // 发送消息
- const emit = defineEmits(['onMessage'])
- const handleSend = (message) => {
- emit('onMessage', message)
- }
- // 接受消息
- const handleResponse = (message) => {
- AiChatRef.value?.handleMessageResponse(message)
- }
- defineExpose({ handleResponse })
- </script>
- <style lang="less">
- @import './styles/variables.less';
- @import './styles/index.less';
- // 移动端下点击的高亮
- .ai-layout * {
- -webkit-tap-highlight-color: transparent;
- }
- .chat-container {
- position: fixed;
- bottom: 0;
- right: 0;
- width: 680px;
- height: 680px;
- max-height: 90%;
- z-index: 101;
- font-size: 14px;
- transform: scale(0);
- transform-origin: right bottom;
- display: flex;
- flex-direction: column;
- align-items: center;
- transition:
- width 0.5s,
- height 0.5s;
- // 移动端默认字体16px
- //.is-mobile & {
- // font-size: 16px;
- // line-height: 24px;
- //}
- .chat-bg {
- width: 100%;
- height: 100%;
- position: absolute;
- left: 0;
- top: 0;
- background-image: url('img/layout/chat-bg.jpg');
- background-color: transparent;
- background-repeat: no-repeat;
- background-size: 100% auto;
- box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 0.1);
- overflow: hidden;
- border-radius: 12px;
- }
- .chat-topbar {
- height: 52px;
- width: 100%;
- flex-shrink: 0;
- z-index: 1;
- display: flex;
- justify-content: space-between;
- align-items: center;
- &-text {
- font-size: 20px;
- font-weight: bold;
- color: #fff;
- margin-left: 100px;
- }
- &-operates {
- display: flex;
- margin-right: 10px;
- &_icon {
- margin-right: 8px;
- width: 26px;
- height: 26px;
- background-color: #fff;
- border-radius: 6px;
- box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08);
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
- color: #666;
- &.newChat {
- width: auto;
- padding: 0 8px;
- }
- }
- }
- }
- .chat-main {
- flex: 1;
- width: 100%;
- height: calc(100% - 52px);
- display: flex;
- .chat-figure {
- width: 90px;
- height: 100px;
- position: absolute;
- left: 0;
- top: -30px;
- overflow: hidden;
- text-align: center;
- img {
- width: 80%;
- }
- }
- .chat-window {
- flex: 1;
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- border-radius: 8px;
- overflow: hidden;
- .chat-content {
- position: relative;
- overflow: auto;
- width: 100%;
- flex: 1;
- }
- }
- }
- &:not(.is-expand) {
- .chat-content-ai {
- background: linear-gradient(180deg, rgba(235, 239, 248, 0.56), #ebeff8);
- border: 1px solid;
- border-image: linear-gradient(180deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0)) 1 1;
- backdrop-filter: blur(4px);
- }
- }
- &.is-expand {
- width: 90%;
- max-width: 1200px;
- height: 90%;
- max-height: 900px;
- // 展开时 样式
- //overflow: hidden;
- border-radius: 16px;
- .chat-topbar {
- &-ai {
- display: none;
- }
- &-text {
- margin-left: 30px;
- font-size: 28px;
- }
- }
- .chat-main {
- .chat-figure {
- width: 376px;
- display: flex;
- justify-content: center;
- align-items: center;
- position: static;
- height: auto;
- margin-bottom: 100px;
- z-index: 99;
- img {
- width: 100%;
- }
- }
- }
- }
- &.show {
- animation: scaleIn 0.15s ease-in-out 0s 1 normal forwards;
- }
- &.hide {
- display: none;
- }
- }
- .is-mobile .chat-container {
- bottom: 0;
- right: 0;
- width: 100%;
- height: 100%;
- max-height: 100%;
- .chat-bg {
- background-image: url('img/layout/chat-bg-h5.jpg');
- }
- .chat-topbar {
- height: 56px;
- position: absolute;
- left: 0;
- top: 12px;
- z-index: 100;
- display: flex;
- justify-content: space-between;
- .close-back {
- margin-left: 22px;
- }
- }
- .chat-main {
- position: relative;
- width: 100%;
- height: 100%;
- .chat-figure {
- width: 100%;
- display: flex;
- justify-content: center;
- align-items: flex-start;
- position: static;
- height: auto;
- img {
- width: 24%;
- transition: width 0.5s;
- margin-top: 20px;
- }
- }
- .chat-window {
- position: absolute;
- bottom: 0;
- left: 0;
- z-index: 99;
- width: 100%;
- height: calc(100% - 130px);
- transition: height 0.5s;
- border-radius: 16px 16px 0 0;
- box-shadow: 0px -3px 10px 0px rgba(0, 0, 0, 0.06);
- overflow: hidden;
- .chat-content-ai {
- border: none;
- }
- }
- }
- &.is-expand {
- .chat-figure {
- img {
- width: 64%;
- }
- }
- .chat-window {
- height: calc(100% - 354px);
- min-height: 66%;
- backdrop-filter: blur(10px);
- }
- }
- }
- .open-chat {
- position: fixed;
- width: 60px;
- right: 30px;
- bottom: 200px;
- cursor: pointer;
- z-index: 101;
- transform: scale(0);
- .is-mobile & {
- width: 60px;
- right: 10px;
- bottom: 100px;
- .open-tip {
- display: none !important;
- }
- }
- img {
- width: 100%;
- transition: all 0.5s;
- position: relative;
- z-index: 99;
- &:not(.is-drag):hover + .open-tip {
- display: block;
- animation: scaleIn 0.15s ease-in-out 0.15s 1 normal forwards;
- }
- // 拖动过程中隐藏
- &:has(.is-drag) {
- display: none;
- }
- }
- .open-tip {
- position: absolute;
- right: -4px;
- top: 22px;
- z-index: 98;
- width: 236px;
- font-size: 14px;
- font-weight: 600;
- color: #315e97;
- padding: 12px 64px 12px 16px;
- background: linear-gradient(180deg, #dbf1ff, #ffffff);
- border: 1px solid;
- border-image: linear-gradient(360deg, rgba(255, 255, 255, 0), #ffffff) 1 1;
- border-radius: 8px;
- box-shadow: 0px 4px 20px 0px rgba(76, 107, 148, 0.15);
- display: none;
- transform: scale(0);
- transform-origin: top right;
- &:hover {
- display: block;
- animation: scaleIn 0.15s ease-in-out 0.15s 1 normal forwards;
- }
- .hi {
- color: #1d80ff;
- line-height: 26px;
- & + div {
- width: 120px;
- text-align: justify;
- text-align-last: justify;
- }
- }
- }
- &.toggle-off {
- display: none;
- }
- &.toggle-on {
- animation: scaleIn 0.15s ease-in-out 0.15s 1 normal forwards;
- }
- }
- @keyframes scaleIn {
- 0% {
- transform: scale(0);
- }
- 100% {
- transform: scale(1);
- }
- }
- </style>
|