feat:【ele】【ai】优化 chat 界面

This commit is contained in:
YunaiV
2025-11-19 11:47:26 +08:00
parent 07cb707e82
commit 42f30230f2
2 changed files with 210 additions and 157 deletions

View File

@@ -673,7 +673,8 @@ onMounted(async () => {
{{ conversationInProgress ? '进行中' : '发送' }} {{ conversationInProgress ? '进行中' : '发送' }}
</ElButton> </ElButton>
<ElButton <ElButton
type="danger" type="primary"
:danger="true"
@click="stopStream()" @click="stopStream()"
v-if="conversationInProgress === true" v-if="conversationInProgress === true"
> >

View File

@@ -166,87 +166,119 @@ async function getConversationGroupByCreateTime(
return groupMap; return groupMap;
} }
/** 新建对话 */ async function createConversation() {
async function handleConversationCreate() { // 1. 新建对话
// 1. 创建对话 const conversationId = await createChatConversationMy(
const conversationId = await createChatConversationMy({ {} as unknown as AiChatConversationApi.ChatConversation,
roleId: undefined, );
} as unknown as AiChatConversationApi.ChatConversation); // 2. 获取对话内容
// 2. 刷新列表
await getChatConversationList(); await getChatConversationList();
// 3. 选中对话
// 3. 回调 await handleConversationClick(conversationId);
emits('onConversationCreate', conversationId); // 4. 回调
emits('onConversationCreate');
} }
/** 清空未置顶的对话 */ /** 清空未置顶的对话 */
async function handleConversationClear() { async function handleClearConversation() {
await confirm({ try {
title: '清空置顶的对话', await confirm('确认后对话会全部清空置顶的对话除外。');
content: h('div', {}, [ await deleteChatConversationMyByUnpinned();
h('p', '确认清空未置顶的对话吗?'), ElMessage.success($t('ui.actionMessage.operationSuccess'));
h('p', '清空后,未置顶的对话将被删除,无法恢复!'), // 清空对话、对话内容
]), activeConversationId.value = null;
// 获取对话列表
await getChatConversationList();
// 回调 方法
emits('onConversationClear');
} catch {}
}
/** 删除聊天对话 */
async function deleteChatConversation(
conversation: AiChatConversationApi.ChatConversation,
) {
try {
// 删除的二次确认
await confirm(`是否确认删除对话 - ${conversation.title}?`);
// 发起删除
await deleteChatConversationMy(conversation.id);
ElMessage.success('对话已删除');
// 刷新列表
await getChatConversationList();
// 回调
emits('onConversationDelete', conversation);
} catch {}
}
/** 对话置顶 */
async function handleTop(conversation: AiChatConversationApi.ChatConversation) {
// 更新对话置顶
conversation.pinned = !conversation.pinned;
await updateChatConversationMy(conversation);
// 刷新对话
await getChatConversationList();
}
/** 修改对话的标题 */
async function updateConversationTitle(
conversation: AiChatConversationApi.ChatConversation,
) {
// 1. 二次确认
await prompt({
async beforeClose(scope) {
if (scope.isConfirm) {
if (scope.value) {
try {
// 2. 发起修改
await updateChatConversationMy({
id: conversation.id,
title: scope.value,
} as AiChatConversationApi.ChatConversation);
ElMessage.success('重命名成功');
// 3. 刷新列表
await getChatConversationList();
// 4. 过滤当前切换的
const filterConversationList = conversationList.value.filter(
(item) => {
return item.id === conversation.id;
},
);
if (
filterConversationList.length > 0 &&
filterConversationList[0] && // tip避免切换对话
activeConversationId.value === filterConversationList[0].id!
) {
emits('onConversationClick', filterConversationList[0]);
}
} catch {
return false;
}
} else {
ElMessage.error('请输入标题');
return false;
}
}
},
component: () => {
return h(ElInput, {
placeholder: '请输入标题',
clearable: true,
modelValue: conversation.title,
});
},
content: '请输入标题',
title: '修改标题',
modelPropName: 'modelValue',
}); });
// 清空
await deleteChatConversationMyByUnpinned();
// 刷新列表
await getChatConversationList();
// 回调
emits('onConversationClear');
} }
/** 删除对话 */ // ============ 角色仓库 ============
async function handleConversationDelete(id: number) {
await confirm({
title: '删除对话',
content: h('div', {}, [
h('p', '确认删除该对话吗?'),
h('p', '删除后,该对话将被删除,无法恢复!'),
]),
});
// 删除
await deleteChatConversationMy(id);
// 刷新列表
await getChatConversationList();
// 回调
emits('onConversationDelete', id);
}
/** 置顶对话 */ /** 角色仓库抽屉 */
async function handleConversationPin(conversation: any) { const handleRoleRepository = async () => {
// 更新
await updateChatConversationMy({
id: conversation.id,
pinned: !conversation.pinned,
} as AiChatConversationApi.ChatConversation);
// 刷新列表
await getChatConversationList();
}
/** 编辑对话 */
async function handleConversationEdit(conversation: any) {
const title = await prompt({
title: '编辑对话',
content: '请输入对话标题',
defaultValue: conversation.title,
});
// 更新
await updateChatConversationMy({
id: conversation.id,
title,
} as AiChatConversationApi.ChatConversation);
// 刷新列表
await getChatConversationList();
// 提示
ElMessage.success($t('ui.actionMessage.operationSuccess'));
}
/** 打开角色仓库 */
async function handleRoleRepositoryOpen() {
drawerApi.open(); drawerApi.open();
} };
/** 监听 activeId 变化 */ /** 监听 activeId 变化 */
watch( watch(
@@ -260,138 +292,158 @@ const { activeId } = toRefs(props);
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
// 获取对话列表 // 获取 对话列表
await getChatConversationList(); await getChatConversationList();
// 设置选中的对话 // 默认选中
if (activeId.value) { if (props.activeId) {
activeConversationId.value = activeId.value; activeConversationId.value = props.activeId;
} else {
// 首次默认选中第一个
if (conversationList.value.length > 0 && conversationList.value[0]) {
activeConversationId.value = conversationList.value[0].id;
// 回调 onConversationClick
emits('onConversationClick', conversationList.value[0]);
}
} }
}); });
defineExpose({ getChatConversationList }); defineExpose({ createConversation });
</script> </script>
<template> <template>
<ElAside <ElAside
class="bg-card relative flex h-full flex-col overflow-hidden border-r border-gray-200"
width="280px" width="280px"
class="relative flex h-full flex-col justify-between overflow-hidden p-4"
> >
<Drawer /> <Drawer />
<!-- 头部 --> <!-- 左顶部对话 -->
<div class="flex flex-col p-4"> <div class="flex h-full flex-col">
<div class="mb-4 flex flex-row items-center justify-between"> <ElButton class="h-9 w-full" type="primary" @click="createConversation">
<div class="text-lg font-bold">对话</div> <IconifyIcon icon="lucide:plus" class="mr-1" />
<div class="flex flex-row"> 新建对话
<ElButton </ElButton>
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
text
@click="handleConversationCreate"
>
<IconifyIcon icon="lucide:plus" />
</ElButton>
<ElButton
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
text
@click="handleConversationClear"
>
<IconifyIcon icon="lucide:trash" />
</ElButton>
<ElButton
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
text
@click="handleRoleRepositoryOpen"
>
<IconifyIcon icon="lucide:user" />
</ElButton>
</div>
</div>
<ElInput <ElInput
v-model="searchName" v-model="searchName"
placeholder="搜索对话" size="large"
@keyup.enter="searchConversation" class="search-input mt-4"
placeholder="搜索历史记录"
@keyup="searchConversation"
> >
<template #suffix> <template #prefix>
<IconifyIcon icon="lucide:search" /> <IconifyIcon icon="lucide:search" />
</template> </template>
</ElInput> </ElInput>
</div>
<!-- 对话列表 --> <!-- 左中间对话列表 -->
<div class="flex-1 overflow-y-auto px-4"> <div class="mt-2 flex-1 overflow-auto">
<div v-if="loading" class="flex h-full items-center justify-center"> <!-- 情况一加载中 -->
<div class="text-sm text-gray-400">加载中...</div> <ElEmpty v-if="loading" description="." v-loading="loading" />
</div>
<div v-else-if="Object.keys(conversationMap).length === 0"> <!-- 情况二按照 group 分组 -->
<ElEmpty description="暂无对话" />
</div>
<div v-else>
<div <div
v-for="(conversations, groupName) in conversationMap" v-for="conversationKey in Object.keys(conversationMap)"
:key="groupName" :key="conversationKey"
> >
<div <div
v-if="conversations.length > 0" v-if="conversationMap[conversationKey].length > 0"
class="mb-2 mt-4 text-xs text-gray-400" class="classify-title pt-2"
> >
{{ groupName }} <p class="mx-1">
{{ conversationKey }}
</p>
</div> </div>
<div <div
v-for="conversation in conversations" v-for="conversation in conversationMap[conversationKey]"
:key="conversation.id" :key="conversation.id"
class="group relative mb-2 cursor-pointer rounded-lg p-2 transition-all hover:bg-gray-100"
:class="{
'bg-gray-100': activeConversationId === conversation.id,
}"
@click="handleConversationClick(conversation.id)" @click="handleConversationClick(conversation.id)"
@mouseenter="hoverConversationId = conversation.id" @mouseover="hoverConversationId = conversation.id"
@mouseleave="hoverConversationId = null" @mouseout="hoverConversationId = null"
class="mt-1"
> >
<div class="flex items-center"> <div
<ElAvatar class="mb-2 flex cursor-pointer flex-row items-center justify-between rounded-lg px-2 leading-10 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
v-if="conversation.roleAvatar" :class="[
:src="conversation.roleAvatar" conversation.id === activeConversationId
:size="28" ? 'bg-primary/10 dark:bg-primary/20'
/> : '',
<SvgGptIcon v-else class="size-7" /> ]"
<div class="ml-2 flex-1 overflow-hidden"> >
<div class="truncate text-sm font-medium"> <div class="flex items-center">
<ElAvatar
v-if="conversation.roleAvatar"
:src="conversation.roleAvatar"
:size="28"
/>
<SvgGptIcon v-else class="size-6" />
<span
class="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap p-2 text-sm font-normal"
>
{{ conversation.title }} {{ conversation.title }}
</div> </span>
</div> </div>
<div <div
v-if="hoverConversationId === conversation.id" v-show="hoverConversationId === conversation.id"
class="flex flex-row" class="relative right-0.5 flex items-center text-gray-400"
> >
<ElButton <ElButton
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100" class="mr-0 px-1"
text link
@click.stop="handleConversationPin(conversation)" @click.stop="handleTop(conversation)"
> >
<IconifyIcon <IconifyIcon
:icon=" v-if="!conversation.pinned"
conversation.pinned ? 'lucide:pin-off' : 'lucide:pin' icon="lucide:arrow-up-to-line"
" />
<IconifyIcon
v-if="conversation.pinned"
icon="lucide:arrow-down-from-line"
/> />
</ElButton> </ElButton>
<ElButton <ElButton
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100" class="mr-0 px-1"
text link
@click.stop="handleConversationEdit(conversation)" @click.stop="updateConversationTitle(conversation)"
> >
<IconifyIcon icon="lucide:edit" /> <IconifyIcon icon="lucide:edit" />
</ElButton> </ElButton>
<ElButton <ElButton
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100" class="mr-0 px-1"
text link
@click.stop="handleConversationDelete(conversation.id)" @click.stop="deleteChatConversation(conversation)"
> >
<IconifyIcon icon="lucide:trash" /> <IconifyIcon icon="lucide:trash-2" />
</ElButton> </ElButton>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 底部占位 -->
<div class="h-12 w-full"></div>
</div>
<!-- 左底部工具栏 -->
<div
class="bg-card absolute bottom-1 left-0 right-0 mb-4 flex items-center justify-between px-5 leading-9 text-gray-400 shadow-sm"
>
<div
class="flex cursor-pointer items-center text-gray-400"
@click="handleRoleRepository"
>
<IconifyIcon icon="lucide:user" />
<span class="ml-1">角色仓库</span>
</div>
<div
class="flex cursor-pointer items-center text-gray-400"
@click="handleClearConversation"
>
<IconifyIcon icon="lucide:trash" />
<span class="ml-1">清空未置顶对话</span>
</div>
</div> </div>
</ElAside> </ElAside>
</template> </template>