feat: 添加退出登录, 优化闪跳

This commit is contained in:
Gavan
2025-07-22 15:06:39 +08:00
parent 472ad769a9
commit 7c33568da2
13 changed files with 234 additions and 33 deletions

View File

@@ -37,6 +37,10 @@ export enum ContentType {
Text = "text/plain",
}
const whitePathnameList = ['/user/login', '/login'];
const whiteApiList = ['/api/v1/user/profile', '/api/v1/admin/profile'];
const redirectToLogin = () => {
const redirectAfterLogin = encodeURIComponent(location.href);
const search = `redirect=${redirectAfterLogin}`;
@@ -72,6 +76,9 @@ export class HttpClient<SecurityDataType = unknown> {
},
(err) => {
if (err?.response?.status === 401) {
if (whitePathnameList.includes(location.pathname)) {
return;
}
Message.error('尚未登录')
redirectToLogin();
return

View File

@@ -175,6 +175,51 @@ export const getAdminLoginHistory = (
...params,
});
/**
* @description 管理员登出
*
* @tags Admin
* @name PostAdminLogout
* @summary 管理员登出
* @request POST:/api/v1/admin/logout
* @response `200` `WebResp` OK
*/
export const postAdminLogout = (params: RequestParams = {}) =>
request<WebResp>({
path: `/api/v1/admin/logout`,
method: "POST",
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 管理员信息
*
* @tags Admin
* @name GetAdminProfile
* @summary 管理员信息
* @request GET:/api/v1/admin/profile
* @response `200` `(WebResp & {
data?: DomainAdminUser,
})` OK
*/
export const getAdminProfile = (params: RequestParams = {}) =>
request<
WebResp & {
data?: DomainAdminUser;
}
>({
path: `/api/v1/admin/profile`,
method: "GET",
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 获取系统设置
*

View File

@@ -125,7 +125,7 @@ export const getModelList = (params: RequestParams = {}) =>
});
/**
* @description 报告
* @description 报告支持多种操作accept接受补全、suggest建议、reject拒绝补全并记录用户输入、file_written文件写入
*
* @tags OpenAIV1
* @name PostReport

View File

@@ -189,6 +189,25 @@ export const getLoginHistory = (
...params,
});
/**
* @description 用户登出
*
* @tags User
* @name PostLogout
* @summary 用户登出
* @request POST:/api/v1/user/logout
* @response `200` `WebResp` OK
*/
export const postLogout = (params: RequestParams = {}) =>
request<WebResp>({
path: `/api/v1/user/logout`,
method: "POST",
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description 用户 OAuth 回调
*

View File

@@ -58,6 +58,9 @@ export enum ContentType {
Text = "text/plain",
}
const whitePathnameList = ["/user/login", "/login"];
const whiteApiList = ["/api/v1/user/profile", "/api/v1/admin/profile"];
const redirectToLogin = () => {
const redirectAfterLogin = encodeURIComponent(location.href);
const search = `redirect=${redirectAfterLogin}`;
@@ -101,6 +104,9 @@ export class HttpClient<SecurityDataType = unknown> {
},
(err) => {
if (err?.response?.status === 401) {
if (whitePathnameList.includes(location.pathname)) {
return;
}
Message.error("尚未登录");
redirectToLogin();
return;

View File

@@ -26,6 +26,7 @@ export enum ConstsReportAction {
ReportActionAccept = "accept",
ReportActionSuggest = "suggest",
ReportActionFileWritten = "file_written",
ReportActionReject = "reject",
}
export enum ConstsModelType {
@@ -367,8 +368,11 @@ export interface DomainLoginReq {
password?: string;
/** 会话Id插件登录时必填 */
session_id?: string;
/** 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin */
source?: ConstsLoginSource;
/**
* 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin
* @default "plugin"
*/
source: ConstsLoginSource;
/** 用户名 */
username?: string;
}
@@ -511,10 +515,16 @@ export interface DomainReportReq {
action?: ConstsReportAction;
/** 内容 */
content?: string;
/** 光标位置用于reject action */
cursor_position?: number;
/** task_id or resp_id */
id?: string;
/** 当前文件的原文用于reject action */
source_code?: string;
/** 工具 */
tool?: string;
/** 用户输入的新文本用于reject action */
user_input?: string;
}
export interface DomainSetting {
@@ -1070,6 +1080,9 @@ export interface GetUserOauthSignupOrInParams {
redirect_url?: string;
/** 会话ID */
session_id?: string;
/** 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin */
source?: "plugin" | "browser";
/**
* 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin
* @default "plugin"
*/
source: "plugin" | "browser";
}

View File

@@ -1,14 +1,24 @@
import { Button, IconButton, Stack } from '@mui/material';
import { Icon, message } from '@c-x/ui';
import { Button, IconButton, Stack, Tooltip } from '@mui/material';
import { message } from '@c-x/ui';
import { postLogout } from '@/api/User';
import { postAdminLogout } from '@/api/Admin';
import { useNavigate, useLocation } from 'react-router-dom';
import LogoutIcon from '@mui/icons-material/Logout';
import DownloadIcon from '@mui/icons-material/Download';
import { Box } from '@mui/material';
import Bread from './Bread';
const Header = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
const onLogout = async () => {
if (pathname.startsWith('/user')) {
await postLogout();
} else {
await postAdminLogout();
}
message.success('退出登录成功');
navigate(pathname.startsWith('/user') ? '/user/login' : '/login');
};
return (
<Stack
direction={'row'}
@@ -33,25 +43,22 @@ const Header = () => {
>
</Button>
<IconButton
title='退出登录'
size='small'
sx={{
bgcolor: '#fff',
<Tooltip title='退出登录' arrow>
<IconButton
title='退出登录'
size='small'
sx={{
bgcolor: '#fff',
'&:hover': {
color: 'primary.main',
},
}}
onClick={() => {
message.success('退出登录成功');
navigate(pathname.startsWith('/user/') ? '/user/login' : '/login', {
replace: true,
});
}}
>
<LogoutIcon sx={{ fontSize: 16 }} />
</IconButton>
'&:hover': {
color: 'primary.main',
},
}}
onClick={onLogout}
>
<LogoutIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</Stack>
</Stack>
);

25
ui/src/context/index.tsx Normal file
View File

@@ -0,0 +1,25 @@
import { createContext } from 'react';
import { DomainUser, DomainAdminUser } from '@/api/types';
export const AuthContext = createContext<
[
DomainUser | DomainAdminUser | null,
{
loading: boolean;
setUser: (user: DomainUser | DomainAdminUser) => void;
refreshUser: () => void;
}
]
>([
null,
{
setUser: () => {},
loading: true,
refreshUser: () => {},
},
]);
export const CommonContext = createContext<{
contactModalOpen: boolean;
setContactModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}>({ contactModalOpen: false, setContactModalOpen: () => {} });

10
ui/src/hooks/context.ts Normal file
View File

@@ -0,0 +1,10 @@
import { use } from 'react';
import { AuthContext, CommonContext } from '@/context';
export const useAuthContext = () => {
return use(AuthContext);
};
export const useCommonContext = () => {
return use(CommonContext);
};

View File

@@ -1,4 +1,5 @@
import ReactDOM from 'react-dom/client';
import { useState, useEffect } from 'react';
import 'dayjs/locale/zh-cn';
import { RouterProvider } from 'react-router-dom';
import '@/assets/fonts/font.css';
@@ -6,22 +7,86 @@ import '@/assets/fonts/iconfont';
import './index.css';
import '@/assets/styles/markdown.css';
import { ThemeProvider } from '@c-x/ui';
import { getUserProfile } from '@/api/UserManage';
import { getAdminProfile } from '@/api/Admin';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { AuthContext } from './context';
import { DomainUser, DomainAdminUser } from './api/types';
import { lightTheme } from './theme';
import router from './router';
import { getRedirectUrl } from './utils';
import { Loading } from '@c-x/ui';
dayjs.locale('zh-cn');
dayjs.extend(duration);
dayjs.extend(relativeTime);
const App = () => {
const [user, setUser] = useState<DomainUser | DomainAdminUser | null>(null);
const [loading, setLoading] = useState(true);
const onGotoRedirect = (source: 'user' | 'admin') => {
const redirectUrl = getRedirectUrl(source);
window.location.href = redirectUrl.href;
};
const getUser = () => {
setLoading(true);
if (location.pathname.startsWith('/user')) {
return getUserProfile()
.then((res) => {
setUser(res);
if (location.pathname.startsWith('/user/login')) {
onGotoRedirect('user');
}
})
.finally(() => {
setLoading(false);
});
} else {
return getAdminProfile()
.then((res) => {
console.log(res);
setUser(res);
if (location.pathname.startsWith('/login')) {
onGotoRedirect('admin');
}
})
.finally(() => {
setLoading(false);
});
}
};
useEffect(() => {
getUser();
}, []);
if (loading) {
return null;
}
return (
<ThemeProvider theme={lightTheme}>
<AuthContext.Provider
value={[
user,
{
loading,
setUser,
refreshUser: getUser,
},
]}
>
<RouterProvider router={router} />
</AuthContext.Provider>
</ThemeProvider>
);
};
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<ThemeProvider theme={lightTheme}>
<RouterProvider router={router} />
</ThemeProvider>
);
root.render(<App />);

View File

@@ -147,6 +147,7 @@ const AuthPage = () => {
const loginResult = await postLogin({
...data,
session_id: sessionId,
source: ConstsLoginSource.LoginSourcePlugin,
});
if (!loginResult.redirect_url) {

View File

@@ -26,7 +26,7 @@ import { useRequest } from 'ahooks';
import { postRegister, getUserOauthSignupOrIn } from '@/api/User';
import { getGetSetting } from '@/api/Admin';
import { Icon } from '@c-x/ui';
import { DomainSetting } from '@/api/types';
import { ConstsLoginSource, DomainSetting } from '@/api/types';
import DownloadIcon from '@mui/icons-material/Download';
import MenuBookIcon from '@mui/icons-material/MenuBook';
@@ -140,6 +140,7 @@ const Invite = () => {
platform,
redirect_url: `${window.location.origin}/invite/${id}/2`,
inviate_code: id,
source: 'plugin',
}).then((res) => {
if (res.url) {
window.location.href = res.url;

View File

@@ -16,6 +16,7 @@ import { postAdminLogin } from '@/api/Admin';
import { useForm, Controller } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { styled } from '@mui/material/styles';
import { ConstsLoginSource } from '@/api/types';
import { Icon } from '@c-x/ui';
import Logo from '@/assets/images/logo.png';
import { getRedirectUrl } from '@/utils';
@@ -71,6 +72,7 @@ const LoginPage = () => {
return postAdminLogin({
username: data.username,
password: data.password,
source: ConstsLoginSource.LoginSourceBrowser,
});
};