| | |
| | | </div> |
| | | </el-tooltip> |
| | | <el-tooltip content="历史会话" placement="right"> |
| | | <div class="nav__chat"> |
| | | <div class="nav__chat" @click="historyChatRoomClick()"> |
| | | <div class="nav__chat-icon"> |
| | | <span class="chat_img ywifont ywicon-cshy-shizhong !text-[28px] text-[#fff]"></span> |
| | | </div> |
| | |
| | | <span class="use_name">{{ firstUserCharacter }}</span> |
| | | </div> |
| | | <div class="isShow_Profile" v-show="isShowExitLogin"> |
| | | <div class="exit" @click="feedbackClick"><i class="ywifont ywicon-youxiang"></i> 用户反馈</div> |
| | | <div class="exit" @click="logoutClick"><i class="ywifont ywicon-tuichu"></i> 退出登录</div> |
| | | </div> |
| | | </div> |
| | |
| | | <span class="use_name">登</span> |
| | | </div> |
| | | </div> |
| | | <div class="agent_line mt-4"></div> |
| | | |
| | | <el-popover placement="right-start" :width="136" trigger="hover"> |
| | | <template #reference> |
| | | <div class="nav__chat"> |
| | | <div class="nav__chat-icon"> |
| | | <span class="chat_img ywifont ywicon-shouji !text-[20px] text-[#fff]"></span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <template #default> |
| | | <div class="relative"> |
| | | <div class="p-0 box-shadow-none rounded-[10px]"> |
| | | <p class="text-center text-[#5e6772] m-0 p-0 leading-5 text-[12px]">请使用手机浏览器扫描二维码</p> |
| | | <p class="text-center text-[red] m-0 p-0 leading-5 text-[12px]">(不支持微信扫描)</p> |
| | | <div ref="setPhoneQRCode" class="mt-[8px] mb-[8px]"></div> |
| | | <p class="text-center text-[#5e6772] m-0 p-0 leading-5"> |
| | | 扫码下载 <br /> |
| | | WI 水务智能助手 |
| | | </p> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-popover> |
| | | </div> |
| | | </div> |
| | | <div class="nav_history_list bg-[#1c1e1d]" v-show="isShowHistoryChatRoom" ref="historyChatRoomRef"> |
| | | <div class="flex flex-col flex-auto w-[210.98px] rounded-t-lg box-border relative opacity-100 overflow-y-auto h100 p-[12px]"> |
| | | <div class="group flex-0 relative w100 h-[34px] bg-[#2b2c30]"> |
| | | <el-input clearable v-model="queryParams.title" placeholder="搜索" class="set-input"> |
| | | <template #prefix> |
| | | <el-icon><search /></el-icon> |
| | | </template> |
| | | </el-input> |
| | | <div |
| | | class="absolute hidden top-[100%] w-[84px] z-[1001] left-0 group-hover:block overflow-hidden rounded-md text-gray-500 bg-[#fff] py-1.5" |
| | | > |
| | | <div |
| | | class="w100 relative hover:bg-[#e6f1ff]" |
| | | v-for="item in Object.keys(dateFilterMap)" |
| | | :key="item" |
| | | @click="filterDateClick(Number(item))" |
| | | > |
| | | <div |
| | | class="size-2 absolute left-2 rounded-full bg-[#2a82e4]" |
| | | :style="{ display: item === activeDateFilter + '' ? 'block' : 'none' }" |
| | | style="top: calc(50% - 0.25rem)" |
| | | ></div> |
| | | <div class="w100 relative h-[28px] flex items-center justify-center cursor-pointer">{{ dateFilterMap[item] }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex-auto text-[#ccc] flex flex-col items-center mt-6 overflow-y-auto" ref="chatRoomRef"> |
| | | <div |
| | | :class="{ 'bg-[#41424a]': item.id === activeRoomId }" |
| | | class="group flex items-center w-full h-10 rounded-md cursor-pointer px-2 py-2 flex-0" |
| | | v-for="(item, index) in queryData" |
| | | :key="index" |
| | | @click="roomClick(item)" |
| | | > |
| | | <div class="ywifont ywicon-xiaoxi1 flex-0 mr-2.5"></div> |
| | | <div class="flex-auto text-ellipsis text-nowrap group-hover:text-[#0084ff]">{{ item.title }}</div> |
| | | <div class="text-gray-100 flex items-center space-x-2 ml-1"> |
| | | <div class="ywifont invisible ywicon-bianji group-hover:visible !" @click="editChat(item)"></div> |
| | | <el-popconfirm title="确定删除聊天记录?" @confirm.stop="confirmDeleteChatRoom(item)" width="180"> |
| | | <template #reference> |
| | | <div class="ywifont invisible ywicon-shanchu3 group-hover:visible"></div> |
| | | </template> |
| | | </el-popconfirm> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="expand-sidebar" @click="toggleSidebar" v-if="!isSharePage"> |
| | | <i class="text-[#fff] transition-all ywifont ywicon-zuoyoujiantou1"></i> |
| | | </div> |
| | | <el-dialog v-model="userFeedbackVisible" title="用户反馈" width="500" :before-close="handleCloseFeedback"> |
| | | <el-input v-model="userFeedbackText" :rows="8" type="textarea" placeholder="欢迎说说你的想法" /> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleCloseFeedback">取消</el-button> |
| | | <el-button type="primary" @click="confirmFeedback" :disabled="is_input_title"> 确 定 </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref, watchEffect } from 'vue'; |
| | | import { isLoginStatus, isSharePage, isShowLogin, newChatRoomClick } from '/@/stores/chatRoom'; |
| | | import { onClickOutside } from '@vueuse/core'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import moment from 'moment'; |
| | | import QRCode from 'qrcodejs2-fixes'; |
| | | import { computed, nextTick, onMounted, ref, watch, watchEffect } from 'vue'; |
| | | import type { ChatRoomItem } from './components/types'; |
| | | import { DeleteHistoryGroups, setHistoryGroupTitle } from '/@/api/ai/chat'; |
| | | import { SERVE_URL } from '/@/constants'; |
| | | import { useSearch } from '/@/hooks/useSearch'; |
| | | import { DateFilter, dateFilterMap } from '/@/model/types/date'; |
| | | import { |
| | | activeRoomId, |
| | | chatRoomList, |
| | | gotoAnswerPage, |
| | | isLoginStatus, |
| | | isSharePage, |
| | | isShowLogin, |
| | | newChatRoomClick, |
| | | selectFirstRoom, |
| | | } from '/@/stores/chatRoom'; |
| | | import emitter from '/@/utils/mitt'; |
| | | import { accessSessionKey, userNameKey } from '/@/utils/request'; |
| | | import { Local, LoginInfo } from '/@/utils/storage'; |
| | | import { gotoRoute } from '/@/utils/route'; |
| | | import { Local, LoginInfo } from '/@/utils/storage'; |
| | | import { debounce, getRecentDateRange } from '/@/utils/util'; |
| | | const emit = defineEmits(['toggleSidebar']); |
| | | const prop = defineProps(['isShow']); |
| | | const userName = ref(''); |
| | |
| | | const isShowExitLogin = ref(false); |
| | | isLoginStatus.value = !!Local.get(accessSessionKey); |
| | | const toggleSidebar = () => { |
| | | emit('toggleSidebar', false); |
| | | emit('toggleSidebar', true); |
| | | }; |
| | | //#region ====================== 公司信息 ====================== |
| | | const companyClick = () => { |
| | | gotoRoute({ name: "AboutUs" }); |
| | | //#region ====================== 历史会话 =================== |
| | | const isShowHistoryChatRoom = ref(false); |
| | | const historyChatRoomRef = ref(null); |
| | | const chatRoomRef = ref<HTMLDivElement>(null); |
| | | const queryParams = ref({ |
| | | title: '', |
| | | }); |
| | | const historyChatRoomClick = () => { |
| | | isShowHistoryChatRoom.value = true; |
| | | }; |
| | | const roomClick = (room: ChatRoomItem) => { |
| | | activeRoomId.value = room.id; |
| | | gotoAnswerPage(room); |
| | | }; |
| | | const confirmDeleteChatRoom = async (room: ChatRoomItem) => { |
| | | const res = await DeleteHistoryGroups({ |
| | | history_group_id: room.id, |
| | | }); |
| | | |
| | | const foundIndex = chatRoomList.value.findIndex((item) => item === room); |
| | | chatRoomList.value.splice(foundIndex, 1); |
| | | if (chatRoomList.value.length === 0) { |
| | | newChatRoomClick(); |
| | | return; |
| | | } |
| | | roomClick(chatRoomList.value[0]); |
| | | chatRoomRef.value.firstElementChild?.scrollIntoView(); |
| | | }; |
| | | const editChat = (room: ChatRoomItem) => { |
| | | ElMessageBox.prompt('', '重命名', { |
| | | confirmButtonText: '确认', |
| | | cancelButtonText: '取消', |
| | | inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/, |
| | | inputValue: room.title, |
| | | inputErrorMessage: '请输入新的名称', |
| | | }) |
| | | .then(async (edit) => { |
| | | const res = await setHistoryGroupTitle({ |
| | | history_group_id: room.id, |
| | | title: edit.value, |
| | | }); |
| | | if (res.json_ok) { |
| | | const foundIndex = chatRoomList.value.findIndex((item) => item.id === activeRoomId.value); |
| | | chatRoomList.value[foundIndex].title = edit.value; |
| | | chatRoomRef.value.firstElementChild?.scrollIntoView(); |
| | | } |
| | | }) |
| | | .catch(({ value }) => { |
| | | ElMessage({ |
| | | type: 'info', |
| | | message: '取消修改', |
| | | }); |
| | | }); |
| | | }; |
| | | onClickOutside( |
| | | historyChatRoomRef, |
| | | () => { |
| | | isShowHistoryChatRoom.value = false; |
| | | }, |
| | | { |
| | | ignore: ['.el-message-box', '.el-popper'], |
| | | } |
| | | ); |
| | | //#endregion |
| | | //#region ====================== 公司信息 ====================== |
| | | const companyClick = () => { |
| | | gotoRoute({ name: 'AboutUs' }); |
| | | }; |
| | | //#endregion |
| | | //#region ====================== 用户反馈 ====================== |
| | | const userFeedbackVisible = ref(false); |
| | | const userFeedbackText = ref(''); |
| | | const is_input_title = computed(() => { |
| | | return userFeedbackText.value == '' ? true : false; |
| | | }); |
| | | const feedbackClick = () => { |
| | | userFeedbackVisible.value = true; |
| | | }; |
| | | const handleCloseFeedback = () => { |
| | | userFeedbackVisible.value = false; |
| | | }; |
| | | const confirmFeedback = () => { |
| | | const data = { |
| | | content: userFeedbackText.value, |
| | | contact: '用户反馈', |
| | | }; //TODO 发送用户反馈 |
| | | handleCloseFeedback(); |
| | | }; |
| | | |
| | | //#endregion |
| | | //#region ====================== 显示/退出登录 ====================== |
| | | //登录 |
| | |
| | | isLoginStatus.value = false; |
| | | LoginInfo.remove(); |
| | | }; |
| | | |
| | | const toggleExitLoginBtnRef = ref<HTMLDivElement>(null); |
| | | |
| | | const listenClickOtherExit = (e) => { |
| | | if (toggleExitLoginBtnRef.value !== e.target && !toggleExitLoginBtnRef.value?.contains(e.target)) { |
| | | isShowExitLogin.value = false; |
| | | } |
| | | onClickOutside(toggleExitLoginBtnRef, () => { |
| | | isShowExitLogin.value = false; |
| | | }); |
| | | //#endregion |
| | | //#region ====================== 日期筛选 ====================== |
| | | const activeDateFilter = ref<DateFilter>(DateFilter.All); |
| | | const filterDateClick = (dateFilter: DateFilter) => { |
| | | activeDateFilter.value = dateFilter; |
| | | }; |
| | | const filteredChatRoomList = computed(() => { |
| | | if (activeDateFilter.value === DateFilter.All) return chatRoomList.value; |
| | | else { |
| | | let dayCount = null; |
| | | switch (activeDateFilter.value) { |
| | | case DateFilter.AWeek: |
| | | dayCount = 7; |
| | | break; |
| | | |
| | | case DateFilter.AMonth: |
| | | dayCount = 30; |
| | | |
| | | break; |
| | | case DateFilter.ThreeMonth: |
| | | dayCount = 90; |
| | | break; |
| | | } |
| | | const [startDay, endDay] = getRecentDateRange(dayCount); |
| | | const data = chatRoomList.value.filter((item) => moment(item.createTime).isBetween(startDay, endDay)); |
| | | return data; |
| | | } |
| | | }); |
| | | //#endregion |
| | | //#region ====================== 扫码手机端下载 ====================== |
| | | const setPhoneQRCode = ref<HTMLElement | null>(null); |
| | | // 初始化生成二维码 |
| | | const initQrcode = () => { |
| | | let currentTime = new Date().getTime(); |
| | | const url = `${SERVE_URL}ai_html/views/mobileDownload/index.html?v=${currentTime}`; |
| | | nextTick(() => { |
| | | (<HTMLElement>setPhoneQRCode.value).innerHTML = ''; |
| | | new QRCode(setPhoneQRCode.value, { |
| | | text: url, |
| | | width: 126, |
| | | height: 126, |
| | | colorDark: '#000000', |
| | | colorLight: '#ffffff', |
| | | }); |
| | | }); |
| | | }; |
| | | //#endregion |
| | | //#region ====================== 搜索聊天室 ====================== |
| | | const { query, queryData } = useSearch(filteredChatRoomList, queryParams); |
| | | const debounceQuery = debounce(query); |
| | | watch( |
| | | () => queryParams.value.title, |
| | | (val) => { |
| | | debounceQuery(); |
| | | } |
| | | ); |
| | | //#endregion |
| | | watchEffect(() => { |
| | | if (!isLoginStatus.value) return; |
| | | userName.value = Local.get(userNameKey); |
| | | }); |
| | | onMounted(() => { |
| | | onMounted(async () => { |
| | | selectFirstRoom(); |
| | | |
| | | emitter.on('openLoginDlg', () => { |
| | | if (isShowLogin.value || isLoginStatus.value) return; |
| | | openLoginDlg(); |
| | |
| | | emitter.on('logout', () => { |
| | | logoutClick(); |
| | | }); |
| | | document.addEventListener('click', listenClickOtherExit); |
| | | initQrcode(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | line-height: 0; |
| | | text-align: center; |
| | | text-transform: none; |
| | | |
| | | height: 40px; |
| | | text-rendering: optimizeLegibility; |
| | | -webkit-font-smoothing: antialiased; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .nav__profile { |
| | | width: 40px; |
| | | height: 40px; |
| | |
| | | padding: 16px 12px; |
| | | gap: 8px; |
| | | cursor: pointer; |
| | | &:hover { |
| | | background-color: rgba(0, 0, 0, 0.04); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | .nav_history_list { |
| | | overflow: hidden; |
| | | border-left: 1px solid rgba(237, 239, 245, 0.45); |
| | | width: 211px; |
| | | height: calc(100% - 42px); |
| | | position: absolute; |
| | | right: 0px; |
| | | left: 65px; |
| | | z-index: 100; |
| | | top: 42px; |
| | | opacity: 1; |
| | | transition: opacity 0.2s; |
| | | } |
| | | .set-input { |
| | | :deep(.el-input__wrapper) { |
| | | width: 100%; |
| | | height: 100%; |
| | | font-size: 12px; |
| | | font-weight: 400; |
| | | letter-spacing: 0; |
| | | color: #e5e5e5; |
| | | border-radius: 6px; |
| | | border: 1px solid transparent; |
| | | box-sizing: border-box; |
| | | line-height: 34px; |
| | | // padding-left: 31px; |
| | | padding-right: 10px; |
| | | background-color: transparent; |
| | | cursor: pointer; |
| | | transition: color 0.2s ease-in-out; |
| | | box-shadow: unset; |
| | | } |
| | | :deep(.el-input__inner) { |
| | | &::placeholder { |
| | | color: white; |
| | | } |
| | | color: white; |
| | | } |
| | | } |
| | | ::-webkit-scrollbar { |
| | | height: 0; |
| | | width: 0; |
| | | color: transparent; |
| | | } |
| | | .expand-sidebar { |
| | | width: 20px; |
| | | height: 48px; |
| | | background: rgba(0, 0, 0, 0.2); |
| | | position: absolute; |
| | | top: 50%; |
| | | right: 0px; |
| | | transform: translate(100%, -50%); |
| | | z-index: 9; |
| | | cursor: pointer; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 0 3px; |
| | | border-radius: 0px 5px 5px; |
| | | } |
| | | </style> |