From 82210fced4310307d5a2b03470d16a8ec98331ab Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期五, 19 七月 2024 11:16:41 +0800 Subject: [PATCH] iconfont更新及聊天组件反馈功能实现 --- src/api/ai/chat.ts | 12 + customer_list/ch/static/fonts/iconfont/iconfont.css | 513 ++++++++++++++++-------------------------- customer_list/ch/static/fonts/iconfont/iconfont.ttf | 0 src/components/chat/Chat.vue | 64 +++++ src/hooks/usePageDisplay.ts | 41 ++- src/hooks/useClickOther.ts | 29 ++ src/components/chat/components/FeedbackPanel.vue | 52 ++++ customer_list/ch/static/fonts/iconfont/iconfont.woff2 | 0 customer_list/ch/static/fonts/iconfont/iconfont.woff | 0 src/components/chat/chatComponents/summaryCom/SummaryCom.vue | 4 10 files changed, 376 insertions(+), 339 deletions(-) diff --git a/customer_list/ch/static/fonts/iconfont/iconfont.css b/customer_list/ch/static/fonts/iconfont/iconfont.css index 4971d6f..902543c 100644 --- a/customer_list/ch/static/fonts/iconfont/iconfont.css +++ b/customer_list/ch/static/fonts/iconfont/iconfont.css @@ -1,416 +1,291 @@ @font-face { - font-family: 'iconfont'; /* Project id 2298093 */ - src: url('./iconfont.woff2') format('woff2'), url('./iconfont.woff') format('woff'), url('./iconfont.ttf') format('truetype'); + font-family: "ywicon"; /* Project id 4499025 */ + src: url('iconfont.woff2?t=1721295446708') format('woff2'), + url('iconfont.woff?t=1721295446708') format('woff'), + url('iconfont.ttf?t=1721295446708') format('truetype'); } -.iconfont { - font-family: 'iconfont' !important; - font-size: 16px; - font-style: normal; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +.ywicon { + font-family: "ywicon" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -.icon-quanjushezhi_o:before { - content: '\eb80'; +.icon-wentifankui:before { + content: "\e614"; } -.icon-yunshangchuan_o:before { - content: '\ebb3'; +.icon-zuoyoujiantou:before { + content: "\e673"; } -.icon-yunxiazai_o:before { - content: '\ebb4'; +.icon-zuoyoujiantou1:before { + content: "\e602"; } -.icon-shuaxin:before { - content: '\e63e'; +.icon-cedian:before { + content: "\e65a"; } -.icon-diannao1:before { - content: '\e622'; +.icon-shuzhuangtu:before { + content: "\e600"; } -.icon-barcode-qr:before { - content: '\e61e'; +.icon-tubiao-zhexiantu:before { + content: "\eb96"; } -.icon-zhongduancanshuchaxun:before { - content: '\e638'; +.icon-sandiantu:before { + content: "\e927"; } -.icon-shouye_dongtaihui:before { - content: '\e606'; +.icon-gongzuozongjie:before { + content: "\e63e"; } -.icon-putong:before { - content: '\e603'; +.icon-dizhi:before { + content: "\e610"; } -.icon-dongtai:before { - content: '\e659'; -} - -.icon-wenducanshu-05:before { - content: '\e634'; -} - -.icon-zhongduancanshu:before { - content: '\e63b'; -} - -.icon-tongzhi1:before { - content: '\e63a'; -} - -.icon-tongzhi2:before { - content: '\e649'; -} - -.icon-tongzhi3:before { - content: '\e648'; -} - -.icon-tongzhi4:before { - content: '\e60c'; +.icon-youxiang:before { + content: "\e639"; } .icon-dianhua:before { - content: '\e615'; + content: "\e65b"; } -.icon-xianshimima:before { - content: '\e63c'; +.icon-shoucangxuanzhong:before { + content: "\e6c0"; } -.icon-yincangmima:before { - content: '\e63d'; +.icon-shoucang2:before { + content: "\e6c1"; } -.icon-shuxing:before { - content: '\e67a'; +.icon-fold:before { + content: "\e6af"; } -.icon-juxingkaobei:before { - content: '\e7a5'; +.icon-bengzhan:before { + content: "\e609"; } -.icon-shuxingtu:before { - content: '\e685'; +.icon-biyan:before { + content: "\e819"; } -.icon-bolangneng:before { - content: '\e745'; +.icon-unfold:before { + content: "\e6b0"; } -.icon-bolangnengshiyanchang:before { - content: '\e746'; +.icon-bengzhan1:before { + content: "\e60b"; } -.icon--chaifenhang:before { - content: '\e6d1'; +.icon-bianji:before { + content: "\e646"; } -.icon--chaifenlie:before { - content: '\e6d0'; +.icon-tianjia:before { + content: "\e606"; } -.icon-tupianyulan:before { - content: '\e67e'; +.icon-jiaose:before { + content: "\e830"; } -.icon-15tupianyulan:before { - content: '\e624'; +.icon-shanchu:before { + content: "\e61a"; } -.icon-728bianjiqi_zitidaxiao:before { - content: '\e660'; +.icon-shanchu3:before { + content: "\e637"; } -.icon-ziti:before { - content: '\e7b1'; +.icon-el-icon-download:before { + content: "\e6b1"; } -.icon-font-size:before { - content: '\eaef'; +.icon-el-icon-magic-stick:before { + content: "\e6b2"; } -.icon-tuodong:before { - content: '\e6a8'; +.icon-download2:before { + content: "\e6b3"; } -.icon-zhongyingwen1:before { - content: '\e7a3'; +.icon-folder-opened:before { + content: "\e6b4"; } -.icon-fuhao-yingwen:before { - content: '\e714'; +.icon-el-icon-scissors:before { + content: "\e74d"; } -.icon-fuhao-zhongwen:before { - content: '\e712'; +.icon-a-zaizhiqingkuang1:before { + content: "\e6c8"; } -.icon-diqiu:before { - content: '\e689'; +.icon-zhiwei1:before { + content: "\e6c9"; } -.icon-xingqiu:before { - content: '\e65c'; +.icon-xiaoxi:before { + content: "\e65c"; } -.icon-diqiu1:before { - content: '\e631'; +.icon-changyonglogo37:before { + content: "\e71e"; } -.icon-huanjingxingqiu:before { - content: '\e617'; +.icon-windows:before { + content: "\e880"; } -.icon-zidingyibuju:before { - content: '\e637'; +.icon-pointer:before { + content: "\e7bb"; } -.icon-dayin:before { - content: '\e612'; +.icon-weixin:before { + content: "\e695"; } -.icon-step:before { - content: '\e601'; +.icon-weibiaoti517:before { + content: "\e61d"; } -.icon-30xuanzhongyuanxingfill:before { - content: '\e677'; +.icon-ai23:before { + content: "\e68a"; } -.icon-shibai:before { - content: '\e60b'; +.icon-tishi:before { + content: "\e622"; } -.icon-7_round_solid:before { - content: '\e64d'; +.icon-zanting:before { + content: "\e67d"; } -.icon-6_round_solid:before { - content: '\e64e'; +.icon-yingyongzhongxin:before { + content: "\e67f"; } -.icon-9_round_solid:before { - content: '\e64f'; +.icon-tuichu:before { + content: "\e689"; } -.icon-1_round_solid:before { - content: '\e650'; +.icon-compare:before { + content: "\e68e"; } -.icon-5_round_solid:before { - content: '\e651'; -} - -.icon-2_round_solid:before { - content: '\e654'; -} - -.icon-0_round_solid:before { - content: '\e655'; -} - -.icon-3_round_solid:before { - content: '\e656'; -} - -.icon-4_round_solid:before { - content: '\e657'; -} - -.icon-8_round_solid:before { - content: '\e658'; -} - -.icon-radio-off-full:before { - content: '\ea6b'; -} - -.icon-tongzhi:before { - content: '\e600'; -} - -.icon-ditu:before { - content: '\e8bc'; -} - -.icon-ico:before { - content: '\e646'; -} - -.icon-chazhaobiaodanliebiao:before { - content: '\e76a'; -} - -.icon-biaodan:before { - content: '\e61d'; -} - -.icon-siweidaotu:before { - content: '\e614'; -} - -.icon-jiliandongxuanzeqi:before { - content: '\e616'; -} - -.icon-caijian:before { - content: '\e611'; -} - -.icon-fuwenben:before { - content: '\e7e4'; -} - -.icon-fuwenbenkuang:before { - content: '\e66f'; -} - -.icon-shangchuan:before { - content: '\e663'; -} - -.icon-xuanzeqi:before { - content: '\e635'; -} - -.icon-fangkuang:before { - content: '\e642'; -} - -.icon-gouxuan-weixuanzhong-xianxingfangkuang:before { - content: '\e77b'; -} - -.icon-shidu:before { - content: '\e60a'; -} - -.icon-yangan:before { - content: '\e67d'; -} - -.icon-wendu:before { - content: '\e686'; -} - -.icon-zaosheng:before { - content: '\e61c'; -} - -.icon-jinridaiban:before { - content: '\e60f'; -} - -.icon-AIshiyanshi:before { - content: '\e609'; -} - -.icon-shenqingkaiban:before { - content: '\e639'; -} - -.icon-zhongyingwenqiehuan:before { - content: '\e611'; -} - -.icon-zhongyingwen:before { - content: '\e605'; -} - -.icon-zhongyingzhuanhuan:before { - content: '\e6a2'; -} - -.icon-zhongyingwenyuyan:before { - content: '\e609'; -} - -.icon-shuju:before { - content: '\e613'; -} - -.icon-ico_shuju:before { - content: '\e6ff'; -} - -.icon-shuju1:before { - content: '\e60e'; -} - -.icon-fuzhiyemian:before { - content: '\e772'; -} - -.icon-caozuo-wailian:before { - content: '\e711'; -} - -.icon-icon-:before { - content: '\e620'; -} - -.icon-gerenzhongxin:before { - content: '\e60d'; -} - -.icon-caidan:before { - content: '\e652'; -} - -.icon-xitongshezhi:before { - content: '\e69b'; -} - -.icon-neiqianshujuchucun:before { - content: '\e62f'; -} - -.icon-shouye:before { - content: '\e653'; -} - -.icon-quanxian:before { - content: '\e610'; -} - -.icon-zujian:before { - content: '\e85e'; -} - -.icon-crew_feature:before { - content: '\e602'; -} - -.icon-gongju:before { - content: '\e62d'; -} - -.icon-skin:before { - content: '\e636'; -} - -.icon-shixinyuan:before { - content: '\e669'; -} - -.icon-webicon318:before { - content: '\e6a9'; -} - -.icon-dian:before { - content: '\e608'; +.icon-gerenxinxi_o:before { + content: "\ebcc"; } .icon-fullscreen:before { - content: '\e623'; + content: "\e601"; +} + +.icon-kaiji:before { + content: "\e626"; } .icon-tuichuquanping:before { - content: '\e641'; + content: "\e64c"; } + +.icon-lanyalianjie:before { + content: "\e6c2"; +} + +.icon-select_icon:before { + content: "\e661"; +} + +.icon-pingmufangda:before { + content: "\e7e9"; +} + +.icon-daochu1:before { + content: "\e682"; +} + +.icon-kaiji2:before { + content: "\e641"; +} + +.icon-fenxiang2:before { + content: "\e62e"; +} + +.icon-guanbi:before { + content: "\e61f"; +} + +.icon-shaixuan1:before { + content: "\e6aa"; +} + +.icon-copy:before { + content: "\e6b8"; +} + +.icon-buzan:before { + content: "\e677"; +} + +.icon-dianzan:before { + content: "\ec7f"; +} + +.icon-yulan:before { + content: "\e730"; +} + +.icon-guanbiyulan:before { + content: "\e788"; +} + +.icon-sanweiditu:before { + content: "\e605"; +} + +.icon-jiankong:before { + content: "\e7bd"; +} + +.icon-jiegou:before { + content: "\e672"; +} + +.icon-tuceng1:before { + content: "\e645"; +} + +.icon-shexiangtou:before { + content: "\e620"; +} + +.icon-shuibeng:before { + content: "\e604"; +} + +.icon-24shuidifa-tint-01:before { + content: "\e656"; +} + +.icon-liuliangji-:before { + content: "\e643"; +} + +.icon-zhuangtai:before { + content: "\e6c3"; +} + +.icon-a-appround15:before { + content: "\e92f"; +} + diff --git a/customer_list/ch/static/fonts/iconfont/iconfont.ttf b/customer_list/ch/static/fonts/iconfont/iconfont.ttf index 9df435d..ae832f1 100644 --- a/customer_list/ch/static/fonts/iconfont/iconfont.ttf +++ b/customer_list/ch/static/fonts/iconfont/iconfont.ttf Binary files differ diff --git a/customer_list/ch/static/fonts/iconfont/iconfont.woff b/customer_list/ch/static/fonts/iconfont/iconfont.woff index 4a0251f..5ae5b90 100644 --- a/customer_list/ch/static/fonts/iconfont/iconfont.woff +++ b/customer_list/ch/static/fonts/iconfont/iconfont.woff Binary files differ diff --git a/customer_list/ch/static/fonts/iconfont/iconfont.woff2 b/customer_list/ch/static/fonts/iconfont/iconfont.woff2 index c3d4611..cc6436a 100644 --- a/customer_list/ch/static/fonts/iconfont/iconfont.woff2 +++ b/customer_list/ch/static/fonts/iconfont/iconfont.woff2 Binary files differ diff --git a/src/api/ai/chat.ts b/src/api/ai/chat.ts index aa7d9f8..1ca211c 100644 --- a/src/api/ai/chat.ts +++ b/src/api/ai/chat.ts @@ -297,3 +297,15 @@ }, }); }; + + +export const reportHistoryProblem = async (params, req: any = request) => { + return req({ + url: 'history/report_history_problem', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +}; \ No newline at end of file diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue index 9c14602..25f06fe 100644 --- a/src/components/chat/Chat.vue +++ b/src/components/chat/Chat.vue @@ -3,7 +3,7 @@ <div class="h-full flex flex-col items-center overflow-y-auto"> <div ref="chatListDom" class="h-full" :style="{ width: chatWidth }"> <div - class="group flex px-4 py-6 hover:bg-slate-100 rounded-lg" + class="group flex px-4 py-6 hover:bg-slate-100 rounded-lg relative" :class="{ 'flex-row-reverse': item.role === RoleEnum.user }" v-for="(item, index) of computedMessageList" :key="index" @@ -17,16 +17,16 @@ /> <div class="flex-auto flex" :class="{ 'justify-end': item.role === RoleEnum.user }"> <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }"> - <div class="relative w-full" v-if="item.content?.values"> + <div class="w-full" v-if="item.content?.values"> <div class="text-sm rounded-[6px] p-4 leading-relaxed" :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }" > <div v-if="item.content.errCode === ErrorCode.Message" class="text-red-500 w-full">{{ item.content.msg }}</div> - <component v-else :is="answerTypeMapCom[item.content.type]" :data="item.content.values" :originData="item"/> + <component v-else :is="answerTypeMapCom[item.content.type]" :data="item.content.values" :originData="item" /> </div> - <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 mr-2 mt-2 space-x-2"> + <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 mr-4 mt-2 space-x-2"> <div class="flex items-center justify-center size-[15px]" v-if="item.content?.type === AnswerType.Text || item.content?.type === AnswerType.Knowledge" @@ -45,6 +45,29 @@ :class="{ 'text-[#0284ff]': item.state === AnswerState.Unlike }" class="p-2 ywicon icon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]" @click="unLikeClick(item)" + /> + </div> + <div class="flex items-center justify-center size-[15px] relative"> + <i + class="p-2 ywicon icon-wentifankui cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]" + @click=" + ($event) => + feedbackClick( + $event, + item, + computedMessageList + .filter((v) => v.role === RoleEnum.assistant) + .findIndex((v) => v.historyId === item.historyId) + ) + " + /> + <FeedbackPanel + v-show="feedbackIsShow && currentFeedbackMapItem === item" + ref="feedbackPanelRef" + v-model:isShow="feedbackIsShow" + v-model:content="feedbackContent" + :chatItem="currentFeedbackMapItem" + :position="feedbackPosition" /> </div> </div> @@ -95,6 +118,8 @@ import { v4 as uuidv4 } from 'uuid'; import _ from 'lodash'; import { ErrorCode } from '/@/utils/request'; +import FeedbackPanel from './components/FeedbackPanel.vue'; +import { useClickOther } from '/@/hooks/useClickOther'; const chatWidth = '75%'; @@ -414,6 +439,37 @@ forbidScroll = false; }); }; +const feedbackPosition = ref({ + x: 0, + y: 0, +}); + +const feedbackIsShow = ref(false); +const feedbackContent = ref(''); +const feedbackPanelRef = ref<HTMLDivElement>(null); +const currentFeedbackMapItem = ref(null); +const curFeedbackIndex = ref(0); +const feedbackClick = async (e, item, index) => { + currentFeedbackMapItem.value = item; + curFeedbackIndex.value = index; + const offsetX = -4; + const offsetY = -8; + feedbackIsShow.value = true; + nextTick(() => { + feedbackPosition.value = { + x: -feedbackPanelRef.value[index].$el.clientWidth + offsetX, + y: -feedbackPanelRef.value[index].$el.clientHeight + offsetY, + }; + }); +}; +useClickOther( + computed(() => feedbackPanelRef.value[curFeedbackIndex.value]), + feedbackIsShow, + () => { + feedbackIsShow.value = false; + feedbackContent.value = ''; + } +); //#endregion </script> diff --git a/src/components/chat/chatComponents/summaryCom/SummaryCom.vue b/src/components/chat/chatComponents/summaryCom/SummaryCom.vue index 490bdbd..8424ecc 100644 --- a/src/components/chat/chatComponents/summaryCom/SummaryCom.vue +++ b/src/components/chat/chatComponents/summaryCom/SummaryCom.vue @@ -3,8 +3,8 @@ <template v-if="data && data.length > 0"> <template v-if="summaryList && summaryList.length > 0"> <div class="w-full" v-for="(item, idx) in summaryList" :key="idx"> - <h3>{{ item.title }}</h3> - <el-table ref="tableRefList" class="w-full" :data="[{}]"> + <h2>{{ item.title }}</h2> + <el-table ref="tableRefList" class="w-full mt-5" :data="[{}]"> <el-table-column v-for="(col, index) in item.values" :label="col.title" :key="index"> <template #default="scope"> {{ col?.value }} diff --git a/src/components/chat/components/FeedbackPanel.vue b/src/components/chat/components/FeedbackPanel.vue new file mode 100644 index 0000000..12a2b44 --- /dev/null +++ b/src/components/chat/components/FeedbackPanel.vue @@ -0,0 +1,52 @@ +<template> + <div + class="w-64 bg-white absolute p-4 rounded-md border-[1.5px] border-solid border-gray-300 z-10" + :style="{ left: position.x + 'px', top: position.y + 'px' }" + > + <div class="flex justify-between"> + <h3>浣犵殑鍙嶉灏�<br />甯姪 WI 姘村姟浼樺寲杩涙</h3> + <i class="ywicon icon-guanbi right-0 top-0 !text-[20px] text-gray-500 cursor-pointer" @click="closeClick"></i> + </div> + <div class="mt-6 flex-col flex"> + <el-input v-model="content" resize="none" class="" type="textarea" :rows="3"></el-input> + <el-button :disabled="!content" color="#1d86ff" class="m-auto mt-3" type="primary" @click="submitFeedback">鎻� 浜�</el-button> + </div> + </div> +</template> + +<script setup lang="ts"> +import { ref, type PropType, nextTick } from 'vue'; +import { reportHistoryProblem } from '/@/api/ai/chat'; +import { ElMessage } from 'element-plus'; + +const props = defineProps({ + position: { + type: Object as PropType<any>, + }, + chatItem: { + type: Object as PropType<any>, + }, +}); +const isShow = defineModel('isShow', { + type: Boolean, +}); +const content = defineModel('content', { + type: String, +}); +const submitFeedback = async () => { + const res = await reportHistoryProblem({ + history_id: props.chatItem.historyId, + report_note: content.value, + }); + ElMessage.success('鎰熻阿鎮ㄧ殑鍙嶉'); + nextTick(() => { + closeClick(); + }); +}; + +const closeClick = () => { + isShow.value = false; + content.value = ''; +}; +</script> +<style scoped lang="scss"></style> diff --git a/src/hooks/useClickOther.ts b/src/hooks/useClickOther.ts new file mode 100644 index 0000000..0586ea1 --- /dev/null +++ b/src/hooks/useClickOther.ts @@ -0,0 +1,29 @@ +import { computed, watch, type Ref, watchEffect, nextTick } from 'vue'; + +export const useClickOther = ( + excludeEle: Ref<HTMLElement>[] | Ref<HTMLElement>, + isShow: Ref<boolean>, + handleClickOther: () => void = () => { + isShow.value = false; + } +) => { + const domList = computed(() => { + const computedEle = Array.isArray(excludeEle) ? excludeEle : [excludeEle]; + return computedEle.map((item) => item.value.$el); + }); + const listenClickOtherExit = (e) => { + if (!domList.value.includes(e.target) && domList.value.every((item) => !item.contains(e.target))) { + handleClickOther(); + } + }; + + watch(isShow, (val) => { + if (val) { + setTimeout(() => { + document.addEventListener('click', listenClickOtherExit); + }, 0); + } else { + document.removeEventListener('click', listenClickOtherExit); + } + }); +}; diff --git a/src/hooks/usePageDisplay.ts b/src/hooks/usePageDisplay.ts index e9cb5f1..78d866d 100644 --- a/src/hooks/usePageDisplay.ts +++ b/src/hooks/usePageDisplay.ts @@ -1,23 +1,36 @@ -import { onActivated, onDeactivated, ref } from 'vue'; +import { onActivated, onBeforeUnmount, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; +import router from '../router'; /** * 寮�鍚矾鐢辩紦瀛橀〉闈紝绂诲紑鏃讹紝鍜岃繘鍏ユ椂鍙栨秷/寮�鍚闃呬簨浠� * @returns */ -export const usePageDisplay = (pageShow?: () => void, pageHide?: () => void) => { - const haveExecutedMounted = ref(false); - onActivated(() => { - if (!haveExecutedMounted.value) { - return; - } - pageShow?.(); - }); +export const usePageDisplay = (pageShow?: () => void, pageHide?: () => void, needExecutedMounted = true) => { + const haveExecutedMounted = ref(!needExecutedMounted); + const currentRoute = router.currentRoute.value; - onDeactivated(() => { - pageHide?.(); - }); + const isKeepAlive = currentRoute.meta.isKeepAlive ?? false; + if (isKeepAlive) { + onActivated(() => { + if (!haveExecutedMounted.value) { + return; + } + pageShow?.(); + }); + + onDeactivated(() => { + pageHide?.(); + }); + } else { + onMounted(() => { + pageShow?.(); + }); + onBeforeUnmount(() => { + pageHide?.(); + }); + } return { - haveExecutedMounted - }; + haveExecutedMounted, + }; }; -- Gitblit v1.9.3