对接登录和退出登录

This commit is contained in:
linxd
2025-07-17 13:21:00 +08:00
parent 60ccbf659c
commit 907dd03023
10 changed files with 604 additions and 357 deletions

View File

@ -1,24 +1,41 @@
import { useState } from "react";
import { useState, useEffect } from "react";
// 获取用户信息 这里没用dva, 因为包装组件写法复杂(当前框架),尤其是有form表单组件时
export const useUser = () => {
const [user, setUser] = useState<any>({
role: 'admin1', // 模拟用户权限
role: 'admin1', // 初始默认值
});
const getUserInfo = ()=>{
return user;
}
const [loading, setLoading] = useState(false);
// 获取用户信息
const getUserInfo = async () => {
return JSON.parse(sessionStorage.getItem('userInfo') || user || '{}');
};
const setUserInfo = (userInfo: any) => {
sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
setUser(userInfo);
}
const getUserRole = ()=>{
};
const getUserRole = () => {
return user.role;
}
};
const setToken = (token: string) => {
sessionStorage.setItem('token', token);
}
};
const getToken = () => {
return sessionStorage.getItem('token');
}
return { user, getUserInfo, setUserInfo, getUserRole, setToken, getToken };
};
return {
user,
loading,
getUserInfo,
setUserInfo,
getUserRole,
setToken,
getToken
};
};

View File

@ -1,11 +1,70 @@
import React from 'react';
import { Link, useIntl } from 'umi';
const User: React.FC = (props) => {
import React, { useEffect, useState } from 'react';
import { Link, useIntl, connect, history } from 'umi';
import { Dropdown, Button, Modal } from 'antd';
import { DownOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import type { ConnectProps, Dispatch } from 'umi';
import type { UserModelState } from '@/models/user';
import { message } from 'antd';
interface PageProps extends ConnectProps {
user: UserModelState; // dva model状态
dispatch: Dispatch; // dva dispatch方法
}
const User: React.FC<PageProps> = ({ user, dispatch }) => {
const intl = useIntl();
useEffect(() => {
if (user.token) {
dispatch({
type: 'user/fetchUserInfo',
});
}
}, [user.token]);
const handleMenuClick = (e: any) => {
if (e.key === 'logout') {
Modal.confirm({
title: '确定退出登录吗?',
icon: <ExclamationCircleOutlined />,
content: '退出登录后,您将需要重新登录',
onOk() {
dispatch({
type: 'user/logout',
}).then(() => {
sessionStorage.clear();
message.success('退出登录成功');
history.push('/login');
});
},
onCancel() {
return;
},
});
}
};
return (
<div className="user">
<Link to={'/login'}>{intl.formatMessage({ id: '登录' })}</Link>
{user.userInfo?.fullName ? (
<Dropdown
trigger={['hover']}
menu={{
items: [
{
key: 'logout',
label: '退出登录',
},
],
onClick: handleMenuClick,
}}
>
<Button type="link">
{`${user.userInfo?.fullName}`}
<DownOutlined />
</Button>
</Dropdown>
) : (
<Link to={'/login'}>{intl.formatMessage({ id: '登录' })}</Link>
)}
</div>
);
};
export default User;
export default connect(({ user }: { user: UserModelState }) => ({ user }))(User);

View File

@ -1,6 +1,8 @@
import breadcrumb from './breadcrumb';
import user from './user';
export default {
breadcrumb,
user,
// ... 其他 models
};

90
src/models/user.ts Normal file
View File

@ -0,0 +1,90 @@
import type { Effect, Reducer } from 'umi';
import { getUserinfo, Logout } from '@/servers/api/login';
export type User = {
userId: string;
userName: string;
userType: string;
};
export type UserInfo = {
userId: string;
fullName: string;
loginName: string;
userType: string;
authorityList: { roleId: string, roleName: string, roleCode: string, roleScope: string }[];
};
export type UserModelState = {
user?: User;
token?: string;
userInfo?: UserInfo;
};
export type UserModelType = {
namespace: 'user';
state: UserModelState;
effects: {
fetchUserInfo: Effect;
logout: Effect;
};
reducers: {
saveLoginUser: Reducer<UserModelState>;
saveUserInfo: Reducer<UserModelState>;
clearUserInfo: Reducer;
};
};
const UserModel: UserModelType = {
namespace: 'user',
state: {
user: undefined, // 登录返回的用户信息
token: sessionStorage.getItem('token') || '', // 登录返回的token
userInfo: undefined, // 请求userInfo返回的用户信息
},
effects: {
*fetchUserInfo(_, { call, put }) {
const response = yield call(getUserinfo);
yield put({
type: 'saveUserInfo',
payload: response,
});
},
*logout(_, { call, put }) {
yield call(Logout);
yield put({
type: 'clearUserInfo'
});
},
},
reducers: {
clearUserInfo(state) {
return {
...state,
userInfo: undefined,
token: '',
user: {},
};
},
saveLoginUser(state, action) {
return {
...state,
user: action.payload.user || {},
token: action.payload.token || '',
};
},
saveUserInfo(state, action) {
return {
...state,
userInfo: action.payload || {},
};
},
}
};
export default UserModel;

View File

@ -1,52 +1,49 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { Form, Input, Button, Checkbox, Tabs, message } from 'antd';
import { UserOutlined, LockOutlined, EyeInvisibleOutlined, EyeTwoTone, HomeOutlined } from '@ant-design/icons';
import { history, useIntl } from 'umi';
import {
UserOutlined,
LockOutlined,
EyeInvisibleOutlined,
EyeTwoTone,
HomeOutlined,
} from '@ant-design/icons';
import { history, useIntl, connect } from 'umi';
import type { ConnectProps, Dispatch } from 'umi';
import './login.less';
import { getCaptcha, supplierLogin, expertLogin, accountLogin } from '@/servers/api/login';
import { encryptWithRsa } from '@/utils/encryptWithRsa'
import { getCaptcha, supplierLogin, getUserinfo, findMenuList ,accountLogin} from '@/servers/api/login';
import { encryptWithRsa } from '@/utils/encryptWithRsa';
import type { UserModelState } from '@/models/user';
const { TabPane } = Tabs;
const LoginPage: React.FC = () => {
const [activeKey, setActiveKey] = useState('supplierLogin');
interface PageProps extends ConnectProps {
user: UserModelState; // dva model状态
dispatch: Dispatch; // dva dispatch方法
}
const LoginPage: React.FC<PageProps> = ({ user, dispatch }) => {
const [activeKey, setActiveKey] = useState('agent');
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [captchaImg, setCaptchaImg] = useState<string>('');
const [captchaKey, setCaptchaKey] = useState<string>('');
//切换后 走不同接口
const loginApiMap: { [key: string]: (params: any) => Promise<any> } = {
supplierLogin,
expertLogin,
accountLogin
};
const intl = useIntl();
const [captchaImg, setCaptchaImg] = useState('');
const [captchaKey, setCaptchaKey] = useState('');
const onFinish = async (values: any) => {
setLoading(true);
try {
const params = {
...values,
password: encryptWithRsa(values.password, false),
encryptValue: encryptWithRsa(values.identifying)
encryptValue: encryptWithRsa(values.identifying),
};
const loginRes = await loginApiMap[activeKey](params);
const loginRes = await accountLogin(params);
if (loginRes.code === 200) {
sessionStorage.setItem('token', loginRes.data.token);
//存入供应商用户id
if(activeKey === 'supplierLogin') {
sessionStorage.setItem('userId', loginRes.data.supplierUser.userId);
} else if(activeKey === 'expertLogin') {
//存入专家用户id
// sessionStorage.setItem('userId', loginRes.data.expertUser.userId);
} else if(activeKey === 'accountLogin') {
//存入招标代理用户id
// sessionStorage.setItem('userId', loginRes.data.supplierUser.userId);
}
sessionStorage.setItem('userId', loginRes.data.user.userId);
sessionStorage.setItem('currentUser', JSON.stringify(loginRes.data));
dispatch({
type: 'user/saveLoginUser',
payload: loginRes.data,
})
message.success('登录成功');
history.push('/index');
} else {
@ -57,19 +54,6 @@ const LoginPage: React.FC = () => {
}
};
const fetchCaptcha = async () => {
const res = await getCaptcha();
if (res.code === 200) {
setCaptchaImg(res.data.base64Image);
setCaptchaKey(res.data.code);
}
};
useEffect(() => {
fetchCaptcha();
}, [activeKey]);
const handleTabChange = (key: string) => {
setActiveKey(key);
form.resetFields();
@ -77,11 +61,11 @@ const LoginPage: React.FC = () => {
// 根据当前选中的Tab决定跳转到哪个注册页面
const handleRegister = () => {
switch(activeKey) {
case 'supplierLogin':
switch (activeKey) {
case 'supplier':
history.push('/register/supplier');
break;
case 'expertLogin':
case 'expert':
history.push('/register/expert');
break;
default:
@ -92,49 +76,62 @@ const LoginPage: React.FC = () => {
// 渲染注册链接只在供应商和专家Tab下显示
const renderRegisterLink = () => {
if (activeKey === 'agent') {
if (activeKey === 'agent' || activeKey === 'expert') {
return null; // 招标代理不显示注册链接
}
return (
<div className="register-link">
{intl.formatMessage({ id: 'login.register.tip' })}
<a onClick={handleRegister}>
{intl.formatMessage({ id: 'login.register.action' })}
</a>
<a onClick={handleRegister}>{intl.formatMessage({ id: 'login.register.action' })}</a>
</div>
);
};
// 获取验证码
const fetchCaptcha = async () => {
const res = await getCaptcha();
if (res.code === 200) {
setCaptchaImg(res.data.base64Image);
setCaptchaKey(res.data.code);
}
};
useEffect(() => {
fetchCaptcha();
}, [activeKey]);
return (
<div className='login-page'>
<div className='login-container'>
{/* <div className='back-home'>
<div className="login-page">
<div className="login-container">
<div className="back-home">
<a onClick={() => history.push('/index')}>
<HomeOutlined /> {intl.formatMessage({ id: 'login.back.home' })}
</a>
</div> */}
<div className='login-title'>{intl.formatMessage({ id: 'login.title' })}</div>
<div className="login-tab-container">
<Tabs activeKey={activeKey} onChange={handleTabChange} className='login-tabs'>
<TabPane tab={intl.formatMessage({ id: 'login.tab.supplier' })} key="supplierLogin" />
<TabPane tab={intl.formatMessage({ id: 'login.tab.expert' })} key="expertLogin" />
<TabPane tab={intl.formatMessage({ id: 'login.tab.agent' })} key="accountLogin" />
</Tabs>
</div>
<div className="login-title">{intl.formatMessage({ id: 'login.title' })}</div>
{/* <div className="login-tab-container">
<Tabs activeKey={activeKey} onChange={handleTabChange} className='login-tabs'>
<TabPane tab={intl.formatMessage({ id: 'login.tab.supplier' })} key="supplier" />
<TabPane tab={intl.formatMessage({ id: 'login.tab.expert' })} key="expert" />
<TabPane tab={intl.formatMessage({ id: 'login.tab.agent' })} key="agent" />
</Tabs>
</div> */}
<Form
form={form}
name="login"
className='login-form'
className="login-form"
initialValues={{ remember: false }}
onFinish={onFinish}
>
<Form.Item
name="account"
rules={[{ required: true, message: intl.formatMessage({ id: 'login.username.placeholder' }) + '!' }]}
rules={[
{
required: true,
message: intl.formatMessage({ id: 'login.username.placeholder' }) + '!',
},
]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" />}
@ -145,18 +142,28 @@ const LoginPage: React.FC = () => {
<Form.Item
name="password"
rules={[{ required: true, message: intl.formatMessage({ id: 'login.password.placeholder' }) + '!' }]}
rules={[
{
required: true,
message: intl.formatMessage({ id: 'login.password.placeholder' }) + '!',
},
]}
>
<Input.Password
prefix={<LockOutlined className="site-form-item-icon" />}
placeholder={intl.formatMessage({ id: 'login.password.placeholder' })}
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
iconRender={(visible) => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
size="large"
/>
</Form.Item>
<Form.Item
name="identifying"
rules={[{ required: true, message: intl.formatMessage({ id: 'login.captcha.placeholder' }) + '!' }]}
rules={[
{
required: true,
message: intl.formatMessage({ id: 'login.captcha.placeholder' }) + '!',
},
]}
>
<Input
placeholder={intl.formatMessage({ id: 'login.captcha.placeholder' })}
@ -166,7 +173,7 @@ const LoginPage: React.FC = () => {
prefix={null}
suffix={
<img
src={`data:image/png;base64,${captchaImg}`}
src={`data:image/png;base64,${captchaImg}`}
alt="验证码"
style={{ cursor: 'pointer', height: 32, verticalAlign: 'middle' }}
onClick={fetchCaptcha}
@ -176,7 +183,7 @@ const LoginPage: React.FC = () => {
</Form.Item>
<Form.Item>
<div className='login-options'>
<div className="login-options">
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>{intl.formatMessage({ id: 'login.remember' })}</Checkbox>
</Form.Item>
@ -187,10 +194,16 @@ const LoginPage: React.FC = () => {
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button" loading={loading} size="large">
<Button
type="primary"
htmlType="submit"
className="login-form-button"
loading={loading}
size="large"
>
{intl.formatMessage({ id: 'login.button' })}
</Button>
{activeKey !== 'accountLogin' && renderRegisterLink()}
{renderRegisterLink()}
</Form.Item>
</Form>
</div>
@ -198,4 +211,4 @@ const LoginPage: React.FC = () => {
);
};
export default LoginPage;
export default connect(({ user }: { user: UserModelState }) => ({ user }))(LoginPage);

View File

@ -37,5 +37,37 @@ export async function supplierLogin (data: API.LoginSupplier) {
data
});
}
/**
* 用户信息
*/
export async function getUserinfo() {
return request('/v1/userinfo/get', {
method: 'GET'
});
}
/**
* 退出
*/
export async function Logout() {
return request('/v1/login/logout', {
method: 'POST'
});
}
/**
* 路由
*/
export async function findMenuList(data: any) {
return request('/v1/menu/findMenuList', {
method: 'POST',
data
});
}
/**
* 部门
*/
export async function queryUserOrgAll() {
return request('/org/queryUserOrgAll', {
method: 'GET'
});
}

View File

@ -1,10 +1,11 @@
/**
* request 网络请求工具
* 更详细的 api 文档: https://github.com/umijs/umi-request
* 更详细的 api 文档: https://github.com/umijs/umi-request
*/
import { extend } from 'umi-request';
import { message } from 'antd';
import { history } from 'umi';
import { useUser } from '@/hooks/useUser';
const codeMessage = {
200: '服务器成功返回请求的数据。',
@ -24,6 +25,11 @@ const codeMessage = {
504: '网关超时。',
};
// 获取Token的函数
const getToken = () => {
return sessionStorage.getItem('token');
};
/**
* 异常处理程序
*/
@ -58,45 +64,28 @@ const request = extend({
});
// request拦截器, 改变url 或 options.
request.interceptors.request.use((url, options) => {
if (
options.method === 'post' ||
options.method === 'put' ||
options.method === 'delete' ||
options.method === 'get'
) {
//如果是获取token的url则不加token
// headers = {
// Authorization: getUserToken() == null ? null : getUserToken(),
// JwtToken: getSessionUserData() == null ? null : getSessionUserData().userId,
// ...options.headers
// }
// options.headers = headers;
}
// 获取 token
const token = sessionStorage.getItem('token');
const userId = sessionStorage.getItem('userId');
return {
url: url,
options: { ...options },
url,
options: {
...options,
headers: {
...(options.headers || {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(userId ? { Mall3Check: `${userId}` } : {}),
},
},
};
});
//response拦截
request.interceptors.response.use(async (response) => {
const data = await response.clone().json();
//返回请求头状态码为401
if (
(response.status === 401 && data?.code == '90401') ||
(response.status === 500 && data?.code == '90500')
) {
if ((response.status === 401) || (response.status === 500)) {
history.replace({
pathname: '/401',
query: {
code: '90401',
},
pathname: '/login',
});
} else {
//2021.9.7 zhoujianlong 新增风险防控专用错误码4004
if (data.code != undefined && data.code != '200' && data.code != '4004' && data.code !== '1') {
message.error(data.message, 3);
}
}
return response;
});

View File

@ -1,16 +1,15 @@
import React from 'react';
import { Redirect } from 'umi';
import { message } from 'antd';
import { useUser } from '@/hooks/useUser';
import { connect, Redirect, history } from 'umi';
import { message, Modal } from 'antd';
// 权限校验
export default (props: any) => {
const { getToken } = useUser();
if (getToken()) {
return <div>{props.children}</div>;
const AuthWrapper = ({ user, children }: any) => {
if (user.token) {
return <div>{children}</div>;
} else {
// 提示后跳转
message.error('请先登录');
return <Redirect to="/login" />;
}
};
export default connect(({ user }: any) => ({
user,
}))(AuthWrapper);