yangyin
2024-12-02 269b41f9de3e34f06e123341c8f1fb7d9ef4b87f
src/layout/component/sidebar/SidebarOther.vue
@@ -12,7 +12,7 @@
               </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>
@@ -32,6 +32,7 @@
                  <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>
@@ -40,18 +41,122 @@
                  <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('');
@@ -59,12 +164,97 @@
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 ====================== 显示/退出登录 ======================
//登录
@@ -83,19 +273,76 @@
   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();
@@ -103,7 +350,7 @@
   emitter.on('logout', () => {
      logoutClick();
   });
   document.addEventListener('click', listenClickOtherExit);
   initQrcode();
});
</script>
@@ -168,12 +415,13 @@
                  line-height: 0;
                  text-align: center;
                  text-transform: none;
                  height: 40px;
                  text-rendering: optimizeLegibility;
                  -webkit-font-smoothing: antialiased;
               }
            }
         }
         .nav__profile {
            width: 40px;
            height: 40px;
@@ -223,7 +471,70 @@
         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>