feat: 新增未登录情况下的处理和添加路由动画

This commit is contained in:
何嘉悦 2025-05-27 01:08:07 +08:00
parent edbb5ec5ba
commit d15519e18f
13 changed files with 114 additions and 17 deletions

View File

@ -13,4 +13,6 @@ VITE_WEB_BASE_API = '/prod-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli # 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip VITE_BUILD_COMPRESS = gzip
VITE_API_URL = http://122.51.75.95:6039 # VITE_API_URL = http://122.51.75.95:6039
VITE_API_URL = http://129.211.24.7:6039

View File

@ -6,8 +6,10 @@ import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { login } from '@/api'; import { login } from '@/api';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session';
const userStore = useUserStore(); const userStore = useUserStore();
const sessionStore = useSessionStore();
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
@ -30,8 +32,10 @@ async function handleSubmit() {
res.data.token && userStore.setToken(res.data.token); res.data.token && userStore.setToken(res.data.token);
res.data.userInfo && userStore.setUserInfo(res.data.userInfo); res.data.userInfo && userStore.setUserInfo(res.data.userInfo);
ElMessage.success('登录成功'); ElMessage.success('登录成功');
router.replace('/');
userStore.closeLoginDialog(); userStore.closeLoginDialog();
//
await sessionStore.requestSessionList(1, true);
router.replace('/');
} }
catch (error) { catch (error) {
console.error('请求错误:', error); console.error('请求错误:', error);

View File

@ -15,4 +15,4 @@ export const COLLAPSE_THRESHOLD: number = 600;
export const SIDE_BAR_WIDTH: number = 280; export const SIDE_BAR_WIDTH: number = 280;
// 路由白名单地址[本地存在的路由 staticRouter.ts 中] // 路由白名单地址[本地存在的路由 staticRouter.ts 中]
export const ROUTER_WHITE_LIST: string[] = ['/chat', '/500', '/403', '/404']; export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/not_login', '/500', '/403', '/404'];

View File

@ -18,7 +18,7 @@ const sessionStore = useSessionStore();
const sessionId = computed(() => 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 loadMoreLoading = computed(() => sessionStore.isLoadingMore);
const active = computed(() => sessionStore.currentSession?.id); const active = ref<string | undefined>();
onMounted(async () => { onMounted(async () => {
// //
@ -28,6 +28,7 @@ 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}`;
} }
}); });
@ -176,7 +177,8 @@ function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo
:items-style="{ :items-style="{
marginLeft: '8px', marginLeft: '8px',
userSelect: 'none', userSelect: 'none',
borderRadius: '16px', borderRadius: '10px',
padding: '8px 12px',
}" }"
:items-active-style="{ :items-active-style="{
backgroundColor: '#fff', backgroundColor: '#fff',

View File

@ -3,8 +3,10 @@
import Popover from '@/components/Popover/index.vue'; import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session';
const userStore = useUserStore(); const userStore = useUserStore();
const sessionStore = useSessionStore();
const src = computed( const src = computed(
() => userStore.userInfo?.avatar ?? 'https://avatars.githubusercontent.com/u/76239030', () => userStore.userInfo?.avatar ?? 'https://avatars.githubusercontent.com/u/76239030',
); );
@ -62,9 +64,12 @@ function handleClick(item: any) {
roundButton: true, roundButton: true,
autofocus: false, autofocus: false,
}) })
.then(() => { .then(async () => {
// 退 // 退
userStore.logout(); await userStore.logout();
//
await sessionStore.requestSessionList(1, true);
await sessionStore.createSessionBtn();
ElMessage({ ElMessage({
type: 'success', type: 'success',
message: '退出成功', message: '退出成功',

View File

@ -14,7 +14,7 @@ function handleCreatChat() {
<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 select-none"
@click="handleCreatChat" @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">

View File

@ -14,6 +14,8 @@ const userStore = useUserStore();
const designStore = useDesignStore(); const designStore = useDesignStore();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
const currentSession = computed(() => sessionStore.currentSession);
onMounted(() => { onMounted(() => {
// () // ()
document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`); document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`);
@ -55,7 +57,7 @@ onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtr
> >
<Collapse /> <Collapse />
<CreateChat /> <CreateChat />
<div class="w-0.5px h-30px bg-[rgba(217,217,217)]" /> <div v-if="currentSession" class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
</div> </div>
<!-- 中间 --> <!-- 中间 -->

View File

@ -1,10 +1,21 @@
<!-- Main --> <!-- Main -->
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router';
import { useDesignStore } from '@/stores'; import { useDesignStore } from '@/stores';
import { useKeepAliveStore } from '@/stores/modules/keepAlive'; import { useKeepAliveStore } from '@/stores/modules/keepAlive';
const designStore = useDesignStore(); const designStore = useDesignStore();
const keepAliveStore = useKeepAliveStore(); const keepAliveStore = useKeepAliveStore();
const useroute = useRoute();
const transitionName = computed(() => {
if (useroute.meta.isDefaultChat) {
return 'slide';
}
else {
return designStore.pageAnimateType;
}
});
// //
const isRouterShow = ref(true); const isRouterShow = ref(true);
@ -15,7 +26,7 @@ provide('refresh', refreshMainPage);
<template> <template>
<el-main class="layout-main"> <el-main class="layout-main">
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition :name="designStore.pageAnimateType" mode="out-in" appear> <transition :name="transitionName" mode="out-in" appear>
<keep-alive :max="10" :include="keepAliveStore.keepAliveName"> <keep-alive :max="10" :include="keepAliveStore.keepAliveName">
<component :is="Component" v-if="isRouterShow" :key="route.fullPath" /> <component :is="Component" v-if="isRouterShow" :key="route.fullPath" />
</keep-alive> </keep-alive>
@ -24,4 +35,52 @@ provide('refresh', refreshMainPage);
</el-main> </el-main>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
.layout-main {
overflow: hidden;
}
/* 默认聊天页面:上下滑动动画 */
.slide-enter-from {
margin-top: 200px;
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s; /* 缓出动画 */
}
.slide-enter-to {
margin-top: 0;
opacity: 1;
}
.slide-leave-from {
margin-top: 0;
opacity: 1;
}
.slide-leave-to {
margin-top: 200px;
opacity: 0;
}
/* 带id聊天页面中间缩放动画 */
.zoom-fade-enter-from {
transform: scale(0.8); /* 进入前:缩小隐藏 */
opacity: 0;
}
.zoom-fade-enter-active,
.zoom-fade-leave-active {
transition: all 0.3s; /* 缓入动画 */
}
.zoom-fade-enter-to {
transform: scale(1); /* 进入后:正常大小 */
opacity: 1;
}
.zoom-fade-leave-from {
transform: scale(1); /* 离开前:正常大小 */
opacity: 1;
}
.zoom-fade-leave-to {
transform: scale(0.8); /* 离开后:缩小隐藏 */
opacity: 0;
}
</style>

View File

@ -5,7 +5,9 @@ import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue';
const route = useRoute(); const route = useRoute();
const sessionId = computed(() => Number(route.params?.id)); const sessionId = computed(() => route.params?.id);
console.log(sessionId.value);
// 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);

View File

@ -13,6 +13,7 @@ const senderValue = ref('');
const isDeepThinking = computed(() => chatStore.isDeepThinking); const isDeepThinking = computed(() => chatStore.isDeepThinking);
async function handleSend() { async function handleSend() {
localStorage.setItem('chatContent', senderValue.value);
await sessionStore.createSessionList({ await sessionStore.createSessionList({
userId: userStore.userInfo?.userId as number, userId: userStore.userInfo?.userId as number,
sessionContent: senderValue.value, sessionContent: senderValue.value,

View File

@ -14,6 +14,7 @@ export const layoutRouter: RouteRecordRaw[] = [
component: () => import('@/pages/chat/index.vue'), component: () => import('@/pages/chat/index.vue'),
meta: { meta: {
title: '通用聊天页面', title: '通用聊天页面',
isDefaultChat: true,
icon: 'HomeFilled', icon: 'HomeFilled',
isHide: '1', isHide: '1',
isKeepAlive: '0', // 是否缓存路由数据[0是1否] isKeepAlive: '0', // 是否缓存路由数据[0是1否]
@ -27,6 +28,7 @@ export const layoutRouter: RouteRecordRaw[] = [
component: () => import('@/pages/chat/index.vue'), component: () => import('@/pages/chat/index.vue'),
meta: { meta: {
title: '带 ID 的聊天页面', title: '带 ID 的聊天页面',
isDefaultChat: false,
}, },
}, },
], ],

View File

@ -13,6 +13,9 @@ export const useChatStore = defineStore('chat', () => {
}; };
const requestChatList = async (sessionId: number) => { const requestChatList = async (sessionId: number) => {
// 如果没有 token 则不查询聊天记录
if (!userStore.token)
return;
try { try {
const res = await getChatList({ const res = await getChatList({
sessionId, sessionId,

View File

@ -26,7 +26,7 @@ export const useSessionStore = defineStore('session', () => {
// 会话列表核心状态 // 会话列表核心状态
const sessionList = ref<ChatSessionVo[]>([]); // 会话数据列表 const sessionList = ref<ChatSessionVo[]>([]); // 会话数据列表
const currentPage = ref(1); // 当前页码从1开始 const currentPage = ref(1); // 当前页码从1开始
const pageSize = ref(20); // 每页显示数量 const pageSize = ref(25); // 每页显示数量
const hasMore = ref(true); // 是否还有更多数据 const hasMore = ref(true); // 是否还有更多数据
const isLoading = ref(false); // 全局加载状态(初始加载/刷新) const isLoading = ref(false); // 全局加载状态(初始加载/刷新)
const isLoadingMore = ref(false); // 加载更多状态(区分初始加载) const isLoadingMore = ref(false); // 加载更多状态(区分初始加载)
@ -45,6 +45,12 @@ export const useSessionStore = defineStore('session', () => {
// 获取会话列表(核心分页方法) // 获取会话列表(核心分页方法)
const requestSessionList = async (page: number = currentPage.value, force: boolean = false) => { const requestSessionList = async (page: number = currentPage.value, force: boolean = false) => {
// 如果没有token就直接清空
if (!userStore.token) {
sessionList.value = [];
return;
}
if (!force && ((page > 1 && !hasMore.value) || isLoading.value || isLoadingMore.value)) if (!force && ((page > 1 && !hasMore.value) || isLoading.value || isLoadingMore.value))
return; return;
@ -62,13 +68,11 @@ export const useSessionStore = defineStore('session', () => {
const resArr = await get_session_list(params); const resArr = await get_session_list(params);
// 预处理会话分组 // 预处理会话分组 并添加前缀图标
const res = processSessions(resArr.rows); const res = processSessions(resArr.rows);
const allSessions = new Map(sessionList.value.map(item => [item.id, item])); // 现有所有数据 const allSessions = new Map(sessionList.value.map(item => [item.id, item])); // 现有所有数据
res.forEach(item => res.forEach(item => allSessions.set(item.id, { ...item })); // 更新/添加数据
allSessions.set(item.id, { ...item, prefixIcon: markRaw(ChatLineRound) }),
); // 更新/添加数据
// 按服务端排序重建列表(假设分页数据是按时间倒序,第一页是最新,后续页依次递减) // 按服务端排序重建列表(假设分页数据是按时间倒序,第一页是最新,后续页依次递减)
// 此处需根据接口返回的排序规则调整,假设每页数据是递增的(第一页最新,第二页次新,第三页 oldest // 此处需根据接口返回的排序规则调整,假设每页数据是递增的(第一页最新,第二页次新,第三页 oldest
@ -104,6 +108,16 @@ export const useSessionStore = defineStore('session', () => {
// 发送消息后创建新会话 // 发送消息后创建新会话
const createSessionList = async (data: Omit<CreateSessionDTO, 'id'>) => { const createSessionList = async (data: Omit<CreateSessionDTO, 'id'>) => {
if (!userStore.token) {
router.replace({
name: 'chatWithId',
params: {
id: 'not_login',
},
});
return;
}
try { try {
const res = await create_session(data); const res = await create_session(data);
// 创建会话后立刻查询列表会话 // 创建会话后立刻查询列表会话
@ -202,6 +216,7 @@ export const useSessionStore = defineStore('session', () => {
return { return {
...session, ...session,
group, // 新增分组键字段 group, // 新增分组键字段
prefixIcon: markRaw(ChatLineRound), // 图标为静态组件,使用 markRaw 标记为静态组件
}; };
}); });
} }