feat: 新增会话管理删除,编辑接口

This commit is contained in:
何嘉悦 2025-05-26 07:42:56 +08:00
parent ebbaca9f35
commit b00380d617
11 changed files with 361 additions and 102 deletions

View File

@ -4,12 +4,20 @@ import type {
// CreateSessionVO, // CreateSessionVO,
GetSessionListParams, GetSessionListParams,
} from './types'; } from './types';
import { get, post } from '@/utils/request'; import { del, get, post, put } from '@/utils/request';
export function getSessionList(params: GetSessionListParams) { export function get_session_list(params: GetSessionListParams) {
return get<ChatSessionVo[]>('/system/session/list', params); return get<ChatSessionVo[]>('/system/session/list', params);
} }
export function createSession(data: CreateSessionDTO) { export function create_session(data: CreateSessionDTO) {
return post('/system/session', data); return post('/system/session', data);
} }
export function update_session(data: ChatSessionVo) {
return put('/system/session', data);
}
export function delete_session(ids: string[]) {
return del(`/system/session/${ids}`);
}

View File

@ -1,3 +1,5 @@
import type { Component } from 'vue';
export interface GetSessionListParams { export interface GetSessionListParams {
/** /**
* *
@ -68,7 +70,8 @@ export interface ChatSessionVo {
/** /**
* *
*/ */
id?: number; // id?: number
id?: string;
/** /**
* *
*/ */
@ -85,6 +88,10 @@ export interface ChatSessionVo {
* id * id
*/ */
userId?: number; userId?: number;
/**
*
*/
prefixIcon?: Component;
} }
/** /**
@ -106,7 +113,8 @@ export interface CreateSessionDTO {
/** /**
* *
*/ */
id?: number; // id?: number;
id?: string;
/** /**
* *
*/ */
@ -114,11 +122,11 @@ export interface CreateSessionDTO {
/** /**
* *
*/ */
remark: string; remark?: string;
/** /**
* *
*/ */
sessionContent: string; sessionContent?: string;
/** /**
* *
*/ */

View File

@ -4,6 +4,7 @@ import type { ConversationItem, GroupableOptions } from 'vue-element-plus-x/type
import type { ChatSessionVo } from '@/api/session/types'; import type { ChatSessionVo } from '@/api/session/types';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import logo from '@/assets/images/logo.png'; import logo from '@/assets/images/logo.png';
import SvgIcon from '@/components/SvgIcon/index.vue';
import Collapse from '@/layouts/components/Header/components/Collapse.vue'; import Collapse from '@/layouts/components/Header/components/Collapse.vue';
import { useDesignStore } from '@/stores'; import { useDesignStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session'; import { useSessionStore } from '@/stores/modules/session';
@ -13,19 +14,10 @@ const router = useRouter();
const designStore = useDesignStore(); const designStore = useDesignStore();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
// const sessionId = computed(() => Number(route.params?.id)); const sessionId = computed(() => route.params?.id);
const conversationsList = computed(() => sessionStore.sessionList); const conversationsList = computed(() => sessionStore.sessionList);
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
/* 创建会话 开始 */ const active = ref();
function handleCreatChat() {
console.log('创建新会话');
// ,
sessionStore.createSessionBtn();
}
/* 创建会话 结束 */
/* 会话组件 开始 */
const active = computed<string>(() => (route.params?.id as string) ?? '');
// //
const customGroupOptions: GroupableOptions = { const customGroupOptions: GroupableOptions = {
@ -38,8 +30,27 @@ const customGroupOptions: GroupableOptions = {
}, },
}; };
onMounted(async () => {
//
await sessionStore.requestSessionList();
//
if (conversationsList.value.length > 0 && sessionId.value) {
active.value = sessionId.value;
// ID ()
// sessionStore.currentSession = sessionStore.getSessionById(sessionId.value);
}
});
//
function handleCreatChat() {
// ,
sessionStore.createSessionBtn();
}
//
function handleChange(item: ConversationItem<ChatSessionVo>) { function handleChange(item: ConversationItem<ChatSessionVo>) {
console.log('点击了会话 item', item); sessionStore.setCurrentSession(item);
active.value = item.id;
router.replace({ router.replace({
name: 'chatWithId', name: 'chatWithId',
params: { params: {
@ -47,11 +58,83 @@ function handleChange(item: ConversationItem<ChatSessionVo>) {
}, },
}); });
} }
/* 会话组件 结束 */
watchEffect(() => { //
console.log('active', active.value, '>>>'); async function handleLoadMore() {
}); if (!sessionStore.hasMore)
return; //
await sessionStore.loadMoreSessions();
}
//
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
switch (command) {
case 'delete':
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger',
cancelButtonClass: 'el-button--info',
roundButton: true,
})
.then(() => {
//
sessionStore.deleteSessions([item.id!]);
nextTick(() => {
if (item.id === active.value) {
//
sessionStore.createSessionBtn();
}
});
})
.catch(() => {
//
});
break;
case 'rename':
ElMessageBox.prompt('', '编辑对话名称', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputErrorMessage: '请输入对话名称',
confirmButtonClass: 'el-button--primary',
cancelButtonClass: 'el-button--info',
roundButton: true,
inputValue: item.sessionTitle, //
inputValidator: (value) => {
if (!value) {
return false;
}
return true;
},
}).then(({ value }) => {
sessionStore
.updateSession({
id: item.id!,
sessionTitle: value,
sessionContent: item.sessionContent,
})
.then(() => {
ElMessage({
type: 'success',
message: '修改成功',
});
nextTick(() => {
//
if (sessionStore.currentSession?.id === item.id) {
sessionStore.setCurrentSession({
...item,
sessionTitle: value,
});
}
});
});
});
break;
default:
break;
}
}
</script> </script>
<template> <template>
@ -94,7 +177,24 @@ watchEffect(() => {
:tooltip-offset="35" :tooltip-offset="35"
show-built-in-menu show-built-in-menu
:groupable="customGroupOptions" :groupable="customGroupOptions"
row-key="key" row-key="id"
label-key="sessionTitle"
:load-more="handleLoadMore"
:load-more-loading="loadMoreLoading"
:items-style="{
marginLeft: '8px',
userSelect: 'none',
borderRadius: '16px',
}"
:items-active-style="{
backgroundColor: '#fff',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
color: 'rgba(0, 0, 0, 0.85)',
}"
:items-hover-style="{
backgroundColor: 'rgba(0, 0, 0, 0.04)',
}"
@menu-command="handleMenuCommand"
@change="handleChange" @change="handleChange"
/> />
</div> </div>
@ -208,6 +308,12 @@ watchEffect(() => {
// - // -
.conversations-wrap { .conversations-wrap {
height: calc(100vh - 110px); height: calc(100vh - 110px);
.label {
display: flex;
align-items: center;
height: 100%;
}
} }
} }
} }

View File

@ -1,9 +1,21 @@
<!-- 添加新会话按钮 --> <!-- 添加新会话按钮 -->
<script setup lang="ts"></script> <script setup lang="ts">
import { useSessionStore } from '@/stores/modules/session';
const sessionStore = useSessionStore();
/* 创建会话 开始 */
function handleCreatChat() {
// ,
sessionStore.createSessionBtn();
}
/* 创建会话 结束 */
</script>
<template> <template>
<div <div
class="create-chat-container flex-center flex-none p-6px pl-8px pr-8px c-#0057ff b-#0057ff b-rounded-12px border-1px hover:bg-#0057ff hover:c-#fff hover:b-#fff hover:cursor-pointer border-solid" class="create-chat-container flex-center flex-none p-6px pl-8px pr-8px c-#0057ff b-#0057ff b-rounded-12px border-1px hover:bg-#0057ff hover:c-#fff hover:b-#fff hover:cursor-pointer border-solid"
@click="handleCreatChat"
> >
<el-icon size="12" class="flex-center flex-none w-14px h-14px"> <el-icon size="12" class="flex-center flex-none w-14px h-14px">
<Plus /> <Plus />

View File

@ -1,6 +1,11 @@
<!-- 标题编辑 --> <!-- 标题编辑 -->
<script setup lang="ts"> <script setup lang="ts">
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import { useSessionStore } from '@/stores/modules/session';
const sessionStore = useSessionStore();
const currentSession = computed(() => sessionStore.currentSession);
function handleClickTitle() { function handleClickTitle() {
ElMessageBox.prompt('', '编辑对话名称', { ElMessageBox.prompt('', '编辑对话名称', {
@ -10,6 +15,7 @@ function handleClickTitle() {
confirmButtonClass: 'el-button--primary', confirmButtonClass: 'el-button--primary',
cancelButtonClass: 'el-button--info', cancelButtonClass: 'el-button--info',
roundButton: true, roundButton: true,
inputValue: currentSession.value?.sessionTitle,
inputValidator: (value) => { inputValidator: (value) => {
if (!value) { if (!value) {
return false; return false;
@ -18,10 +24,25 @@ function handleClickTitle() {
}, },
}) })
.then(({ value }) => { .then(({ value }) => {
ElMessage({ sessionStore
type: 'success', .updateSession({
message: `修改成功:${value}`, id: currentSession.value!.id,
}); sessionTitle: value,
sessionContent: currentSession.value!.sessionContent,
})
.then(() => {
ElMessage({
type: 'success',
message: '修改成功',
});
nextTick(() => {
//
sessionStore.setCurrentSession({
...currentSession.value,
sessionTitle: value,
});
});
});
}) })
.catch(() => { .catch(() => {
// ElMessage({ // ElMessage({
@ -33,14 +54,14 @@ function handleClickTitle() {
</script> </script>
<template> <template>
<div class="w-full h-full flex flex-col justify-center"> <div v-if="currentSession" class="w-full h-full flex flex-col justify-center">
<div class="box-border mr-20px"> <div class="box-border mr-20px">
<div <div
class="title-editing-container p-4px w-fit max-w-full flex items-center justify-start cursor-pointer select-none hover:bg-[rgba(0,0,0,.04)] cursor-pointer rounded-md font-size-14px" class="title-editing-container p-4px w-fit max-w-full flex items-center justify-start cursor-pointer select-none hover:bg-[rgba(0,0,0,.04)] cursor-pointer rounded-md font-size-14px"
@click="handleClickTitle" @click="handleClickTitle"
> >
<div class="text-overflow select-none pr-8px"> <div class="text-overflow select-none pr-8px">
标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑 {{ currentSession.sessionTitle }}
</div> </div>
<SvgIcon name="draft-line" size="14" class="flex-none c-gray-500" /> <SvgIcon name="draft-line" size="14" class="flex-none c-gray-500" />
</div> </div>

View File

@ -4,7 +4,7 @@ import type { ChatSessionVo } from '@/api/session/types';
// import { useUserStore } from '@/stores'; // import { useUserStore } from '@/stores';
import { Conversations } from 'vue-element-plus-x'; import { Conversations } from 'vue-element-plus-x';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
// import { getSessionList } from '@/api'; // import { get_session_list } from '@/api';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -14,17 +14,17 @@ const items = ref<ConversationItem<ChatSessionVo>[]>([]);
function handleChange(item: ConversationItem<ChatSessionVo>) { function handleChange(item: ConversationItem<ChatSessionVo>) {
console.log(item); console.log(item);
router.replace({ // router.replace({
name: 'chat', // name: 'chat',
params: { // params: {
id: item.id, // id: item.id,
}, // },
}); // });
} }
// async function getSessions() { // async function getSessions() {
// try { // try {
// const res = await getSessionList({ // const res = await get_session_list({
// userId: userStore.userInfo?.userId as number, // userId: userStore.userInfo?.userId as number,
// }); // });
// console.log(res); // console.log(res);

View File

@ -2,33 +2,10 @@
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import ChatDefaul from '@/pages/chat/layouts/chatDefaul/index.vue'; import ChatDefaul from '@/pages/chat/layouts/chatDefaul/index.vue';
import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue'; import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue';
import { useChatStore } from '@/stores/modules/chat';
const route = useRoute(); const route = useRoute();
const chatStore = useChatStore();
const senderValue = ref(''); const sessionId = computed(() => Number(route.params?.id));
const chatId = computed(() => Number(route.params?.id));
if (chatId.value) {
chatStore.requestChatList(chatId.value);
}
watch(
() => route.params?.id,
(_id_) => {
if (_id_) {
chatStore.requestChatList(Number(_id_));
const v = localStorage.getItem('chatContent');
if (v) {
senderValue.value = v;
localStorage.removeItem('chatContent');
//
console.log('发送消息 v', v);
}
}
},
{ immediate: true, deep: true },
);
// const a = JSON.parse(`{"id":"chatcmpl-BVD1f4snw4KHOCKIgu4JOMoZbOwRh","object":"chat.completion.chunk","created":1746777939,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_ded0d14823","choices":[{"delta":{"content":"","role":"assistant"},"logprobs":null,"finish_reason":null,"index":0}],"usage":null}`) // const a = JSON.parse(`{"id":"chatcmpl-BVD1f4snw4KHOCKIgu4JOMoZbOwRh","object":"chat.completion.chunk","created":1746777939,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_ded0d14823","choices":[{"delta":{"content":"","role":"assistant"},"logprobs":null,"finish_reason":null,"index":0}],"usage":null}`)
// console.log(a); // console.log(a);
@ -37,9 +14,9 @@ watch(
<template> <template>
<div class="chat-container"> <div class="chat-container">
<!-- 默认聊天页面 --> <!-- 默认聊天页面 -->
<ChatDefaul v-if="!chatId" /> <ChatDefaul v-if="!sessionId" />
<!-- 带id的聊天页面 --> <!-- 带id的聊天页面 -->
<ChatWithId v-else :chat-id="chatId" /> <ChatWithId v-else />
</div> </div>
</template> </template>

View File

@ -1,4 +1,4 @@
<!-- --> <!-- 默认消息列表页 -->
<script setup lang="ts"> <script setup lang="ts">
import WelecomeText from '@/components/WelecomeText/index.vue'; import WelecomeText from '@/components/WelecomeText/index.vue';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';

View File

@ -1,18 +1,37 @@
<!-- 每个回话对应的聊天内容 --> <!-- 每个回话对应的聊天内容 -->
<script setup lang="ts"> <script setup lang="ts">
interface ChatWithIdProps { import { useRoute } from 'vue-router';
chatId?: number; import { useChatStore } from '@/stores/modules/chat';
}
const props = withDefaults(defineProps<ChatWithIdProps>(), { interface ChatWithIdProps {}
chatId: undefined,
}); const props = withDefaults(defineProps<ChatWithIdProps>(), {});
const route = useRoute();
const chatStore = useChatStore();
const senderValue = ref('');
console.log('props ==> ', props); console.log('props ==> ', props);
watch(
() => route.params?.id,
(_id_) => {
if (_id_) {
chatStore.requestChatList(Number(_id_));
const v = localStorage.getItem('chatContent');
if (v) {
senderValue.value = v;
localStorage.removeItem('chatContent');
//
console.log('发送消息 v', v);
}
}
},
{ immediate: true, deep: true },
);
</script> </script>
<template> <template>
<div>新建对话</div> <div>新建对话当前页面对话id: {{ route.params?.id }}</div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -1,69 +1,173 @@
import type { CreateSessionDTO } from '@/api/session/types'; import type { ChatSessionVo, CreateSessionDTO, GetSessionListParams } from '@/api/session/types';
import { ChatLineRound } from '@element-plus/icons-vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { markRaw } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { createSession, getSessionList } from '@/api/session'; import { create_session, delete_session, get_session_list, update_session } from '@/api/session';
import { useUserStore } from './user'; import { useUserStore } from './user';
export const useSessionStore = defineStore('session', () => { export const useSessionStore = defineStore('session', () => {
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const sessionList = ref<CreateSessionDTO[]>([]); // 当前选中的会话信息
const currentSession = ref<ChatSessionVo | null>(null);
// 设置当前会话
const setCurrentSession = (session: ChatSessionVo | null) => {
currentSession.value = session;
};
// 单独点击创建新对话按钮 // 会话列表核心状态
const sessionList = ref<ChatSessionVo[]>([]); // 会话数据列表
const currentPage = ref(1); // 当前页码从1开始
const pageSize = ref(20); // 每页显示数量
const hasMore = ref(true); // 是否还有更多数据
const isLoading = ref(false); // 全局加载状态(初始加载/刷新)
const isLoadingMore = ref(false); // 加载更多状态(区分初始加载)
// 创建新对话(按钮点击)
const createSessionBtn = async () => { const createSessionBtn = async () => {
try { try {
// 直接回到首页
router.replace({ name: 'chat' }); router.replace({ name: 'chat' });
currentSession.value = null;
} }
catch (error) { catch (error) {
console.error('createSessionBtn:', error); console.error('createSessionBtn错误:', error);
} }
}; };
// 发送消息后,创建对话 // 发送消息后创建新会话(并插入列表顶部)
const createSessionList = async (data: CreateSessionDTO) => { const createSessionList = async (data: Omit<CreateSessionDTO, 'id'>) => {
try { try {
const res = await createSession(data); const res = await create_session(data);
localStorage.setItem('chatContent', data.sessionContent);
console.log('并插入列表顶部 res', res);
// 构造新会话对象(根据接口实际返回调整)
const newSession: ChatSessionVo = {
...data,
sessionTitle: data.sessionTitle || '新对话',
prefixIcon: markRaw(ChatLineRound),
};
// 插入到列表顶部(触发视图更新)
sessionList.value.unshift(newSession);
// 跳转聊天页
router.replace({ router.replace({
name: 'chatWithId', name: 'chatWithId',
params: { params: { id: `${res.data}` },
id: `${res.data}`,
},
}); });
// 重置分页状态(新增会话后,后续加载从第一页重新开始)
currentPage.value = 1;
hasMore.value = true;
} }
catch (error) { catch (error) {
console.error('createSession:', error); console.error('createSessionList错误:', error);
} }
}; };
// 获取会话列表 // 获取会话列表(核心分页方法)
const requestSessionList = async () => { const requestSessionList = async (page: number = currentPage.value) => {
if ((page > 1 && !hasMore.value) || isLoading.value || isLoadingMore.value)
return;
isLoading.value = page === 1; // 第一页时标记为全局加载
isLoadingMore.value = page > 1; // 非第一页时标记为加载更多
try { try {
const res = await getSessionList({ const params: GetSessionListParams = {
userId: userStore.userInfo?.userId as number, userId: userStore.userInfo?.userId as number,
}); pageNum: page,
if (res.rows) { pageSize: pageSize.value,
sessionList.value = (res.rows || []).map(item => ({ };
...item,
// 提供默认值 const res = await get_session_list(params);
remark: item.remark || '',
sessionContent: item.sessionContent || '', // 第一页:覆盖原有数据;非第一页:合并新数据
sessionTitle: item.sessionTitle || '', if (page === 1) {
userId: item.userId || -1, sessionList.value
})); = res.rows?.map((item: ChatSessionVo) => {
return {
...item,
prefixIcon: markRaw(ChatLineRound),
};
}) || [];
} }
else {
// 去重处理(避免接口返回重复数据)
const existingIds = new Set(sessionList.value.map(item => item.id));
const newRows = (
res.rows?.map((item: ChatSessionVo) => {
return {
...item,
prefixIcon: markRaw(ChatLineRound),
};
}) || []
).filter(item => !existingIds.has(item.id));
sessionList.value.push(...newRows);
}
// 判断是否还有更多数据(当前页数据量 < pageSize 则无更多)
hasMore.value = (res.rows?.length || 0) === pageSize.value;
currentPage.value = page;
} }
catch (error) { catch (error) {
console.error('getSessionList:', error); console.error('requestSessionList错误:', error);
}
finally {
isLoading.value = false;
isLoadingMore.value = false;
}
};
// 加载更多会话(供组件调用)
const loadMoreSessions = async () => {
if (hasMore.value)
await requestSessionList(currentPage.value + 1);
};
// 更新会话(供组件调用)
const updateSession = async (item: ChatSessionVo) => {
try {
await update_session(item);
await requestSessionList(currentPage.value);
}
catch (error) {
console.error('updateSession错误:', error);
}
};
// 删除会话(供组件调用)
const deleteSessions = async (ids: string[]) => {
try {
await delete_session(ids);
await requestSessionList(currentPage.value);
}
catch (error) {
console.error('deleteSessions错误:', error);
} }
}; };
return { return {
// 当前选中的会话
currentSession,
// 设置当前会话
setCurrentSession,
// 列表状态
sessionList, sessionList,
currentPage,
pageSize,
hasMore,
isLoading,
isLoadingMore,
// 列表方法
createSessionBtn, createSessionBtn,
createSessionList, createSessionList,
requestSessionList, requestSessionList,
loadMoreSessions,
updateSession,
deleteSessions,
}; };
}); });

View File

@ -50,4 +50,8 @@ export const post = request.post;
export const get = request.get; export const get = request.get;
export const put = request.put;
export const del = request.delete;
export default request; export default request;