|
@@ -1,5 +1,5 @@
|
|
|
<script setup>
|
|
|
-import { ref, unref, onMounted, computed, watch } from 'vue';
|
|
|
+import { ref, unref, onMounted, onUnmounted, computed, watch } from 'vue';
|
|
|
import { useMessage, NInput, NSwitch, NPopover, NScrollbar } from 'naive-ui';
|
|
|
import SvgIcon from '@/components/SvgIcon';
|
|
|
|
|
@@ -27,6 +27,9 @@ const isOpen = ref(false);
|
|
|
const selectedOption = ref(null);
|
|
|
const highlightedIndex = ref(0);
|
|
|
|
|
|
+const popoverTriggerRef = ref(null);
|
|
|
+const popoverInnerRef = ref(null);
|
|
|
+
|
|
|
const agentOptions = computed(() => props.options.filter(({ tools }) => tools));
|
|
|
|
|
|
const focusInput = _ => isFocusState.value = true;
|
|
@@ -65,32 +68,35 @@ const commonEmitEvent = (eventName) => {
|
|
|
return message.warning('当前对话进行中');
|
|
|
}
|
|
|
|
|
|
- emit(eventName, val);
|
|
|
+ // emit(eventName, val);
|
|
|
+ emit(eventName, {question: val, selectedOption: selectedOption.value || {}});
|
|
|
|
|
|
inpVal.value = '';
|
|
|
}
|
|
|
|
|
|
+// 回车事件
|
|
|
const handleInpEnter = (event) => {
|
|
|
- console.log( !unref(isOpen) );
|
|
|
- if (event.key === 'Enter' && !event.shiftKey) {
|
|
|
+ if (event.key === 'Enter' && !event.shiftKey && inpVal.value) {
|
|
|
event.preventDefault();
|
|
|
- // commonEmitEvent('onEnter');
|
|
|
+ commonEmitEvent('onEnter');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 点击事件
|
|
|
const handleBtnClick = () => {
|
|
|
- commonEmitEvent("onClick")
|
|
|
+ commonEmitEvent("onClick");
|
|
|
}
|
|
|
|
|
|
const clearInpVal = () => {
|
|
|
inpVal.value = '';
|
|
|
}
|
|
|
|
|
|
-// 键盘上下事件
|
|
|
+// 键盘事件
|
|
|
const handleKeyDown = (event) => {
|
|
|
-
|
|
|
const len = unref(agentOptions).length;
|
|
|
|
|
|
+ if ( !isOpen.value ) return;
|
|
|
+
|
|
|
switch (event.key) {
|
|
|
case 'ArrowUp':
|
|
|
event.preventDefault();
|
|
@@ -101,6 +107,7 @@ const handleKeyDown = (event) => {
|
|
|
highlightedIndex.value = (unref(highlightedIndex) + 1) % len;
|
|
|
break;
|
|
|
case 'Enter':
|
|
|
+ event.preventDefault();
|
|
|
selectOption(unref(highlightedIndex));
|
|
|
break;
|
|
|
default:
|
|
@@ -108,15 +115,30 @@ const handleKeyDown = (event) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 处理点击空白处关闭
|
|
|
+const closePopoverOutside =(event) => {
|
|
|
+ if (!isOpen.value) return;
|
|
|
+ const triggerResult = popoverTriggerRef.value.contains(event.target);
|
|
|
+ const innerResult = popoverInnerRef.value.contains(event.target);
|
|
|
+ isOpen.value = triggerResult || innerResult;
|
|
|
+}
|
|
|
+
|
|
|
+// 选中选项
|
|
|
const selectOption = (index) => {
|
|
|
selectedOption.value = agentOptions.value[index];
|
|
|
- isOpen.value = false;
|
|
|
highlightedIndex.value = index;
|
|
|
+ isOpen.value = false;
|
|
|
clearInpVal();
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
+ document.addEventListener('click', closePopoverOutside);
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ document.removeEventListener('keydown', handleKeyDown);
|
|
|
+ document.removeEventListener('click', closePopoverOutside);
|
|
|
})
|
|
|
|
|
|
defineExpose({
|
|
@@ -130,21 +152,27 @@ defineExpose({
|
|
|
<NPopover
|
|
|
trigger="hover"
|
|
|
width="trigger"
|
|
|
+ display-directive="show"
|
|
|
content-style="padding: 0;"
|
|
|
:show-arrow="false"
|
|
|
:show="isOpen"
|
|
|
>
|
|
|
<template #trigger>
|
|
|
- <div>
|
|
|
+ <div class="popover-trigger" ref="popoverTriggerRef">
|
|
|
<div class="chat-inp-outer border-[1px]" :class="[{ 'border-[#2454FF]': isFocusState }]">
|
|
|
- <div class="helper-tools py-[10px] px-[10px] bg-[#fcfcfc] space-x-[10px]" v-show="selectedOption">
|
|
|
- <span>与</span>
|
|
|
- <p class="agent-name space-x-[5px]" @click="isOpen = true">
|
|
|
- <img src="https://static.fuxicarbon.com/userupload/db77ffe0cef843278a23b0d2db9505fa.png" alt="">
|
|
|
- <span>{{ selectedOption?.title }}</span>
|
|
|
- </p>
|
|
|
- <span>对话中</span>
|
|
|
- </div>
|
|
|
+ <ul class="chat-tools-inner py-[10px] px-[10px] bg-[#fcfcfc]" v-show="selectedOption">
|
|
|
+ <li class="tools-tips space-x-[10px]">
|
|
|
+ <span>与</span>
|
|
|
+ <p class="agent-name space-x-[5px]" @click="isOpen = true">
|
|
|
+ <img src="https://static.fuxicarbon.com/userupload/db77ffe0cef843278a23b0d2db9505fa.png" alt="">
|
|
|
+ <span>{{ selectedOption?.title }}</span>
|
|
|
+ </p>
|
|
|
+ <span>对话中</span>
|
|
|
+ </li>
|
|
|
+ <li class="tools-close" @click="selectedOption = null">
|
|
|
+ <SvgIcon name="chat-icon-close-btn"></SvgIcon>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
<div class="chat-inp-inner">
|
|
|
<div class="inp-wrapper flex-1" @click="handleInpFocus">
|
|
|
<NInput
|
|
@@ -152,7 +180,7 @@ defineExpose({
|
|
|
ref="inpRef"
|
|
|
type="textarea"
|
|
|
size="medium"
|
|
|
- placeholder="输入您的问题或需求,Enter发送,Shift+Enter换行"
|
|
|
+ placeholder="输入@,召唤智能体"
|
|
|
v-model:value="inpVal"
|
|
|
:autosize="{ minRows: 1, maxRows: 5 }"
|
|
|
@focus="focusInput"
|
|
@@ -178,56 +206,88 @@ defineExpose({
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
- <div class="popover-inner">
|
|
|
+ <div class="popover-inner" ref="popoverInnerRef">
|
|
|
<div class="header">
|
|
|
<span>选择智能体</span>
|
|
|
- <p class="close" @click="isOpen = false">
|
|
|
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
- <path d="M3.14905 3.14777L12.8508 12.8496" stroke="#838A95" stroke-linecap="round"></path>
|
|
|
- <path d="M13.0016 2.99786L2.99725 13.0022" stroke="#838A95" stroke-linecap="round"></path>
|
|
|
- <g opacity="0.01" style="mix-blend-mode: darken;">
|
|
|
- <rect width="16" height="16" fill="white"></rect>
|
|
|
- </g>
|
|
|
- </svg>
|
|
|
+ <p class="tools-close" @click="isOpen = false">
|
|
|
+ <SvgIcon name="chat-icon-close-btn"></SvgIcon>
|
|
|
</p>
|
|
|
</div>
|
|
|
- <div class="content">
|
|
|
- <NScrollbar style="max-height: 240px;">
|
|
|
- <div class="item" v-for="item, index in agentOptions" :class="['item', { active: highlightedIndex === index }]" @click="selectOption(index)">
|
|
|
- <p class="icon">
|
|
|
- <img :src="item.banner" alt="">
|
|
|
- </p>
|
|
|
- <p class="ml-[10px] space-x-[5px] text">
|
|
|
- <span class="text-[15px]">{{item.title}}</span>
|
|
|
- <!-- <span class="text-[#888] text-[14px]">/span> -->
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </NScrollbar>
|
|
|
- </div>
|
|
|
+ <NScrollbar style="max-height: 240px;">
|
|
|
+ <div class="item" v-for="item, index in agentOptions" :class="['item', { active: highlightedIndex === index }]" @click="selectOption(index)">
|
|
|
+ <p class="icon">
|
|
|
+ <img :src="item.banner" alt="">
|
|
|
+ </p>
|
|
|
+ <p class="ml-[10px] space-x-[5px] text">
|
|
|
+ <span class="text-[15px]">{{item.title}}</span>
|
|
|
+ <span class="text-[#888] text-[14px]">这里可以补充个描述</span>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </NScrollbar>
|
|
|
</div>
|
|
|
</NPopover>
|
|
|
</template>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
+.chat-inp-outer {
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0px 3px 12px 0px #97D3FF40;
|
|
|
+
|
|
|
+ .chat-tools-inner {
|
|
|
+ @include flex(x, center, between);
|
|
|
+
|
|
|
+ .tools-tips {
|
|
|
+ @include flex(x, center, start);
|
|
|
+ color: #666;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ .agent-name {
|
|
|
+ @include flex(x, center, start);
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .chat-inp-inner {
|
|
|
+ position: relative;
|
|
|
+ @include flex(x, center, between);
|
|
|
+ background: #fff;
|
|
|
+
|
|
|
+ .inp-wrapper {
|
|
|
+ padding: 17px 0px 17px 34px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .submit-btn {
|
|
|
+ @include flex(x, center, center);
|
|
|
+ width: 84px;
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ @include flex(x, center, center);
|
|
|
+ width: 50px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 32px;
|
|
|
+ transition: all .3s;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
.popover-inner {
|
|
|
.header {
|
|
|
@include flex(x, center, between);
|
|
|
padding-bottom: 8px;
|
|
|
font-size: 14px;
|
|
|
color: #666;
|
|
|
- .close {
|
|
|
- @include flex(x, center, center);
|
|
|
- width: 28px;
|
|
|
- height: 28px;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- cursor: pointer;
|
|
|
- &:hover {
|
|
|
- background: #e9eef8;
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
- .content {
|
|
|
+
|
|
|
.item {
|
|
|
@include flex(x, center, start);
|
|
|
padding: 8px 10px;
|
|
@@ -257,51 +317,16 @@ defineExpose({
|
|
|
background: #f0fafe;
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-.chat-inp-outer {
|
|
|
- border-radius: 8px;
|
|
|
- overflow: hidden;
|
|
|
- box-shadow: 0px 3px 12px 0px #97D3FF40;
|
|
|
|
|
|
- .helper-tools {
|
|
|
- @include flex(x, center, start);
|
|
|
- color: #666;
|
|
|
- font-size: 14px;
|
|
|
-
|
|
|
- .agent-name {
|
|
|
- @include flex(x, center, start);
|
|
|
- font-weight: bold;
|
|
|
- color: #333;
|
|
|
- cursor: pointer;
|
|
|
-
|
|
|
- img {
|
|
|
- width: 14px;
|
|
|
- height: 14px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-.chat-inp-inner {
|
|
|
- position: relative;
|
|
|
- @include flex(x, center, between);
|
|
|
+.tools-close {
|
|
|
+ @include flex(x, center, center);
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ border-radius: 6px;
|
|
|
background: #fff;
|
|
|
-
|
|
|
- .inp-wrapper {
|
|
|
- padding: 17px 0px 17px 34px;
|
|
|
- }
|
|
|
-
|
|
|
- .submit-btn {
|
|
|
- @include flex(x, center, center);
|
|
|
- width: 84px;
|
|
|
-
|
|
|
- .btn {
|
|
|
- @include flex(x, center, center);
|
|
|
- width: 50px;
|
|
|
- height: 32px;
|
|
|
- border-radius: 32px;
|
|
|
- transition: all .3s;
|
|
|
- }
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background: #e9eef8;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -313,4 +338,6 @@ defineExpose({
|
|
|
height: 30px;
|
|
|
background: linear-gradient(180deg, rgba(232, 241, 250, 0) 0%, #E7F0FA 95%);
|
|
|
}
|
|
|
+
|
|
|
+
|
|
|
</style>
|