Merge branch 'main' of https://github.com/HeJiaYue520/ruoyi-element-ai
This commit is contained in:
commit
c199d991a9
@ -5,6 +5,8 @@
|
|||||||
"ComputedRef": true,
|
"ComputedRef": true,
|
||||||
"DirectiveBinding": true,
|
"DirectiveBinding": true,
|
||||||
"EffectScope": true,
|
"EffectScope": true,
|
||||||
|
"ElMessage": true,
|
||||||
|
"ElMessageBox": true,
|
||||||
"ExtractDefaultPropTypes": true,
|
"ExtractDefaultPropTypes": true,
|
||||||
"ExtractPropTypes": true,
|
"ExtractPropTypes": true,
|
||||||
"ExtractPublicPropTypes": true,
|
"ExtractPublicPropTypes": true,
|
||||||
@ -71,8 +73,6 @@
|
|||||||
"watch": true,
|
"watch": true,
|
||||||
"watchEffect": true,
|
"watchEffect": true,
|
||||||
"watchPostEffect": true,
|
"watchPostEffect": true,
|
||||||
"watchSyncEffect": true,
|
"watchSyncEffect": true
|
||||||
"ElMessage": true,
|
|
||||||
"ElMessageBox": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/assets/icons/svg/code.svg
Normal file
1
src/assets/icons/svg/code.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1748779086649" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11153" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M781.02 135.046c18.358 16.319 32.98 30.091 43.861 41.312 10.881 11.222 19.214 20.743 24.994 28.564 5.783 7.821 9.353 14.451 10.711 19.894 1.362 5.439 2.043 10.201 2.043 14.282v16.32H712.671c-7.482 0-13.776-2.549-18.873-7.652-5.103-5.098-9.349-11.052-12.755-17.852-3.4-6.8-5.949-13.77-7.646-20.912-1.702-7.14-2.554-13.089-2.554-17.852V65.676h2.043c6.119 0 11.897 0.68 17.34 2.037 5.439 1.362 12.074 4.593 19.891 9.694 7.822 5.098 17.347 12.24 28.564 21.424 11.221 9.179 25.332 21.252 42.339 36.215z" p-id="11154"></path><path d="M753.127 322.244c-82.35 0-149.108-66.758-149.108-149.108V65.033H269.924c-61.762 0-111.831 50.068-111.831 111.831V847.85c0 61.762 50.069 111.831 111.831 111.831h480.873c61.763 0 111.831-50.069 111.831-111.831V322.244H753.127zM468.903 665.756c5.179 5.178 5.179 13.574 0 18.753l-42.193 42.193c-5.178 5.178-13.574 5.179-18.753 0L262.625 581.369c-5.179-5.178-5.179-13.574 0-18.752l42.193-42.194c0.16-0.16 0.348-0.27 0.515-0.42l102.722-102.722c5.178-5.179 13.574-5.178 18.753 0.001l42.194 42.195c5.178 5.179 5.178 13.574 0 18.753l-93.812 93.812 93.713 93.714z m283.602-84.387L607.172 726.702c-5.178 5.179-13.574 5.178-18.752 0l-42.193-42.193c-5.179-5.179-5.179-13.574 0-18.753l93.714-93.714-93.812-93.812c-5.178-5.179-5.178-13.574 0-18.753l42.194-42.195c5.179-5.179 13.574-5.179 18.752-0.001l102.723 102.722c0.166 0.151 0.354 0.26 0.515 0.42l42.193 42.194c5.177 5.177 5.177 13.574-0.001 18.752z" p-id="11155"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
150
src/components/FilesSelect/index.vue
Normal file
150
src/components/FilesSelect/index.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<!-- 文件上传 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
||||||
|
import { useFileDialog } from '@vueuse/core';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import Popover from '@/components/Popover/index.vue';
|
||||||
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||||
|
import { useFilesStore } from '@/stores/modules/files';
|
||||||
|
|
||||||
|
type FilesList = FilesCardProps & {
|
||||||
|
file: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filesStore = useFilesStore();
|
||||||
|
|
||||||
|
/* 弹出面板 开始 */
|
||||||
|
const popoverStyle = ref({
|
||||||
|
padding: '4px',
|
||||||
|
height: 'fit-content',
|
||||||
|
background: 'var(--el-bg-color, #fff)',
|
||||||
|
border: '1px solid var(--el-border-color-light)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
|
||||||
|
});
|
||||||
|
const popoverRef = ref();
|
||||||
|
/* 弹出面板 结束 */
|
||||||
|
|
||||||
|
const { reset, open, onChange } = useFileDialog({
|
||||||
|
// 允许所有图片文件,文档文件,音视频文件
|
||||||
|
accept: 'image/*,video/*,audio/*,application/*',
|
||||||
|
directory: false, // 是否允许选择文件夹
|
||||||
|
multiple: true, // 是否允许多选
|
||||||
|
});
|
||||||
|
|
||||||
|
onChange((files) => {
|
||||||
|
if (!files)
|
||||||
|
return;
|
||||||
|
console.log('files', files);
|
||||||
|
const arr = [] as FilesList[];
|
||||||
|
for (let i = 0; i < files!.length; i++) {
|
||||||
|
const file = files![i];
|
||||||
|
arr.push({
|
||||||
|
uid: crypto.randomUUID(), // 不写 uid,文件列表展示不出来,elx 1.2.0 bug 待修复
|
||||||
|
name: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
file,
|
||||||
|
maxWidth: '200px',
|
||||||
|
showDelIcon: true, // 显示删除图标
|
||||||
|
imgPreview: true, // 显示图片预览
|
||||||
|
imgVariant: 'square', // 图片预览的形状
|
||||||
|
url: URL.createObjectURL(file), // 图片预览地址
|
||||||
|
});
|
||||||
|
}
|
||||||
|
filesStore.setFilesList([...filesStore.filesList, ...arr]);
|
||||||
|
// 重置文件选择器
|
||||||
|
nextTick(() => reset());
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleUploadFiles() {
|
||||||
|
open();
|
||||||
|
popoverRef.value.hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="files-select">
|
||||||
|
<Popover
|
||||||
|
ref="popoverRef"
|
||||||
|
placement="top-start"
|
||||||
|
:offset="[4, 0]"
|
||||||
|
popover-class="popover-content"
|
||||||
|
:popover-style="popoverStyle"
|
||||||
|
trigger="clickTarget"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<Paperclip />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="popover-content-box">
|
||||||
|
<div
|
||||||
|
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
||||||
|
@click="handleUploadFiles"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<Upload />
|
||||||
|
</el-icon>
|
||||||
|
<div class="font-size-14px">
|
||||||
|
上传文件或图片
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
placement="right-end"
|
||||||
|
:offset="[8, 4]"
|
||||||
|
popover-class="popover-content"
|
||||||
|
:popover-style="popoverStyle"
|
||||||
|
trigger="hover"
|
||||||
|
:hover-delay="100"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<div
|
||||||
|
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
||||||
|
>
|
||||||
|
<SvgIcon name="code" size="16" />
|
||||||
|
<div class="font-size-14px">
|
||||||
|
上传代码
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-icon class="ml-auto">
|
||||||
|
<ArrowRight />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="popover-content-box">
|
||||||
|
<div
|
||||||
|
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
ElMessage.warning('暂未开放');
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
代码文件
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
ElMessage.warning('暂未开放');
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
代码文件夹
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@ -28,6 +28,10 @@ const popoverStyle = ref({
|
|||||||
width: '200px',
|
width: '200px',
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
height: 'fit-content',
|
height: 'fit-content',
|
||||||
|
background: 'var(--el-bg-color, #fff)',
|
||||||
|
border: '1px solid var(--el-border-color-light)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
|
||||||
});
|
});
|
||||||
const popoverRef = ref();
|
const popoverRef = ref();
|
||||||
|
|
||||||
@ -45,25 +49,25 @@ function handleClick(item: GetSessionListVO) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="model-switch">
|
<div class="model-select">
|
||||||
<Popover
|
<Popover
|
||||||
ref="popoverRef"
|
ref="popoverRef"
|
||||||
position="top-start"
|
placement="top-start"
|
||||||
:offset="[-14, 10]"
|
:offset="[4, 0]"
|
||||||
:trigger-style="{ cursor: 'pointer' }"
|
|
||||||
popover-class="popover-content"
|
popover-class="popover-content"
|
||||||
:popover-style="popoverStyle"
|
:popover-style="popoverStyle"
|
||||||
|
trigger="clickTarget"
|
||||||
@show="showPopover"
|
@show="showPopover"
|
||||||
>
|
>
|
||||||
<!-- 触发元素插槽 -->
|
<!-- 触发元素插槽 -->
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div
|
<div
|
||||||
class="model-switch-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()]"
|
class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()]"
|
||||||
>
|
>
|
||||||
<div class="model-switch-box-icon">
|
<div class="model-select-box-icon">
|
||||||
<SvgIcon name="models" size="12" />
|
<SvgIcon name="models" size="12" />
|
||||||
</div>
|
</div>
|
||||||
<div class="model-switch-box-text font-size-12px">
|
<div class="model-select-box-text font-size-12px">
|
||||||
{{ currentModelName }}
|
{{ currentModelName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -73,30 +77,30 @@ function handleClick(item: GetSessionListVO) {
|
|||||||
<div
|
<div
|
||||||
v-for="item in popoverList"
|
v-for="item in popoverList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="popover-content-box-items flex rounded-8px select-none transition-all transition-duration-300 flex items-center gap-8px p-4px hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
|
class="popover-content-box-items w-full rounded-8px select-none transition-all transition-duration-300 flex items-center hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
|
||||||
>
|
>
|
||||||
<el-tooltip
|
<Popover
|
||||||
popper-class="rounded-tooltip"
|
trigger-class="popover-trigger-item-text"
|
||||||
effect="dark"
|
popover-class="rounded-tooltip"
|
||||||
placement="right"
|
placement="right"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
:offset="10"
|
:offset="[12, 0]"
|
||||||
:show-arrow="false"
|
|
||||||
transition="zoom-fade"
|
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #trigger>
|
||||||
<div class="popover-content-box-item-text text-wrap max-w-200px rounded-lg">
|
<div
|
||||||
{{ item.remark }}
|
class="popover-content-box-item p-4px font-size-12px text-overflow line-height-16px"
|
||||||
|
:class="{ 'bg-[rgba(0,0,0,.04)] is-select': item.modelName === currentModelName }"
|
||||||
|
@click="handleClick(item)"
|
||||||
|
>
|
||||||
|
{{ item.modelName }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="popover-content-box-item font-size-12px text-overflow w-full line-height-16px"
|
class="popover-content-box-item-text text-wrap max-w-200px rounded-lg p-8px font-size-12px line-height-tight"
|
||||||
:class="{ 'bg-[rgba(0,0,0,.04)] is-select': item.modelName === currentModelName }"
|
|
||||||
@click="handleClick(item)"
|
|
||||||
>
|
>
|
||||||
{{ item.modelName }}
|
{{ item.remark }}
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -104,7 +108,7 @@ function handleClick(item: GetSessionListVO) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.model-switch-box {
|
.model-select-box {
|
||||||
color: var(--el-color-primary, #409eff);
|
color: var(--el-color-primary, #409eff);
|
||||||
border: 1px solid var(--el-color-primary, #409eff);
|
border: 1px solid var(--el-color-primary, #409eff);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -116,20 +120,35 @@ function handleClick(item: GetSessionListVO) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popover-content-box {
|
.popover-content-box {
|
||||||
// background-color: red;
|
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
gap: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.popover-content-box-items {
|
||||||
|
:deep() {
|
||||||
|
.popover-trigger-item-text {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-content-box-item-text {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
// 滚动条样式
|
// 滚动条样式
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@ -1,431 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { CSSProperties } from 'vue';
|
|
||||||
import { onClickOutside, useEventListener } from '@vueuse/core';
|
|
||||||
|
|
||||||
type PopoverPosition =
|
|
||||||
| 'top'
|
|
||||||
| 'top-start'
|
|
||||||
| 'top-end'
|
|
||||||
| 'bottom'
|
|
||||||
| 'bottom-start'
|
|
||||||
| 'bottom-end'
|
|
||||||
| 'left'
|
|
||||||
| 'left-start'
|
|
||||||
| 'left-end'
|
|
||||||
| 'right'
|
|
||||||
| 'right-start'
|
|
||||||
| 'right-end';
|
|
||||||
|
|
||||||
type Offset = [number, number];
|
|
||||||
const props = withDefaults(defineProps<PopoverProps>(), {
|
|
||||||
position: 'bottom',
|
|
||||||
offset: () => [8, 8],
|
|
||||||
boundary: 'viewport',
|
|
||||||
closeOnContentClick: false,
|
|
||||||
closeOnTriggerClick: false,
|
|
||||||
triggerStyle: () => ({}),
|
|
||||||
popoverStyle: () => ({}),
|
|
||||||
popoverClass: '',
|
|
||||||
});
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(e: 'show'): void;
|
|
||||||
(e: 'hide'): void;
|
|
||||||
(e: 'positionChange', pos: PopoverPosition): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const VIEWPORT_PADDING = 16;
|
|
||||||
interface PopoverProps {
|
|
||||||
position?: PopoverPosition;
|
|
||||||
offset?: Offset;
|
|
||||||
triggerStyle?: CSSProperties;
|
|
||||||
popoverStyle?: CSSProperties;
|
|
||||||
popoverClass?: string;
|
|
||||||
boundary?: 'viewport' | HTMLElement;
|
|
||||||
closeOnContentClick?: boolean;
|
|
||||||
closeOnTriggerClick?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerRef = ref<HTMLElement | null>(null);
|
|
||||||
const popoverRef = ref<HTMLElement | null>(null);
|
|
||||||
const showPoperContent = ref(false);
|
|
||||||
const currentPosition = ref<PopoverPosition>(props.position);
|
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
|
||||||
let mutationObserver: MutationObserver | null = null;
|
|
||||||
let updatePositionTimeout: number | null = null;
|
|
||||||
|
|
||||||
// 初始化尺寸与内容变化监听
|
|
||||||
function initObservers() {
|
|
||||||
if (!popoverRef.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// 监听弹出框尺寸变化
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
requestAnimationFrame(updatePosition);
|
|
||||||
});
|
|
||||||
resizeObserver.observe(popoverRef.value);
|
|
||||||
|
|
||||||
// 监听弹出框内容变化(子节点/样式变更)
|
|
||||||
mutationObserver = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
|
||||||
updatePosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestAnimationFrame(updatePosition);
|
|
||||||
});
|
|
||||||
mutationObserver.observe(popoverRef.value, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style', 'class'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 销毁监听
|
|
||||||
function destroyObservers() {
|
|
||||||
resizeObserver?.disconnect();
|
|
||||||
mutationObserver?.disconnect();
|
|
||||||
resizeObserver = null;
|
|
||||||
mutationObserver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 强制更新定位(处理DOM缓存)
|
|
||||||
function updatePosition() {
|
|
||||||
if (!triggerRef.value || !popoverRef.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// 强制触发回流获取最新尺寸
|
|
||||||
triggerRef.value.getBoundingClientRect();
|
|
||||||
popoverRef.value.getBoundingClientRect();
|
|
||||||
|
|
||||||
const triggerRect = triggerRef.value.getBoundingClientRect();
|
|
||||||
const popoverRect = popoverRef.value.getBoundingClientRect();
|
|
||||||
const boundaryRect = getBoundaryRect();
|
|
||||||
|
|
||||||
const adjustedPos = adjustPosition(triggerRect, popoverRect, boundaryRect);
|
|
||||||
currentPosition.value = adjustedPos;
|
|
||||||
emits('positionChange', adjustedPos);
|
|
||||||
|
|
||||||
const { top, left, origin } = calculatePosition(triggerRect, popoverRect, adjustedPos);
|
|
||||||
popoverRef.value.style.top = `${top}px`;
|
|
||||||
popoverRef.value.style.left = `${left}px`;
|
|
||||||
popoverRef.value.style.transformOrigin = origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口尺寸变化防抖处理
|
|
||||||
useEventListener('resize', () => {
|
|
||||||
if (updatePositionTimeout)
|
|
||||||
clearTimeout(updatePositionTimeout);
|
|
||||||
updatePositionTimeout = setTimeout(updatePosition, 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 显示/隐藏状态监听
|
|
||||||
watch(
|
|
||||||
showPoperContent,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
nextTick(() => {
|
|
||||||
initObservers();
|
|
||||||
updatePosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
destroyObservers();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// 边界计算(保持不变)
|
|
||||||
function getBoundaryRect(): DOMRect {
|
|
||||||
if (props.boundary === 'viewport') {
|
|
||||||
return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
|
|
||||||
}
|
|
||||||
return (props.boundary as HTMLElement).getBoundingClientRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定位计算(保持不变)
|
|
||||||
function calculatePosition(triggerRect: DOMRect, popoverRect: DOMRect, position: PopoverPosition) {
|
|
||||||
const [offsetX, offsetY] = props.offset!;
|
|
||||||
const { width: tW, height: tH } = triggerRect;
|
|
||||||
const { width: pW, height: pH } = popoverRect;
|
|
||||||
|
|
||||||
const positionMap: Record<PopoverPosition, { top: number; left: number; origin: string }> = {
|
|
||||||
'top': {
|
|
||||||
top: triggerRect.top - pH - offsetY,
|
|
||||||
left: triggerRect.left + tW / 2 - pW / 2 + offsetX,
|
|
||||||
origin: 'bottom center',
|
|
||||||
},
|
|
||||||
'top-start': {
|
|
||||||
top: triggerRect.top - pH - offsetY,
|
|
||||||
left: triggerRect.left + offsetX,
|
|
||||||
origin: 'bottom left',
|
|
||||||
},
|
|
||||||
'top-end': {
|
|
||||||
top: triggerRect.top - pH - offsetY,
|
|
||||||
left: triggerRect.left + tW - pW + offsetX,
|
|
||||||
origin: 'bottom right',
|
|
||||||
},
|
|
||||||
'bottom': {
|
|
||||||
top: triggerRect.bottom + offsetY,
|
|
||||||
left: triggerRect.left + tW / 2 - pW / 2 + offsetX,
|
|
||||||
origin: 'top center',
|
|
||||||
},
|
|
||||||
'bottom-start': {
|
|
||||||
top: triggerRect.bottom + offsetY,
|
|
||||||
left: triggerRect.left + offsetX,
|
|
||||||
origin: 'top left',
|
|
||||||
},
|
|
||||||
'bottom-end': {
|
|
||||||
top: triggerRect.bottom + offsetY,
|
|
||||||
left: triggerRect.left + tW - pW + offsetX,
|
|
||||||
origin: 'top right',
|
|
||||||
},
|
|
||||||
'left': {
|
|
||||||
top: triggerRect.top + tH / 2 - pH / 2 + offsetY,
|
|
||||||
left: triggerRect.left - pW - offsetX,
|
|
||||||
origin: 'right center',
|
|
||||||
},
|
|
||||||
'left-start': {
|
|
||||||
top: triggerRect.top + offsetY,
|
|
||||||
left: triggerRect.left - pW - offsetX,
|
|
||||||
origin: 'right top',
|
|
||||||
},
|
|
||||||
'left-end': {
|
|
||||||
top: triggerRect.top + tH - pH + offsetY,
|
|
||||||
left: triggerRect.left - pW - offsetX,
|
|
||||||
origin: 'right bottom',
|
|
||||||
},
|
|
||||||
'right': {
|
|
||||||
top: triggerRect.top + tH / 2 - pH / 2 + offsetY,
|
|
||||||
left: triggerRect.right + offsetX,
|
|
||||||
origin: 'left center',
|
|
||||||
},
|
|
||||||
'right-start': {
|
|
||||||
top: triggerRect.top + offsetY,
|
|
||||||
left: triggerRect.right + offsetX,
|
|
||||||
origin: 'left top',
|
|
||||||
},
|
|
||||||
'right-end': {
|
|
||||||
top: triggerRect.top + tH - pH + offsetY,
|
|
||||||
left: triggerRect.right + offsetX,
|
|
||||||
origin: 'left bottom',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return positionMap[position];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 边界调整(保持不变)
|
|
||||||
function adjustPosition(
|
|
||||||
triggerRect: DOMRect,
|
|
||||||
popoverRect: DOMRect,
|
|
||||||
boundaryRect: DOMRect,
|
|
||||||
): PopoverPosition {
|
|
||||||
const allPositions: PopoverPosition[] = [
|
|
||||||
'top',
|
|
||||||
'top-start',
|
|
||||||
'top-end',
|
|
||||||
'bottom',
|
|
||||||
'bottom-start',
|
|
||||||
'bottom-end',
|
|
||||||
'left',
|
|
||||||
'left-start',
|
|
||||||
'left-end',
|
|
||||||
'right',
|
|
||||||
'right-start',
|
|
||||||
'right-end',
|
|
||||||
];
|
|
||||||
const candidatePositions = [props.position, ...allPositions.filter(p => p !== props.position)];
|
|
||||||
|
|
||||||
for (const pos of candidatePositions) {
|
|
||||||
const { top, left } = calculatePosition(triggerRect, popoverRect, pos);
|
|
||||||
if (
|
|
||||||
top >= boundaryRect.top + VIEWPORT_PADDING
|
|
||||||
&& left >= boundaryRect.left + VIEWPORT_PADDING
|
|
||||||
&& top + popoverRect.height <= boundaryRect.bottom - VIEWPORT_PADDING
|
|
||||||
&& left + popoverRect.width <= boundaryRect.right - VIEWPORT_PADDING
|
|
||||||
) {
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return props.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 交互逻辑(保持不变)
|
|
||||||
function handleTriggerClick() {
|
|
||||||
if (showPoperContent.value) {
|
|
||||||
props.closeOnTriggerClick && hidePopover();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showPoperContent.value = true;
|
|
||||||
nextTick(() => emits('show'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleContentClick(e: MouseEvent) {
|
|
||||||
props.closeOnContentClick && hidePopover();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hidePopover() {
|
|
||||||
showPoperContent.value = false;
|
|
||||||
emits('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickOutside(popoverRef, () => !props.closeOnTriggerClick && hidePopover(), {
|
|
||||||
ignore: [triggerRef],
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
destroyObservers();
|
|
||||||
updatePositionTimeout && clearTimeout(updatePositionTimeout);
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ show: () => (showPoperContent.value = true), hide: hidePopover });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="triggerRef"
|
|
||||||
:style="props.triggerStyle"
|
|
||||||
role="button"
|
|
||||||
aria-haspopup="true"
|
|
||||||
:aria-expanded="showPoperContent"
|
|
||||||
@click.stop="handleTriggerClick"
|
|
||||||
>
|
|
||||||
<slot name="trigger" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition name="popover-fade" @before-enter="updatePosition">
|
|
||||||
<div
|
|
||||||
v-if="showPoperContent"
|
|
||||||
ref="popoverRef"
|
|
||||||
class="popover-content"
|
|
||||||
:style="props.popoverStyle"
|
|
||||||
:class="[props.popoverClass]"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="false"
|
|
||||||
:data-popper-placement="currentPosition"
|
|
||||||
@click="handleContentClick"
|
|
||||||
>
|
|
||||||
<slot name="header" />
|
|
||||||
<slot />
|
|
||||||
<slot name="footer" />
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.popover-fade-enter-active,
|
|
||||||
.popover-fade-leave-active {
|
|
||||||
transition:
|
|
||||||
opacity 0.2s ease,
|
|
||||||
transform 0.2s ease;
|
|
||||||
will-change: transform, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-fade-enter-from,
|
|
||||||
.popover-fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-fade-enter-to,
|
|
||||||
.popover-fade-leave-from {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-content {
|
|
||||||
position: fixed;
|
|
||||||
min-width: 120px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
||||||
padding: 12px 16px;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border: 6px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement^="top"]::before {
|
|
||||||
top: 100%;
|
|
||||||
border-top-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="top"]::before {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="top-start"]::before {
|
|
||||||
left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="top-end"]::before {
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement^="bottom"]::before {
|
|
||||||
bottom: 100%;
|
|
||||||
border-bottom-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="bottom"]::before {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="bottom-start"]::before {
|
|
||||||
left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="bottom-end"]::before {
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement^="left"]::before {
|
|
||||||
left: 100%;
|
|
||||||
border-left-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="left"]::before {
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="left-start"]::before {
|
|
||||||
top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="left-end"]::before {
|
|
||||||
bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement^="right"]::before {
|
|
||||||
right: 100%;
|
|
||||||
border-right-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="right"]::before {
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="right-start"]::before {
|
|
||||||
top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-popper-placement="right-end"]::before {
|
|
||||||
bottom: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,31 +1,9 @@
|
|||||||
<!-- Popover 弹框 -->
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import { flip, offset, shift, useFloating } from '@floating-ui/vue';
|
import { arrow, autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue';
|
||||||
|
import { onClickOutside } from '@vueuse/core';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<PopoverProps>(), {
|
export type PopoverPlacement =
|
||||||
position: 'bottom',
|
|
||||||
offset: () => [8, 8],
|
|
||||||
boundary: 'viewport',
|
|
||||||
closeOnContentClick: false,
|
|
||||||
closeOnTriggerClick: false,
|
|
||||||
triggerStyle: () => ({}),
|
|
||||||
popoverStyle: () => ({}),
|
|
||||||
popoverClass: '',
|
|
||||||
});
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(e: 'show'): void;
|
|
||||||
(e: 'hide'): void;
|
|
||||||
(e: 'positionChange', pos: PopoverPosition): void;
|
|
||||||
}>();
|
|
||||||
const reference = ref();
|
|
||||||
const floating = ref(null);
|
|
||||||
const { floatingStyles } = useFloating(reference, floating, {
|
|
||||||
placement: props.position,
|
|
||||||
middleware: [offset(props.offset[0]), flip(), shift()],
|
|
||||||
});
|
|
||||||
|
|
||||||
type PopoverPosition =
|
|
||||||
| 'top'
|
| 'top'
|
||||||
| 'top-start'
|
| 'top-start'
|
||||||
| 'top-end'
|
| 'top-end'
|
||||||
@ -39,54 +17,189 @@ type PopoverPosition =
|
|||||||
| 'right-start'
|
| 'right-start'
|
||||||
| 'right-end';
|
| 'right-end';
|
||||||
|
|
||||||
type Offset = [number, number];
|
export type Offset = [number, number];
|
||||||
interface PopoverProps {
|
|
||||||
position?: PopoverPosition;
|
export interface PopoverProps {
|
||||||
|
placement?: PopoverPlacement;
|
||||||
offset?: Offset;
|
offset?: Offset;
|
||||||
triggerStyle?: CSSProperties;
|
|
||||||
popoverStyle?: CSSProperties;
|
popoverStyle?: CSSProperties;
|
||||||
popoverClass?: string;
|
popoverClass?: string;
|
||||||
boundary?: 'viewport' | HTMLElement;
|
trigger?: 'hover' | 'click' | 'clickTarget';
|
||||||
closeOnContentClick?: boolean;
|
triggerStyle?: CSSProperties;
|
||||||
closeOnTriggerClick?: boolean;
|
triggerClass?: string;
|
||||||
|
hoverDelay?: number; // 悬停延迟关闭时间(ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPoperContent = ref(false);
|
const props = withDefaults(defineProps<PopoverProps>(), {
|
||||||
|
placement: 'bottom',
|
||||||
|
offset: () => [0, 0],
|
||||||
|
trigger: 'hover',
|
||||||
|
hoverDelay: 0, // 默认300ms延迟关闭
|
||||||
|
});
|
||||||
|
|
||||||
function handleTriggerClick() {
|
const emits = defineEmits<{
|
||||||
showPoperContent.value = !showPoperContent.value;
|
(e: 'show'): void;
|
||||||
if (showPoperContent.value) {
|
(e: 'hide'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const triggerRef = ref<HTMLElement | null>(null);
|
||||||
|
const popoverRef = ref<HTMLElement | null>(null);
|
||||||
|
const floatingArrow = ref<HTMLElement | null>(null);
|
||||||
|
const showPopover = ref(false);
|
||||||
|
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
// 新增:记录鼠标是否在触发元素或内容区域内
|
||||||
|
const isHovering = ref(false);
|
||||||
|
|
||||||
|
const { floatingStyles } = useFloating(triggerRef, popoverRef, {
|
||||||
|
placement: props.placement,
|
||||||
|
transform: false,
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
middleware: [
|
||||||
|
shift(),
|
||||||
|
flip(),
|
||||||
|
arrow({ element: floatingArrow }),
|
||||||
|
offset({
|
||||||
|
mainAxis: props.offset[0],
|
||||||
|
crossAxis: props.offset[1],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (!showPopover.value) {
|
||||||
|
showPopover.value = true;
|
||||||
emits('show');
|
emits('show');
|
||||||
}
|
}
|
||||||
else {
|
// 显示时强制清除定时器(无论是否在悬停)
|
||||||
|
if (hideTimeout)
|
||||||
|
clearTimeout(hideTimeout);
|
||||||
|
hideTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (showPopover.value) {
|
||||||
|
showPopover.value = false;
|
||||||
emits('hide');
|
emits('hide');
|
||||||
}
|
}
|
||||||
|
hideTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, hide });
|
||||||
|
|
||||||
|
watch(showPopover, (newValue) => {
|
||||||
|
if (newValue && props.trigger !== 'hover') {
|
||||||
|
onClickOutside(popoverRef, () => hide(), {
|
||||||
|
ignore: [triggerRef] as any[],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触发元素鼠标事件调整(同步isHovering状态)
|
||||||
|
function handleTriggerMouseEnter() {
|
||||||
|
if (props.trigger === 'hover') {
|
||||||
|
isHovering.value = true; // 进入触发元素
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTriggerMouseLeave() {
|
||||||
|
if (props.trigger === 'hover') {
|
||||||
|
isHovering.value = false; // 离开触发元素
|
||||||
|
// 仅当鼠标不在内容区域时,才设置延迟关闭
|
||||||
|
scheduleHideIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域鼠标事件调整(同步isHovering状态)
|
||||||
|
function handlePopoverMouseEnter() {
|
||||||
|
if (props.trigger === 'hover') {
|
||||||
|
isHovering.value = true; // 进入内容区域
|
||||||
|
if (hideTimeout)
|
||||||
|
clearTimeout(hideTimeout); // 取消关闭
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePopoverMouseLeave() {
|
||||||
|
if (props.trigger === 'hover') {
|
||||||
|
isHovering.value = false; // 离开内容区域
|
||||||
|
// 仅当鼠标不在触发元素时,才设置延迟关闭
|
||||||
|
scheduleHideIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:统一延迟关闭逻辑(仅当完全离开两个区域时触发)
|
||||||
|
function scheduleHideIfNeeded() {
|
||||||
|
// 如果鼠标仍在任一区域(isHovering为true),不关闭
|
||||||
|
if (isHovering.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 否则设置延迟关闭
|
||||||
|
hideTimeout = setTimeout(() => {
|
||||||
|
if (!isHovering.value) {
|
||||||
|
// 再次确认是否仍离开
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
}, props.hoverDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (props.trigger === 'click') {
|
||||||
|
showPopover.value ? hide() : show();
|
||||||
|
}
|
||||||
|
else if (props.trigger === 'clickTarget' && !showPopover.value) {
|
||||||
|
show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
<div ref="reference" :style="props.triggerStyle" @click.stop="handleTriggerClick">
|
ref="triggerRef"
|
||||||
<slot name="trigger" />
|
class="popover-trigger"
|
||||||
</div>
|
:class="[props.triggerClass]"
|
||||||
|
:style="[props.triggerStyle]"
|
||||||
|
@mouseenter="handleTriggerMouseEnter"
|
||||||
|
@mouseleave="handleTriggerMouseLeave"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
<Transition name="popover-fade">
|
<Transition name="popover-fade">
|
||||||
<div
|
<div
|
||||||
v-if="showPoperContent"
|
v-if="showPopover"
|
||||||
ref="floating"
|
ref="popoverRef"
|
||||||
class="popover-content"
|
:style="[floatingStyles, props.popoverStyle]"
|
||||||
:style="{
|
class="popover-content-box"
|
||||||
...floatingStyles,
|
|
||||||
...props.popoverStyle,
|
|
||||||
}"
|
|
||||||
:class="[props.popoverClass]"
|
:class="[props.popoverClass]"
|
||||||
|
@mouseenter="handlePopoverMouseEnter"
|
||||||
|
@mouseleave="handlePopoverMouseLeave"
|
||||||
>
|
>
|
||||||
<slot name="header" />
|
|
||||||
<slot />
|
<slot />
|
||||||
<slot name="footer" />
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss">
|
||||||
|
.popover-fade-enter-active,
|
||||||
|
.popover-fade-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-fade-enter-from,
|
||||||
|
.popover-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-fade-enter-to,
|
||||||
|
.popover-fade-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -28,10 +28,16 @@ onMounted(async () => {
|
|||||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||||
// 通过 ID 查询详情,设置当前会话 (因为有分页)
|
// 通过 ID 查询详情,设置当前会话 (因为有分页)
|
||||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||||
active.value = `${sessionId.value}`;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => sessionStore.currentSession,
|
||||||
|
(newValue) => {
|
||||||
|
active.value = newValue ? `${newValue.id}` : undefined;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 创建会话
|
// 创建会话
|
||||||
function handleCreatChat() {
|
function handleCreatChat() {
|
||||||
// 创建会话, 跳转到默认聊天
|
// 创建会话, 跳转到默认聊天
|
||||||
|
|||||||
@ -94,8 +94,8 @@ function handleClick(item: any) {
|
|||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<Popover
|
<Popover
|
||||||
ref="popoverRef"
|
ref="popoverRef"
|
||||||
position="bottom-end"
|
placement="bottom-end"
|
||||||
:offset="[0, 8]"
|
trigger="clickTarget"
|
||||||
:trigger-style="{ cursor: 'pointer' }"
|
:trigger-style="{ cursor: 'pointer' }"
|
||||||
popover-class="popover-content"
|
popover-class="popover-content"
|
||||||
:popover-style="popoverStyle"
|
:popover-style="popoverStyle"
|
||||||
@ -105,7 +105,7 @@ function handleClick(item: any) {
|
|||||||
<el-avatar :src="src" :size="28" fit="fit" shape="circle" />
|
<el-avatar :src="src" :size="28" fit="fit" shape="circle" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="popover-content-box">
|
<div class="popover-content-box shadow-lg">
|
||||||
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
||||||
<div
|
<div
|
||||||
v-if="!item.divider"
|
v-if="!item.divider"
|
||||||
@ -130,4 +130,12 @@ function handleClick(item: any) {
|
|||||||
width: 520px;
|
width: 520px;
|
||||||
height: 520px;
|
height: 520px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popover-content-box {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
<!-- 默认消息列表页 -->
|
<!-- 默认消息列表页 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
||||||
|
import FilesSelect from '@/components/FilesSelect/index.vue';
|
||||||
import ModelSelect from '@/components/ModelSelect/index.vue';
|
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||||||
import WelecomeText from '@/components/WelecomeText/index.vue';
|
import WelecomeText from '@/components/WelecomeText/index.vue';
|
||||||
import { useUserStore } from '@/stores';
|
import { useUserStore } from '@/stores';
|
||||||
|
import { useFilesStore } from '@/stores/modules/files';
|
||||||
import { useSessionStore } from '@/stores/modules/session';
|
import { useSessionStore } from '@/stores/modules/session';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
|
const filesStore = useFilesStore();
|
||||||
|
|
||||||
const senderValue = ref('');
|
const senderValue = ref('');
|
||||||
|
const senderRef = ref();
|
||||||
|
|
||||||
async function handleSend() {
|
async function handleSend() {
|
||||||
localStorage.setItem('chatContent', senderValue.value);
|
localStorage.setItem('chatContent', senderValue.value);
|
||||||
@ -18,12 +24,33 @@ async function handleSend() {
|
|||||||
remark: senderValue.value.slice(0, 10),
|
remark: senderValue.value.slice(0, 10),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDeleteCard(_item: FilesCardProps, index: number) {
|
||||||
|
filesStore.deleteFileByIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filesStore.filesList.length,
|
||||||
|
(val) => {
|
||||||
|
if (val > 0) {
|
||||||
|
nextTick(() => {
|
||||||
|
senderRef.value.openHeader();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nextTick(() => {
|
||||||
|
senderRef.value.closeHeader();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-defaul-wrap">
|
<div class="chat-defaul-wrap">
|
||||||
<WelecomeText />
|
<WelecomeText />
|
||||||
<Sender
|
<Sender
|
||||||
|
ref="senderRef"
|
||||||
v-model="senderValue"
|
v-model="senderValue"
|
||||||
class="chat-defaul-sender"
|
class="chat-defaul-sender"
|
||||||
:auto-size="{
|
:auto-size="{
|
||||||
@ -35,16 +62,43 @@ async function handleSend() {
|
|||||||
allow-speech
|
allow-speech
|
||||||
@submit="handleSend"
|
@submit="handleSend"
|
||||||
>
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="sender-header p-12px pt-6px pb-0px">
|
||||||
|
<Attachments
|
||||||
|
:items="filesStore.filesList"
|
||||||
|
:hide-upload="true"
|
||||||
|
@delete-card="handleDeleteCard"
|
||||||
|
>
|
||||||
|
<template #prev-button="{ show, onScrollLeft }">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="prev-next-btn left-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
|
||||||
|
@click="onScrollLeft"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<ArrowLeftBold />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #next-button="{ show, onScrollRight }">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="prev-next-btn right-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
|
||||||
|
@click="onScrollRight"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<ArrowRightBold />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Attachments>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
||||||
|
<FilesSelect />
|
||||||
<ModelSelect />
|
<ModelSelect />
|
||||||
<div
|
|
||||||
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]"
|
|
||||||
>
|
|
||||||
<el-icon>
|
|
||||||
<Paperclip />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sender>
|
</Sender>
|
||||||
|
|||||||
@ -2,12 +2,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
||||||
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
||||||
|
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
||||||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
||||||
import { Loading, Position } from '@element-plus/icons-vue';
|
|
||||||
import { useXStream } from 'vue-element-plus-x';
|
import { useXStream } from 'vue-element-plus-x';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { send } from '@/api/chat/index';
|
import { send } from '@/api/chat/index';
|
||||||
|
import FilesSelect from '@/components/FilesSelect/index.vue';
|
||||||
|
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||||||
import { useChatStore } from '@/stores/modules/chat';
|
import { useChatStore } from '@/stores/modules/chat';
|
||||||
|
import { useFilesStore } from '@/stores/modules/files';
|
||||||
import { useModelStore } from '@/stores/modules/model';
|
import { useModelStore } from '@/stores/modules/model';
|
||||||
|
|
||||||
type MessageItem = BubbleProps & {
|
type MessageItem = BubbleProps & {
|
||||||
@ -18,7 +21,7 @@ type MessageItem = BubbleProps & {
|
|||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { startStream: _, cancel, data, error, isLoading } = useXStream();
|
const { cancel, error, isLoading } = useXStream();
|
||||||
|
|
||||||
// const BASE_URL = 'https://api.siliconflow.cn/v1/chat/completions';
|
// const BASE_URL = 'https://api.siliconflow.cn/v1/chat/completions';
|
||||||
// 仅供测试,请勿拿去测试其他付费模型
|
// 仅供测试,请勿拿去测试其他付费模型
|
||||||
@ -28,18 +31,22 @@ const { startStream: _, cancel, data, error, isLoading } = useXStream();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const modelStore = useModelStore();
|
const modelStore = useModelStore();
|
||||||
const inputValue = ref('帮我写一篇小米手机介绍');
|
const filesStore = useFilesStore();
|
||||||
|
|
||||||
|
const inputValue = ref('');
|
||||||
const senderRef = ref<any>(null);
|
const senderRef = ref<any>(null);
|
||||||
const bubbleItems = ref<MessageItem[]>([]);
|
const bubbleItems = ref<MessageItem[]>([]);
|
||||||
const bubbleListRef = ref<BubbleListInstance | null>(null);
|
const bubbleListRef = ref<BubbleListInstance | null>(null);
|
||||||
const processedIndex = ref(0);
|
// const processedIndex = ref(0);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params?.id,
|
() => route.params?.id,
|
||||||
async (_id_) => {
|
async (_id_) => {
|
||||||
if (_id_) {
|
if (_id_) {
|
||||||
await chatStore.requestChatList(`${_id_}`);
|
// 清空输入框
|
||||||
// 判断的当前会话id是否有聊天记录
|
inputValue.value = '';
|
||||||
|
|
||||||
|
// 判断的当前会话id是否有聊天记录,有缓存则直接赋值展示
|
||||||
if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) {
|
if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) {
|
||||||
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
|
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
|
||||||
key: item.id,
|
key: item.id,
|
||||||
@ -57,17 +64,39 @@ watch(
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
bubbleListRef.value!.scrollToBottom();
|
bubbleListRef.value!.scrollToBottom();
|
||||||
}, 350);
|
}, 350);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 无缓存则请求聊天记录
|
||||||
|
await chatStore.requestChatList(`${_id_}`);
|
||||||
|
// 请求聊天记录后,赋值回显,并滚动到底部
|
||||||
|
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
|
||||||
|
key: item.id,
|
||||||
|
avatar:
|
||||||
|
item.role === 'user'
|
||||||
|
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
|
||||||
|
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
||||||
|
content: item.content,
|
||||||
|
avatarSize: '32px',
|
||||||
|
role: item.role,
|
||||||
|
typing: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
setTimeout(() => {
|
||||||
|
bubbleListRef.value!.scrollToBottom();
|
||||||
|
}, 350);
|
||||||
|
|
||||||
|
// 如果本地有发送内容 ,则直接发送
|
||||||
const v = localStorage.getItem('chatContent');
|
const v = localStorage.getItem('chatContent');
|
||||||
if (v) {
|
if (v) {
|
||||||
inputValue.value = v;
|
|
||||||
localStorage.removeItem('chatContent');
|
localStorage.removeItem('chatContent');
|
||||||
// 发送消息
|
// 发送消息
|
||||||
console.log('发送消息 v', v);
|
console.log('发送消息 v', v);
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
startSSE();
|
// startSSE();
|
||||||
}, 350);
|
// }, 350);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -113,28 +142,88 @@ function handleDataChunk(chunk: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
// watch(
|
||||||
data,
|
// data,
|
||||||
() => {
|
// () => {
|
||||||
for (let i = processedIndex.value; i < data.value.length; i++) {
|
// for (let i = processedIndex.value; i < data.value.length; i++) {
|
||||||
const chunk = data.value[i].data;
|
// const chunk = data.value[i].data;
|
||||||
handleDataChunk(chunk);
|
// handleDataChunk(chunk);
|
||||||
processedIndex.value++;
|
// processedIndex.value++;
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{ deep: true },
|
// { deep: true },
|
||||||
);
|
// );
|
||||||
|
|
||||||
// 封装错误处理逻辑
|
// 封装错误处理逻辑
|
||||||
function handleError(err: any) {
|
function handleError(err: any) {
|
||||||
console.error('Fetch error:', err);
|
console.error('Fetch error:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startSSE() {
|
function checkJsonSerializable(data: any) {
|
||||||
|
const visited = new WeakSet();
|
||||||
|
let error: any = null;
|
||||||
|
|
||||||
|
function check(value: any, currentPath = 'root') {
|
||||||
|
if (error)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const type = typeof value;
|
||||||
|
|
||||||
|
// 处理基本类型(支持 JSON 的类型)
|
||||||
|
if (type === 'string' || type === 'number' || type === 'boolean' || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 undefined(JSON 不支持)
|
||||||
|
if (value === undefined) {
|
||||||
|
error = { path: currentPath, value, type: 'undefined' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理函数、Symbol(JSON 不支持)
|
||||||
|
if (type === 'function' || type === 'symbol') {
|
||||||
|
error = { path: currentPath, value, type };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数组:递归检查每个元素
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
check(item, `${currentPath}[${index}]`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对象:检查是否为普通对象(非特殊对象)
|
||||||
|
if (type === 'object' && value !== null) {
|
||||||
|
// 检测循环引用
|
||||||
|
if (visited.has(value)) {
|
||||||
|
error = { path: currentPath, value, type: 'circular_reference' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.add(value);
|
||||||
|
|
||||||
|
// 递归检查对象的每个属性
|
||||||
|
for (const [key, val] of Object.entries(value)) {
|
||||||
|
check(val, `${currentPath}.${key}`);
|
||||||
|
}
|
||||||
|
visited.delete(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他不支持的类型(如 Date、RegExp 等)
|
||||||
|
error = { path: currentPath, value, type };
|
||||||
|
}
|
||||||
|
|
||||||
|
check(data);
|
||||||
|
return { valid: !error, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startSSE(chatContent: string) {
|
||||||
try {
|
try {
|
||||||
// 添加用户输入的消息
|
// 添加用户输入的消息
|
||||||
console.log('inputValue.value', inputValue.value);
|
console.log('chatContent', chatContent);
|
||||||
addMessage(inputValue.value, true);
|
addMessage(chatContent, true);
|
||||||
addMessage('', false);
|
addMessage('', false);
|
||||||
|
|
||||||
// 这里有必要调用一下 BubbleList 组件的滚动到底部 手动触发 自动滚动
|
// 这里有必要调用一下 BubbleList 组件的滚动到底部 手动触发 自动滚动
|
||||||
@ -154,6 +243,22 @@ async function startSSE() {
|
|||||||
|
|
||||||
console.log('res', res);
|
console.log('res', res);
|
||||||
|
|
||||||
|
for await (const chunk of res) {
|
||||||
|
console.log('chunk', chunk);
|
||||||
|
const resError = checkJsonSerializable(chunk.result);
|
||||||
|
console.log('resError', resError);
|
||||||
|
|
||||||
|
// 判断 json 序列化失败的情况
|
||||||
|
if (chunk.result && typeof chunk.result !== 'object') {
|
||||||
|
console.log('json 序列化失败');
|
||||||
|
handleDataChunk(chunk.result as string);
|
||||||
|
}
|
||||||
|
else if (chunk.result) {
|
||||||
|
const strChunk = JSON.stringify(chunk.result);
|
||||||
|
handleDataChunk(strChunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// const response = await fetch(BASE_URL, {
|
// const response = await fetch(BASE_URL, {
|
||||||
// method: 'POST',
|
// method: 'POST',
|
||||||
// headers: {
|
// headers: {
|
||||||
@ -190,7 +295,7 @@ function addMessage(message: string, isUser: boolean) {
|
|||||||
avatar: isUser
|
avatar: isUser
|
||||||
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
|
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
|
||||||
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
||||||
avatarSize: '48px',
|
avatarSize: '32px',
|
||||||
role: isUser ? 'user' : 'system',
|
role: isUser ? 'user' : 'system',
|
||||||
placement: isUser ? 'end' : 'start',
|
placement: isUser ? 'end' : 'start',
|
||||||
isMarkdown: !isUser,
|
isMarkdown: !isUser,
|
||||||
@ -211,6 +316,26 @@ function addMessage(message: string, isUser: boolean) {
|
|||||||
function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
|
function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
|
||||||
console.log('value', payload.value, 'status', payload.status);
|
console.log('value', payload.value, 'status', payload.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDeleteCard(_item: FilesCardProps, index: number) {
|
||||||
|
filesStore.deleteFileByIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filesStore.filesList.length,
|
||||||
|
(val) => {
|
||||||
|
if (val > 0) {
|
||||||
|
nextTick(() => {
|
||||||
|
senderRef.value.openHeader();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nextTick(() => {
|
||||||
|
senderRef.value.closeHeader();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -243,32 +368,46 @@ function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
|
|||||||
variant="updown"
|
variant="updown"
|
||||||
clearable
|
clearable
|
||||||
allow-speech
|
allow-speech
|
||||||
|
:loading="isLoading"
|
||||||
@submit="startSSE"
|
@submit="startSSE"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #header>
|
||||||
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
<div class="sender-header p-12px pt-6px pb-0px">
|
||||||
<div
|
<Attachments
|
||||||
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]"
|
:items="filesStore.filesList"
|
||||||
|
:hide-upload="true"
|
||||||
|
@delete-card="handleDeleteCard"
|
||||||
>
|
>
|
||||||
<el-icon>
|
<template #prev-button="{ show, onScrollLeft }">
|
||||||
<Paperclip />
|
<div
|
||||||
</el-icon>
|
v-if="show"
|
||||||
</div>
|
class="prev-next-btn left-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
|
||||||
|
@click="onScrollLeft"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<ArrowLeftBold />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #next-button="{ show, onScrollRight }">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="prev-next-btn right-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
|
||||||
|
@click="onScrollRight"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<ArrowRightBold />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Attachments>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #prefix>
|
||||||
<template #action-list>
|
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
||||||
<div class="footer-container">
|
<FilesSelect />
|
||||||
<el-button v-if="!isLoading" type="danger" circle @click="senderRef.submit()">
|
<ModelSelect />
|
||||||
<el-icon>
|
|
||||||
<Position />
|
|
||||||
</el-icon>
|
|
||||||
</el-button>
|
|
||||||
<el-button v-if="isLoading" type="primary" @click="cancel">
|
|
||||||
<el-icon class="is-loading">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sender>
|
</Sender>
|
||||||
|
|||||||
23
src/stores/modules/files.ts
Normal file
23
src/stores/modules/files.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
||||||
|
// 对话聊天的文件上传列表
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useFilesStore = defineStore('files', () => {
|
||||||
|
const filesList = ref<FilesCardProps & { file: File }[]>([]);
|
||||||
|
|
||||||
|
// 设置文件列表
|
||||||
|
const setFilesList = (list: FilesCardProps & { file: File }[]) => {
|
||||||
|
filesList.value = list;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据索引删除 文件
|
||||||
|
const deleteFileByIndex = (index: number) => {
|
||||||
|
filesList.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filesList,
|
||||||
|
setFilesList,
|
||||||
|
deleteFileByIndex,
|
||||||
|
};
|
||||||
|
});
|
||||||
36
src/styles/elx.scss
Normal file
36
src/styles/elx.scss
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// 覆盖一些 elx 组件样式
|
||||||
|
|
||||||
|
// attachments 图片组件的 1.2.0 版本样式 bug
|
||||||
|
.image-preview-container {
|
||||||
|
flex: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elx-files-card-content {
|
||||||
|
gap: 8px;
|
||||||
|
.elx-files-card-description {
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-next-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% + 3px);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elx-attachments-file-card-wrap {
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elx-attachments-card {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏 elx-sender-header 组件的底部边框
|
||||||
|
.el-sender-header {
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2,6 +2,7 @@
|
|||||||
@use './btn-style.scss';
|
@use './btn-style.scss';
|
||||||
@use 'reset-css';
|
@use 'reset-css';
|
||||||
@use './element-plus.scss';
|
@use './element-plus.scss';
|
||||||
|
@use './elx.scss';
|
||||||
|
|
||||||
body{
|
body{
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
6
types/components.d.ts
vendored
6
types/components.d.ts
vendored
@ -3,7 +3,7 @@
|
|||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {};
|
||||||
|
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
@ -24,8 +24,8 @@ declare module 'vue' {
|
|||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
|
||||||
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
||||||
'Index copy': typeof import('./../src/components/Popover/index copy.vue')['default']
|
|
||||||
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
|
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
|
||||||
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
|
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
|
||||||
Popover: typeof import('./../src/components/Popover/index.vue')['default']
|
Popover: typeof import('./../src/components/Popover/index.vue')['default']
|
||||||
@ -36,7 +36,7 @@ declare module 'vue' {
|
|||||||
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
|
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
|
||||||
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
|
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
|
||||||
}
|
}
|
||||||
export interface ComponentCustomProperties {
|
export interface GlobalDirectives {
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user