Merge pull request #156 from guanweiwang/main

feat: 用户登录和管理员登录合一起
This commit is contained in:
Yoko
2025-07-25 19:21:49 +08:00
committed by GitHub
6 changed files with 277 additions and 150 deletions

View File

@@ -45,9 +45,9 @@ const redirectToLogin = () => {
const redirectAfterLogin = encodeURIComponent(location.href);
const search = `redirect=${redirectAfterLogin}`;
const pathname = location.pathname.startsWith('/user')
? '/user/login'
: '/login';
window.location.href = `${pathname}?${search}`;
? '/login'
: '/login/admin';
window.location.href = `${pathname}`;
};
type ExtractDataProp<T> = T extends { data?: infer U } ? U : never

View File

@@ -65,9 +65,9 @@ const redirectToLogin = () => {
const redirectAfterLogin = encodeURIComponent(location.href);
const search = `redirect=${redirectAfterLogin}`;
const pathname = location.pathname.startsWith("/user")
? "/user/login"
: "/login";
window.location.href = `${pathname}?${search}`;
? "/login"
: "/login/admin";
window.location.href = `${pathname}`;
};
type ExtractDataProp<T> = T extends { data?: infer U } ? U : never;

View File

@@ -17,7 +17,7 @@ const Header = () => {
await postAdminLogout();
}
message.success('退出登录成功');
navigate(pathname.startsWith('/user') ? '/user/login' : '/login');
navigate(pathname.startsWith('/user') ? '/login/user' : '/login/admin');
};
return (
<Stack

View File

@@ -107,18 +107,26 @@ const App = () => {
const getUser = () => {
setLoading(true);
if (location.pathname.startsWith('/user')) {
if (
location.pathname.startsWith('/user') ||
location.pathname === '/login' ||
location.pathname === '/login/user'
) {
return getUserProfile()
.then((res) => {
setUser(res);
if (location.pathname.startsWith('/user/login')) {
if (location.pathname.startsWith('/login')) {
onGotoRedirect('user');
}
})
.finally(() => {
setLoading(false);
});
} else if (!location.pathname.startsWith('/auth')) {
} else if (
!location.pathname.startsWith('/auth') ||
!location.pathname.startsWith('/user') ||
location.pathname === '/login/admin'
) {
return getAdminProfile()
.then((res) => {
setUser(res);

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import {
Box,
Button,
@@ -6,20 +6,24 @@ import {
Typography,
Container,
Paper,
Alert,
CircularProgress,
Grid2 as Grid,
InputAdornment,
IconButton,
Stack,
Divider,
} from '@mui/material';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
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 { ConstsLoginSource, DomainSetting } from '@/api/types';
import { Icon, CusTabs } from '@c-x/ui';
import Logo from '@/assets/images/logo.png';
import { getRedirectUrl } from '@/utils';
import { getGetSetting } from '@/api/Admin';
import { postLogin, getUserOauthSignupOrIn } from '@/api/User';
import { useRequest } from 'ahooks';
// @ts-ignore
import { AestheticFluidBg } from '@/assets/jsm/AestheticFluidBg.module.js';
@@ -57,10 +61,24 @@ interface LoginFormData {
password: string;
}
type TabType = 'user' | 'admin';
const LoginPage = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { tab: tabParam } = useParams();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showPassword, setShowPassword] = useState(false);
const [tab, setTab] = useState<TabType>((tabParam as TabType) || 'user');
const { data: loginSetting = {} as DomainSetting } =
useRequest(getGetSetting);
const { custom_oauth = {}, dingtalk_oauth = {} } = loginSetting;
useEffect(() => {
if (tabParam) {
setTab(tabParam as TabType);
}
}, [tabParam]);
const {
control,
@@ -79,17 +97,80 @@ const LoginPage = () => {
// 处理登录表单提交
const onSubmit = async (data: LoginFormData) => {
setLoading(true);
setError(null);
try {
await loginUser(data);
const redirectUrl = getRedirectUrl();
window.location.href = redirectUrl.href;
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败,请重试');
} finally {
setLoading(false);
if (tab === 'admin') {
try {
await loginUser(data);
const redirectUrl = getRedirectUrl();
window.location.href = redirectUrl.href;
} finally {
setLoading(false);
}
}
if (tab === 'user') {
try {
// 用户登录
await postLogin({
...data,
source: ConstsLoginSource.LoginSourceBrowser,
});
const redirectUrl = getRedirectUrl('user');
window.location.href = redirectUrl.href;
} finally {
setLoading(false);
}
}
};
const oauthEnable = useMemo(() => {
return (
(loginSetting.custom_oauth?.enable ||
loginSetting.dingtalk_oauth?.enable) &&
tab === 'user'
);
}, [loginSetting, tab]);
const disablePasswordLogin = useMemo(() => {
return loginSetting.disable_password_login && tab === 'user';
}, [loginSetting, tab]);
const onOauthLogin = (platform: 'dingtalk' | 'custom') => {
const redirectUrl = getRedirectUrl('user');
getUserOauthSignupOrIn({
platform,
redirect_url: redirectUrl.href,
source: ConstsLoginSource.LoginSourceBrowser,
}).then((res) => {
if (res.url) {
window.location.href = res.url;
}
});
};
const oauthLogin = () => {
return (
<Stack justifyContent='center'>
<Divider sx={{ my: 3, fontSize: 12, borderColor: 'divider' }}>
使
</Divider>
{dingtalk_oauth.enable && (
<IconButton
sx={{ alignSelf: 'center' }}
onClick={() => onOauthLogin('dingtalk')}
>
<Icon type='icon-dingding' sx={{ fontSize: 30 }} />
</IconButton>
)}
{custom_oauth.enable && (
<IconButton
sx={{ alignSelf: 'center' }}
onClick={() => onOauthLogin('custom')}
>
<Icon type='icon-oauth' sx={{ fontSize: 30 }} />
</IconButton>
)}
</Stack>
);
};
useEffect(() => {
@@ -124,131 +205,169 @@ const LoginPage = () => {
>
Monkey Code
</Typography>
<Typography variant='body1' color='text.secondary' paragraph>
</Typography>
</LogoContainer>
<Box component='form' onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={5}>
<Grid size={12}>
<Controller
name='username'
control={control}
defaultValue=''
rules={{
required: '请输入用户名',
minLength: {
value: 2,
message: '用户名至少需要2个字符',
},
}}
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='请输入用户名'
variant='outlined'
error={!!errors.username}
helperText={errors.username?.message}
disabled={loading}
slotProps={{
input: {
startAdornment: (
<Icon
type='icon-zhanghao'
sx={{
color: 'text.primary',
mr: 1,
fontSize: 18,
}}
/>
),
},
}}
/>
)}
/>
</Grid>
<CusTabs
list={[
{ label: '普通账号', value: 'user' },
{ label: '管理员账号', value: 'admin' },
]}
value={tab}
onChange={(value: TabType) => {
setTab(value);
navigate(`/login/${value}?${searchParams.toString()}`);
}}
sx={{
width: '100%',
mb: 4,
height: 40,
border: 'none',
padding: '4px',
'.MuiTab-root': {
width: '50%',
height: 32,
fontSize: 14,
'&.Mui-selected': {
color: 'text.primary',
fontWeight: 500,
},
},
'.MuiTabs-scroller': {
height: 32,
},
'.MuiTabs-indicator': {
borderRadius: '10px',
height: 32,
backgroundColor: '#fff',
},
bgcolor: 'rgba(255, 255, 255, 0.2)',
borderRadius: '10px',
}}
/>
<Grid size={12}>
<Controller
name='password'
control={control}
defaultValue=''
rules={{
required: '请输入密码',
minLength: {
value: 3,
message: '密码至少需要3个字符',
},
}}
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='请输入密码'
type={showPassword ? 'text' : 'password'}
variant='outlined'
error={!!errors.password}
helperText={errors.password?.message}
disabled={loading}
slotProps={{
input: {
startAdornment: (
<Icon
type='icon-mima'
sx={{
color: 'text.primary',
mr: 1,
fontSize: 24,
}}
/>
),
endAdornment: (
<InputAdornment position='end'>
<IconButton
aria-label='切换密码显示'
onClick={() => setShowPassword(!showPassword)}
edge='end'
disabled={loading}
size='small'
>
{showPassword ? (
<Icon
type='icon-kejian'
sx={{ fontSize: 20 }}
/>
) : (
<Icon
type='icon-bukejian'
sx={{ fontSize: 20 }}
/>
)}
</IconButton>
</InputAdornment>
),
},
}}
/>
)}
/>
</Grid>
{!disablePasswordLogin && (
<Box component='form' onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={4}>
<Grid size={12}>
<Controller
name='username'
control={control}
defaultValue=''
rules={{
required: '请输入用户名',
minLength: {
value: 2,
message: '用户名至少需要2个字符',
},
}}
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='请输入用户名'
variant='outlined'
error={!!errors.username}
helperText={errors.username?.message}
disabled={loading}
slotProps={{
input: {
startAdornment: (
<Icon
type='icon-zhanghao'
sx={{
color: 'text.primary',
mr: 1,
fontSize: 18,
}}
/>
),
},
}}
/>
)}
/>
</Grid>
<Grid size={12}>
<Button
type='submit'
fullWidth
variant='contained'
size='large'
disabled={loading}
sx={{ height: 48, textTransform: 'none' }}
>
{loading ? <CircularProgress size={18} /> : '登录'}
</Button>
<Grid size={12}>
<Controller
name='password'
control={control}
defaultValue=''
rules={{
required: '请输入密码',
minLength: {
value: 3,
message: '密码至少需要3个字符',
},
}}
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='请输入密码'
type={showPassword ? 'text' : 'password'}
variant='outlined'
error={!!errors.password}
helperText={errors.password?.message}
disabled={loading}
slotProps={{
input: {
startAdornment: (
<Icon
type='icon-mima'
sx={{
color: 'text.primary',
mr: 1,
fontSize: 24,
}}
/>
),
endAdornment: (
<InputAdornment position='end'>
<IconButton
aria-label='切换密码显示'
onClick={() => setShowPassword(!showPassword)}
edge='end'
disabled={loading}
size='small'
>
{showPassword ? (
<Icon
type='icon-kejian'
sx={{ fontSize: 20 }}
/>
) : (
<Icon
type='icon-bukejian'
sx={{ fontSize: 20 }}
/>
)}
</IconButton>
</InputAdornment>
),
},
}}
/>
)}
/>
</Grid>
<Grid size={12}>
<Button
type='submit'
fullWidth
variant='contained'
size='large'
disabled={loading}
sx={{ height: 48, textTransform: 'none' }}
>
{loading ? <CircularProgress size={18} /> : '登录'}
</Button>
</Grid>
</Grid>
</Grid>
</Box>
</Box>
)}
{oauthEnable && oauthLogin()}
</StyledPaper>
</StyledContainer>
);

View File

@@ -123,12 +123,12 @@ const routerConfig = [
path: '/auth',
element: <Auth />,
},
// {
// path: '/user/login',
// element: <UserLogin />,
// },
{
path: '/user/login',
element: <UserLogin />,
},
{
path: '/login',
path: '/login/:tab?',
element: <Login />,
},
];