feat: ✨ 新增路由守卫

This commit is contained in:
何嘉悦
2025-05-25 18:06:09 +08:00
parent 16b59d6ab2
commit f4d078ca07
52 changed files with 928 additions and 282 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
import type {
ChatSessionVo,
CreateSessionDTO,
CreateSessionVO,
// CreateSessionVO,
GetSessionListParams,
} from './types';
import { get, post } from '@/utils/request';
@@ -11,5 +11,5 @@ export function getSessionList(params: GetSessionListParams) {
}
export function createSession(data: CreateSessionDTO) {
return post<CreateSessionVO>('/system/session', data);
return post('/system/session', data);
}
+3 -3
View File
@@ -137,6 +137,6 @@ export interface CreateSessionDTO {
userId: number;
}
export interface CreateSessionVO {
id: number;
}
// export interface CreateSessionVO {
// id: number;
// }
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

@@ -5,7 +5,7 @@ import type { LoginDTO } from '@/api/auth/types';
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { login } from '@/api';
import { useUserStore } from '@/store';
import { useUserStore } from '@/stores';
const userStore = useUserStore();
@@ -29,7 +29,9 @@ async function handleSubmit() {
console.log(res, 'res');
res.data.token && userStore.setToken(res.data.token);
res.data.userInfo && userStore.setUserInfo(res.data.userInfo);
ElMessage.success('登录成功');
router.replace('/');
userStore.closeLoginDialog();
}
catch (error) {
console.error('请求错误:', error);
@@ -39,7 +41,7 @@ async function handleSubmit() {
<template>
<div class="custom-form">
<el-form ref="formRef" :model="formModel" :rules="rules">
<el-form ref="formRef" :model="formModel" :rules="rules" style="width: 230px">
<el-form-item prop="username">
<el-input v-model="formModel.username" placeholder="请输入用户名" clearable>
<template #prefix>
+1 -1
View File
@@ -2,7 +2,7 @@
import { ref, watch } from 'vue';
import logoPng from '@/assets/images/logo.png';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/store';
import { useUserStore } from '@/stores';
import AccountPassword from './components/FormLogin/AccountPassword.vue';
import QrCodeLogin from './components/QrCodeLogin/index.vue';
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- 欢迎提示词 -->
<script setup lang="ts">
import { useTimeGreeting } from '@/hooks/useTimeGreeting';
import { useUserStore } from '@/store';
import { useUserStore } from '@/stores';
const greeting = useTimeGreeting();
const userStore = useUserStore();
+2 -2
View File
@@ -3,7 +3,7 @@
export const HOME_URL: string = '/chat';
// 登录页地址[默认]
export const LOGIN_URL: string = '/login';
// export const LOGIN_URL: string = '/login';
// 默认主题颜色
export const DEFAULT_THEME_COLOR: string = '#2992FF';
@@ -15,4 +15,4 @@ export const COLLAPSE_THRESHOLD: number = 600;
export const SIDE_BAR_WIDTH: number = 280;
// 路由白名单地址[本地存在的路由 staticRouter.ts 中]
export const ROUTER_WHITE_LIST: string[] = ['/500'];
export const ROUTER_WHITE_LIST: string[] = ['/chat', '/500', '/403', '/404'];
+1 -1
View File
@@ -1,6 +1,6 @@
import { COLLAPSE_THRESHOLD } from '@/config/index';
import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
import { useDesignStore } from '@/store';
import { useDesignStore } from '@/stores';
/**
* 侧边栏折叠切换逻辑组合式函数 方便多个页面调用
+1 -1
View File
@@ -1,7 +1,7 @@
import type { MaybeRef } from 'vue';
import { onBeforeUnmount, ref, unref, watch } from 'vue';
import { COLLAPSE_THRESHOLD, SIDE_BAR_WIDTH } from '@/config/index';
import { useDesignStore } from '@/store';
import { useDesignStore } from '@/stores';
/**
* 这里逻辑是研究豆包的折叠逻辑后,设计的折叠方法
+1 -1
View File
@@ -5,7 +5,7 @@ import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
import Aside from '@/layouts/components/Aside/index.vue';
import Header from '@/layouts/components/Header/index.vue';
import Main from '@/layouts/components/Main/index.vue';
import { useDesignStore } from '@/store';
import { useDesignStore } from '@/stores';
const designStore = useDesignStore();
+24 -10
View File
@@ -1,27 +1,31 @@
<!-- Aside 侧边栏 -->
<script setup lang="ts">
import type { GroupableOptions } from 'vue-element-plus-x/types/Conversations';
import { useRoute } from 'vue-router';
import type { ConversationItem, GroupableOptions } from 'vue-element-plus-x/types/Conversations';
import type { ChatSessionVo } from '@/api/session/types';
import { useRoute, useRouter } from 'vue-router';
import logo from '@/assets/images/logo.png';
import Collapse from '@/layouts/components/Header/components/Collapse.vue';
import { useDesignStore } from '@/store';
import { useChatStore } from '@/store/modules/chat';
import { useDesignStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session';
const route = useRoute();
const chatStore = useChatStore();
const router = useRouter();
const designStore = useDesignStore();
const sessionStore = useSessionStore();
const chatId = computed(() => Number(route.params?.id));
const conversationsList = computed(() => chatStore.chatMap[chatId.value] ?? []);
// const sessionId = computed(() => Number(route.params?.id));
const conversationsList = computed(() => sessionStore.sessionList);
/* 创建会话 开始 */
function handleCreatChat() {
console.log('创建新会话');
// 创建会话, 跳转到默认聊天
sessionStore.createSessionBtn();
}
/* 创建会话 结束 */
/* 会话组件 开始 */
const active = ref('m1');
const active = computed<string>(() => (route.params?.id as string) ?? '');
// 自定义分组选项
const customGroupOptions: GroupableOptions = {
@@ -34,10 +38,20 @@ const customGroupOptions: GroupableOptions = {
},
};
function handleChange() {
console.log('点击了会话');
function handleChange(item: ConversationItem<ChatSessionVo>) {
console.log('点击了会话 item', item);
router.replace({
name: 'chatWithId',
params: {
id: item.id,
},
});
}
/* 会话组件 结束 */
watchEffect(() => {
console.log('active', active.value, '>>>');
});
</script>
<template>
@@ -2,8 +2,12 @@
<script setup lang="ts">
import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/stores';
const src = ref('https://avatars.githubusercontent.com/u/76239030');
const userStore = useUserStore();
const src = computed(
() => userStore.userInfo?.avatar ?? 'https://avatars.githubusercontent.com/u/76239030',
);
/* 弹出面板 开始 */
const popoverStyle = ref({
@@ -40,9 +44,11 @@ const popoverList = ref([
function handleClick(item: any) {
switch (item.key) {
case '1':
ElMessage.warning('暂未开放');
console.log('点击了收藏夹');
break;
case '2':
ElMessage.warning('暂未开放');
console.log('点击了设置');
break;
case '4':
@@ -57,7 +63,7 @@ function handleClick(item: any) {
})
.then(() => {
// 在这里执行退出方法
userStore.logout();
ElMessage({
type: 'success',
message: '退出成功',
@@ -3,7 +3,7 @@
import SvgIcon from '@/components/SvgIcon/index.vue';
import { SIDE_BAR_WIDTH } from '@/config/index';
import { useCollapseToggle } from '@/hooks/useCollapseToggle';
import { useDesignStore } from '@/store';
import { useDesignStore } from '@/stores';
const { changeCollapse } = useCollapseToggle();
const designStore = useDesignStore();
@@ -1,10 +1,9 @@
<!-- LoginBtn 登录按钮 -->
<script setup lang="ts">
import LoginDialog from '@/components/LoginDialog/index.vue';
import { useUserStore } from '@/store';
import { useUserStore } from '@/stores';
const userStore = useUserStore();
const isLoginDialogVisible = computed(() => userStore.isLoginDialogVisible);
// 点击登录按钮时调用Store方法打开弹框
+3 -5
View File
@@ -1,7 +1,7 @@
<!-- Header 头部 -->
<script setup lang="ts">
import { SIDE_BAR_WIDTH } from '@/config/index';
import { useDesignStore, useUserStore } from '@/store';
import { useDesignStore, useUserStore } from '@/stores';
import Avatar from './components/Avatar.vue';
import Collapse from './components/Collapse.vue';
import CreateChat from './components/CreateChat.vue';
@@ -54,10 +54,8 @@ onMounted(() => {
<!-- 右边 -->
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
<Avatar v-if="userStore.token" />
<LoginBtn v-else />
<!-- <Avatar />
<LoginBtn /> -->
<Avatar v-show="userStore.token" />
<LoginBtn v-show="!userStore.token" />
</div>
</div>
</div>
+3 -3
View File
@@ -1,7 +1,7 @@
<!-- Main -->
<script setup lang="ts">
import { useDesignStore } from '@/store';
import { useKeepAliveStore } from '@/store/modules/keepAlive';
import { useDesignStore } from '@/stores';
import { useKeepAliveStore } from '@/stores/modules/keepAlive';
const designStore = useDesignStore();
const keepAliveStore = useKeepAliveStore();
@@ -16,7 +16,7 @@ provide('refresh', refreshMainPage);
<el-main class="layout-main">
<router-view v-slot="{ Component, route }">
<transition :name="designStore.pageAnimateType" mode="out-in" appear>
<keep-alive :max="16" :include="keepAliveStore.keepAliveName">
<keep-alive :max="10" :include="keepAliveStore.keepAliveName">
<component :is="Component" v-if="isRouterShow" :key="route.fullPath" />
</keep-alive>
</transition>
+5 -5
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
import type { ChatSessionVo } from '@/api/session/types';
// import { useUserStore } from '@/store';
// import { useUserStore } from '@/stores';
import { Conversations } from 'vue-element-plus-x';
import { useRoute, useRouter } from 'vue-router';
// import { getSessionList } from '@/api';
@@ -43,13 +43,13 @@ function handleChange(item: ConversationItem<ChatSessionVo>) {
const sessionId = computed<string>(() => route.params?.id as string);
function handleNewSession() {
if (sessionId.value) {
router.replace({ name: 'chatWithoutId' });
router.replace({ name: 'chatWithId' });
}
}
watchEffect(() => {
console.log('active', active.value, '>>>');
});
// watchEffect(() => {
// console.log('active', active.value, '>>>');
// });
</script>
<template>
+1 -1
View File
@@ -3,7 +3,7 @@
import type { LayoutType } from '@/config/design';
// import { useScreenStore } from '@/hooks/useScreen';
import LayoutVertical from '@/layouts/LayoutVertical/index.vue';
import { useDesignStore } from '@/store';
import { useDesignStore } from '@/stores';
// 这里添加布局类型
const LayoutComponent: Record<LayoutType, Component> = {
+2 -2
View File
@@ -4,8 +4,8 @@ import { ElMessage } from 'element-plus';
import { createApp } from 'vue';
import ElementPlusX from 'vue-element-plus-x';
import App from './App.vue';
import router from './router';
import store from './store';
import router from './routers';
import store from './stores';
import './styles/index.scss';
import 'virtual:uno.css';
import 'element-plus/dist/index.css';
+12 -112
View File
@@ -1,20 +1,13 @@
<script setup lang="ts">
import { Sender } from 'vue-element-plus-x';
import { useRoute, useRouter } from 'vue-router';
import { createSession } from '@/api';
import { send } from '@/api/chat';
import WelecomeText from '@/components/WelecomeText/index.vue';
import { ModelEnum } from '@/constants/enums';
import { useUserStore } from '@/store';
import { useChatStore } from '@/store/modules/chat';
import { useRoute } from 'vue-router';
import ChatDefaul from '@/pages/chat/layouts/chatDefaul/index.vue';
import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue';
import { useChatStore } from '@/stores/modules/chat';
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const chatStore = useChatStore();
const senderValue = ref('');
const isSelect = ref(false);
const chatId = computed(() => Number(route.params?.id));
if (chatId.value) {
@@ -30,107 +23,28 @@ watch(
senderValue.value = v;
localStorage.removeItem('chatContent');
// 发送消息
console.log(v);
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}`)
// console.log(a);
const loading = ref(false);
async function handleSend() {
if (!chatId.value) {
try {
const res = await createSession({
userId: userStore.userInfo?.userId as number,
sessionContent: senderValue.value,
sessionTitle: senderValue.value.slice(0, 10),
remark: senderValue.value.slice(0, 10),
});
localStorage.setItem('chatContent', senderValue.value);
router.replace({
name: 'chat',
params: {
id: res.data.id,
},
});
}
catch (error) {
console.error('createSessionError', error);
}
return;
}
// 发送消息
loading.value = true;
const req = send({
sessionId: chatId.value,
model: ModelEnum.GPT_4o_MINI.value,
messages: [
{
role: 'user',
content: senderValue.value,
},
],
});
for await (const chunk of req) {
if (chunk.result && typeof chunk.result === 'string') {
console.log('string:', chunk.result);
// const a = JSON.parse(chunk.result);
// console.log(a,'>>>');
}
console.log(chunk.result);
}
loading.value = false;
}
</script>
<template>
<div class="chat-home-container">
<div class="chat-home-wrap">
<WelecomeText />
<Sender
v-model="senderValue"
class="chat-home-sender"
:auto-size="{
maxRows: 9,
minRows: 3,
}"
:loading="loading"
variant="updown"
clearable
allow-speech
@submit="handleSend"
>
<template #prefix>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
<div
class="flex items-center gap-4px px-12px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
>
<el-icon>
<Paperclip />
</el-icon>
</div>
<div
:class="{ isSelect }"
class="flex items-center gap-4px px-10px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
@click="isSelect = !isSelect"
>
<el-icon>
<ElementPlus />
</el-icon>
<span>深度思考</span>
</div>
</div>
</template>
</Sender>
</div>
<div class="chat-container">
<!-- 默认聊天页面 -->
<ChatDefaul v-if="!chatId" />
<!-- 带id的聊天页面 -->
<ChatWithId v-else :chat-id="chatId" />
</div>
</template>
<style lang="scss" scoped>
.chat-home-container {
.chat-container {
padding: 0 16px;
width: calc(100% - 32px);
position: relative;
@@ -140,19 +54,5 @@ async function handleSend() {
justify-content: center;
height: 100%;
overflow-anchor: none;
.chat-home-wrap {
position: relative;
width: 100%;
max-width: 800px;
min-height: 450px;
display: flex;
flex-direction: column;
align-items: center;
.chat-home-sender {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,91 @@
<!-- -->
<script setup lang="ts">
import WelecomeText from '@/components/WelecomeText/index.vue';
import { useUserStore } from '@/stores';
import { useChatStore } from '@/stores/modules/chat';
import { useSessionStore } from '@/stores/modules/session';
const userStore = useUserStore();
const chatStore = useChatStore();
const sessionStore = useSessionStore();
const senderValue = ref('');
const isDeepThinking = computed(() => chatStore.isDeepThinking);
async function handleSend() {
await sessionStore.createSessionList({
userId: userStore.userInfo?.userId as number,
sessionContent: senderValue.value,
sessionTitle: senderValue.value.slice(0, 10),
remark: senderValue.value.slice(0, 10),
});
}
// 设置是否深度思考
function setIsDeepThinking() {
chatStore.setDeepThinking(!chatStore.isDeepThinking);
}
</script>
<template>
<div class="chat-defaul-wrap">
<WelecomeText />
<Sender
v-model="senderValue"
class="chat-defaul-sender"
:auto-size="{
maxRows: 9,
minRows: 3,
}"
variant="updown"
clearable
allow-speech
@submit="handleSend"
>
<template #prefix>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
<div
class="flex items-center gap-4px px-12px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
>
<el-icon>
<Paperclip />
</el-icon>
</div>
<div
:class="{ 'is-select': isDeepThinking }"
class="flex items-center gap-4px px-10px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
@click="setIsDeepThinking"
>
<el-icon>
<ElementPlus />
</el-icon>
<span>深度思考</span>
</div>
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="scss">
.chat-defaul-wrap {
position: relative;
width: 100%;
max-width: 800px;
min-height: 450px;
display: flex;
flex-direction: column;
align-items: center;
.chat-defaul-sender {
width: 100%;
}
.is-select {
color: var(--el-color-primary, #409eff);
border: 1px solid var(--el-color-primary, #409eff);
border-radius: 15px;
}
}
</style>
@@ -0,0 +1,18 @@
<!-- 每个回话对应的聊天内容 -->
<script setup lang="ts">
interface ChatWithIdProps {
chatId?: number;
}
const props = withDefaults(defineProps<ChatWithIdProps>(), {
chatId: undefined,
});
console.log('props ==> ', props);
</script>
<template>
<div>新建对话</div>
</template>
<style scoped lang="scss"></style>
+65
View File
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { HOME_URL } from '@/config/index.ts';
// 路由跳转
const router = useRouter();
function handleHomePage() {
router.push({ path: HOME_URL });
}
</script>
<template>
<div id="box">
<div id="banner" class="elx-top" />
<div class="elx-bottom">
<div class="elx-text1">
403
</div>
<div class="elx-text2">
对不起,您没有权限访问
</div>
<div class="h-20px" />
<el-button type="primary" plain @click="handleHomePage">
返回首页
</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
#box {
overflow: hidden;
}
#banner {
margin-top: 60px;
background: url("@/assets/images/error/403.png") no-repeat;
background-size: 100%;
}
.elx-top {
width: 600px;
height: 400px;
margin: 0 auto;
}
.elx-bottom {
height: 300px;
margin-top: 20px;
text-align: center;
}
.elx-text1 {
font-size: 46px;
font-weight: bold;
}
.elx-text2 {
padding-top: 30px;
font-family: YouYuan;
font-size: 24px;
font-weight: 600;
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { HOME_URL } from '@/config/index.ts';
// 路由跳转
const router = useRouter();
function handleHomePage() {
router.push({ path: HOME_URL });
}
</script>
<template>
<div id="box">
<div id="banner" class="elx-top" />
<div class="elx-bottom">
<div class="elx-text1">
404
</div>
<div class="elx-text2">
您想看的页面不存在哟
</div>
<div class="h-20px" />
<el-button type="primary" plain @click="handleHomePage">
返回首页
</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
#box {
overflow: hidden;
}
#banner {
margin-top: 60px;
background: url("@/assets/images/error/404.png") no-repeat;
background-size: 100%;
}
.elx-top {
width: 600px;
height: 400px;
margin: 0 auto;
}
.elx-bottom {
height: 300px;
margin-top: 20px;
text-align: center;
}
.elx-text1 {
font-size: 46px;
font-weight: bold;
}
.elx-text2 {
padding-top: 30px;
font-family: YouYuan;
font-size: 24px;
font-weight: 600;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { HOME_URL } from '@/config/index.ts';
// 路由跳转
const router = useRouter();
function handleHomePage() {
router.push({ path: HOME_URL });
}
</script>
<template>
<div id="box">
<div id="banner" class="elx-top" />
<div class="elx-bottom">
<div class="elx-text1">
500
</div>
<div class="elx-text2">
服务器好像开小差了!请稍后试试...
</div>
<div class="h-20px" />
<el-button type="primary" plain @click="handleHomePage">
返回首页
</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
#box {
overflow: hidden;
}
#banner {
margin-top: 60px;
background: url("@/assets/images/error/500.png") no-repeat;
background-size: 100%;
}
.elx-top {
width: 600px;
height: 400px;
margin: 0 auto;
}
.elx-bottom {
height: 300px;
margin-top: 20px;
text-align: center;
}
.elx-text1 {
font-size: 46px;
font-weight: bold;
}
.elx-text2 {
padding-top: 30px;
font-family: YouYuan;
font-size: 24px;
font-weight: 600;
}
</style>
-53
View File
@@ -1,53 +0,0 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import type { LoginDTO } from '@/api/auth/types';
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { login } from '@/api';
import { useUserStore } from '@/store';
const userStore = useUserStore();
const formRef = ref<FormInstance>();
const formModel = reactive<LoginDTO>({
username: '',
password: '',
});
const router = useRouter();
async function handleSubmit() {
try {
await formRef.value?.validate();
const res = await login(formModel);
console.log(res, 'res');
res.data.token && userStore.setToken(res.data.token);
res.data.userInfo && userStore.setUserInfo(res.data.userInfo);
router.replace('/');
}
catch (error) {
console.error('请求错误:', error);
}
}
</script>
<template>
<div class="w-screen h-screen justify-center items-center flex bg-black">
<div class="max-w-[500px] w-full p-5 rounded bg-white/60 backdrop-blur-md">
<h1>登录</h1>
<el-form ref="formRef" :model="formModel">
<el-form-item label="用户名" name="username">
<el-input v-model="formModel.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" name="password">
<el-input v-model="formModel.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">
登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
-41
View File
@@ -1,41 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import { jwtGuard } from './permissions';
const routes: Readonly<RouteRecordRaw>[] = [
{
path: '/',
redirect: 'chat',
component: () => import('@/layouts/index.vue'),
children: [
{
path: '/chat',
name: 'chat',
component: () => import('@/pages/chat/index.vue'),
},
{
path: '/chat:id',
name: 'chatWithoutId',
component: () => import('@/pages/chat/index.vue'),
},
],
},
{
path: '/login',
name: 'login',
component: () => import('@/pages/login/index.vue'),
},
] as const;
const router = createRouter({
history: createWebHistory(),
routes,
});
console.group('Routes');
console.log('routes', router.getRoutes());
console.groupEnd();
router.beforeEach(jwtGuard());
export default router;
-7
View File
@@ -1,7 +0,0 @@
import type { NavigationGuard } from 'vue-router';
export function jwtGuard(): NavigationGuard {
return () => {
console.log('进入路由守卫');
};
}
+85
View File
@@ -0,0 +1,85 @@
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
import { useNProgress } from '@vueuse/integrations/useNProgress';
import { createRouter, createWebHistory } from 'vue-router';
import { ROUTER_WHITE_LIST } from '@/config';
import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter';
import { useUserStore } from '@/stores';
const { start, done } = useNProgress(0, {
showSpinner: false,
trickleSpeed: 200,
minimum: 0.3,
easing: 'ease',
speed: 500,
});
const router = createRouter({
history: createWebHistory(),
routes: [...layoutRouter, ...staticRouter, ...errorRouter],
strict: false,
scrollBehavior: () => ({ left: 0, top: 0 }),
});
console.group('Routes');
console.log('routes', router.getRoutes());
console.groupEnd();
// 路由前置守卫
router.beforeEach(
async (
to: RouteLocationNormalized,
_from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
const userStore = useUserStore();
// 1、NProgress 开始
start();
// 2、标题
document.title = to.meta.title || import.meta.env.VITE_WEB_TITLE;
// 3、权限 预留
// 3、判断是访问登陆页,有Token访问当前页面,token过期访问接口,axios封装则自动跳转登录页面,没有Token重置路由到登陆页。
// if (to.path.toLocaleLowerCase() === LOGIN_URL) {
// // 有Token访问当前页面
// if (userStore.token) {
// return next(from.fullPath);
// }
// else {
// ElMessage.error('账号身份已过期,请重新登录🌻');
// }
// // 没有Token重置路由到登陆页。
// // resetRouter(); // 预留
// return next();
// }
// 4、判断访问页面是否在路由白名单地址[静态路由]中,如果存在直接放行。
if (ROUTER_WHITE_LIST.includes(to.path))
return next();
// 5、判断是否有 Token,没有重定向到 login 页面。
if (!userStore.token)
userStore.logout();
// 其余逻辑 预留...
// 6、正常访问页面。
next();
},
);
// 路由跳转错误
router.onError((error) => {
// 结束全屏动画
done();
console.warn('路由错误', error.message);
});
// 后置路由
router.afterEach(() => {
// 结束全屏动画
done();
});
export default router;
+39
View File
@@ -0,0 +1,39 @@
// 预留
import router from '@/routers/index';
import { useUserStore } from '@/stores';
import { useAuthStore } from '@/stores/modules/auth';
export async function initDynamicRouter() {
const userStore = useUserStore();
const authStore = useAuthStore();
try {
// 1、预留: 获取菜单列表 || 按钮权限列表 || 递归菜单数据
await authStore.requestAuthMenuList();
// 2、判断当前用户是否拥有菜单权限
console.log('authStore.authMenuList', authStore.authMenuList);
if (authStore.authMenuList == null || authStore.authMenuList.length === 0) {
userStore.logout();
return;
}
// 3、添加动态路由
authStore.authMenuList.forEach((item: any) => {
if (item.isFull === '0') {
// 如果是全屏的话,直接为整个页面
router.addRoute(item);
}
else {
router.addRoute('layout', item);
}
});
}
catch (error) {
console.log(error);
// 当菜单请求出错时,重定向到首页
userStore.logout();
return Promise.reject(error);
}
}
+91
View File
@@ -0,0 +1,91 @@
import type { RouteRecordRaw } from 'vue-router';
import { HOME_URL } from '@/config';
// LayoutRouter[布局路由]
export const layoutRouter: RouteRecordRaw[] = [
{
path: '/',
redirect: HOME_URL,
component: () => import('@/layouts/index.vue'),
children: [
{
path: HOME_URL,
name: 'chat',
component: () => import('@/pages/chat/index.vue'),
meta: {
title: '通用聊天页面',
icon: 'HomeFilled',
isHide: '1',
isKeepAlive: '0', // 是否缓存路由数据[0是,1否]
isFull: '1', // 是否缓存全屏[0是,1否]
// enName: "Master Station", // 英文名称 预留
},
},
{
path: '/chat/:id',
name: 'chatWithId',
component: () => import('@/pages/chat/index.vue'),
meta: {
title: '带 ID 的聊天页面',
},
},
],
},
];
// staticRouter[静态路由] 预留
export const staticRouter: RouteRecordRaw[] = [];
// errorRouter (错误页面路由)
export const errorRouter = [
{
path: '/403',
name: '403',
component: () => import('@/pages/error/403.vue'),
meta: {
title: '403页面',
enName: '403 Page', // 英文名称
icon: 'QuestionFilled', // 菜单图标
isHide: '1', // 代表路由在菜单中是否隐藏,是否隐藏[0隐藏,1显示]
isLink: '1', // 是否外链[有值则是外链]
isKeepAlive: '0', // 是否缓存路由数据[0是,1否]
isFull: '1', // 是否缓存全屏[0是,1否]
isAffix: '1', // 是否缓存固定路由[0是,1否]
},
},
{
path: '/404',
name: '404',
component: () => import('@/pages/error/404.vue'),
meta: {
title: '404页面',
enName: '404 Page', // 英文名称
icon: 'CircleCloseFilled', // 菜单图标
isHide: '1', // 代表路由在菜单中是否隐藏,是否隐藏[0隐藏,1显示]
isLink: '1', // 是否外链[有值则是外链]
isKeepAlive: '0', // 是否缓存路由数据[0是,1否]
isFull: '1', // 是否缓存全屏[0是,1否]
isAffix: '1', // 是否缓存固定路由[0是,1否]
},
},
{
path: '/500',
name: '500',
component: () => import('@/pages/error/500.vue'),
meta: {
title: '500页面',
enName: '500 Page', // 英文名称
icon: 'WarningFilled', // 图标
isHide: '1', // 代表路由在菜单中是否隐藏,是否隐藏[0隐藏,1显示]
isLink: '1', // 是否外链[有值则是外链]
isKeepAlive: '0', // 是否缓存路由数据[0是,1否]
isFull: '1', // 是否缓存全屏[0是,1否]
isAffix: '1', // 是否缓存固定路由[0是,1否]
},
},
// 找不到path将跳转404页面
{
path: '/:pathMatch(.*)*',
component: () => import('@/pages/error/404.vue'),
},
];
+18
View File
@@ -0,0 +1,18 @@
import { defineStore } from 'pinia';
// 权限状态管理
export const useAuthStore = defineStore('auth', () => {
// 权限菜单列表
const authMenuList = ref<any[]>([]);
// 请求权限菜单列表
const requestAuthMenuList = async () => {
// const res = await initDynamicRouter();
authMenuList.value = [];
};
return {
authMenuList,
requestAuthMenuList,
};
});
@@ -5,6 +5,7 @@ import { useUserStore } from './user';
export const useChatStore = defineStore('chat', () => {
const userStore = useUserStore();
const chatMap = ref<Record<number, ChatMessageVo[]>>({});
const setChatMap = (id: number, data: ChatMessageVo[]) => {
@@ -26,8 +27,17 @@ export const useChatStore = defineStore('chat', () => {
}
};
// 是否开启深度思考
const isDeepThinking = ref<boolean>(false);
const setDeepThinking = (value: boolean) => {
isDeepThinking.value = value;
};
return {
chatMap,
requestChatList,
isDeepThinking,
setDeepThinking,
};
});
+69
View File
@@ -0,0 +1,69 @@
import type { CreateSessionDTO } from '@/api/session/types';
import { defineStore } from 'pinia';
import { useRouter } from 'vue-router';
import { createSession, getSessionList } from '@/api/session';
import { useUserStore } from './user';
export const useSessionStore = defineStore('session', () => {
const router = useRouter();
const userStore = useUserStore();
const sessionList = ref<CreateSessionDTO[]>([]);
// 单独点击创建新对话按钮
const createSessionBtn = async () => {
try {
// 直接回到首页
router.replace({ name: 'chat' });
}
catch (error) {
console.error('createSessionBtn:', error);
}
};
// 发送消息后,创建对话
const createSessionList = async (data: CreateSessionDTO) => {
try {
const res = await createSession(data);
localStorage.setItem('chatContent', data.sessionContent);
router.replace({
name: 'chatWithId',
params: {
id: `${res.data}`,
},
});
}
catch (error) {
console.error('createSession:', error);
}
};
// 获取会话列表
const requestSessionList = async () => {
try {
const res = await getSessionList({
userId: userStore.userInfo?.userId as number,
});
if (res.rows) {
sessionList.value = (res.rows || []).map(item => ({
...item,
// 提供默认值
remark: item.remark || '',
sessionContent: item.sessionContent || '',
sessionTitle: item.sessionTitle || '',
userId: item.userId || -1,
}));
}
}
catch (error) {
console.error('getSessionList:', error);
}
};
return {
sessionList,
createSessionBtn,
createSessionList,
requestSessionList,
};
});
+5
View File
@@ -16,4 +16,9 @@
--login-dialog-logo-background: #fff;
--login-dialog-logo-text-color: #191919;
/* 覆盖 element-plus 样式 */
--el-border-radius-base: 12px;
--el-messagebox-border-radius: 16px;
}
+3 -2
View File
@@ -2,7 +2,7 @@ import type { HookFetchPlugin } from 'hook-fetch';
import { ElMessage } from 'element-plus';
import hookFetch from 'hook-fetch';
import { sseTextDecoderPlugin } from 'hook-fetch/plugins';
import { useUserStore } from '@/store';
import { useUserStore } from '@/stores';
interface BaseResponse {
code: number;
@@ -34,8 +34,9 @@ function jwtPlugin(): HookFetchPlugin<BaseResponse> {
return response;
}
if (response.result?.code === 401) {
// 如果没有权限,也不退出,则是弹框提示登录
// 如果没有权限,退出,且弹框提示登录
userStore.logout();
userStore.openLoginDialog();
}
ElMessage.error(response.result?.msg);
return Promise.reject(response);