接入登录功能

This commit is contained in:
linxd
2025-07-14 14:00:12 +08:00
parent cd1ab80a5d
commit aa8349d549
14 changed files with 191 additions and 336 deletions

View File

@ -1,13 +1,18 @@
export default { export default {
dev: { dev: {
'/api/v1': {
target: 'http://10.0.0.10:18030',// 茂
changeOrigin: true,
pathRewrite: { '^/api/v1': '/v1' },
},
'/api': { '/api': {
// target: 'http://10.242.37.148:18022',// // target: 'http://10.242.37.148:18022',//
target: 'http://10.0.0.14:18013',// target: 'http://10.0.0.10:18013',//
changeOrigin: true, changeOrigin: true,
pathRewrite: { '^/api': '' }, pathRewrite: { '^/api': '' },
}, },
'/upload': { '/upload': {
target: 'http://10.0.0.14:18013',// target: 'http://10.0.0.10:18013',//
changeOrigin: true, changeOrigin: true,
pathRewrite: { '^/upload': '' }, pathRewrite: { '^/upload': '' },
}, },

View File

@ -70,9 +70,11 @@
"array-move": "3.0.1", "array-move": "3.0.1",
"axios": "0.21.1", "axios": "0.21.1",
"classnames": "2.3.1", "classnames": "2.3.1",
"dayjs": "^1.11.13",
"dva": "2.4.1", "dva": "2.4.1",
"echarts": "^5.2.2", "echarts": "^5.2.2",
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"jsencrypt": "^3.3.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"omit.js": "2.0.2", "omit.js": "2.0.2",

10
pnpm-lock.yaml generated
View File

@ -50,6 +50,9 @@ dependencies:
classnames: classnames:
specifier: 2.3.1 specifier: 2.3.1
version: 2.3.1 version: 2.3.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
dva: dva:
specifier: 2.4.1 specifier: 2.4.1
version: 2.4.1(react-dom@16.14.0)(react@16.14.0) version: 2.4.1(react-dom@16.14.0)(react@16.14.0)
@ -59,6 +62,9 @@ dependencies:
echarts-for-react: echarts-for-react:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.2(echarts@5.6.0)(react@16.14.0) version: 3.0.2(echarts@5.6.0)(react@16.14.0)
jsencrypt:
specifier: ^3.3.2
version: 3.3.2
lodash: lodash:
specifier: 4.17.21 specifier: 4.17.21
version: 4.17.21 version: 4.17.21
@ -10230,6 +10236,10 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/jsencrypt@3.3.2:
resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==}
dev: false
/jsesc@2.5.2: /jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'} engines: {node: '>=4'}

View File

@ -1,7 +1,7 @@
// src/layouts/BasicLayout.tsx // src/layouts/BasicLayout.tsx
import React from 'react'; import React from 'react';
import ProLayout, { PageContainer } from '@ant-design/pro-layout'; import ProLayout, { PageContainer } from '@ant-design/pro-layout';
import { Link, useLocation, useIntl, useHistory } from 'umi'; import { Link, useLocation, useIntl } from 'umi';
import { connect } from 'dva'; import { connect } from 'dva';
import defaultSettings from '../../config/defaultSettings'; import defaultSettings from '../../config/defaultSettings';
import routes from '../../config/router.config'; // 引入你的自定义路由结构 import routes from '../../config/router.config'; // 引入你的自定义路由结构
@ -9,7 +9,7 @@ import { ConfigProvider, Breadcrumb } from 'antd';
import HeaderComponent from './Header'; import HeaderComponent from './Header';
import IconFont from '@/components/IconFont/IconFont'; import IconFont from '@/components/IconFont/IconFont';
import type { BreadcrumbState } from '@/models/breadcrumb'; import type { BreadcrumbState } from '@/models/breadcrumb';
import type { TabModelState, TabItem } from '@/models/tab'; import type { TabModelState } from '@/models/tab';
const MenuRender = (item: any, isSubMenu: boolean) => { const MenuRender = (item: any, isSubMenu: boolean) => {
const intl = useIntl(); const intl = useIntl();
@ -72,12 +72,9 @@ interface BasicLayoutProps {
} }
const BasicLayout: React.FC<BasicLayoutProps> = (props) => { const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
const { children, breadcrumb, tab, dispatch } = props; const { children, tab, dispatch } = props;
const location = useLocation(); const location = useLocation();
const intl = useIntl(); const intl = useIntl();
const history = useHistory();
console.log('tab model state:', tab);
const handleTabChange = (key: string) => { const handleTabChange = (key: string) => {
dispatch({ dispatch({
@ -121,9 +118,12 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
// BreadcrumbRender(routeBreadcrumb, intl, history, breadcrumb.breadcrumbName), // BreadcrumbRender(routeBreadcrumb, intl, history, breadcrumb.breadcrumbName),
}} }}
// 将tab.tabList转换为需要的格式添加国际化处理 // 将tab.tabList转换为需要的格式添加国际化处理
tabList={tab.tabList.map(item => ({ tabList={tab.tabList.map((item) => ({
...item, ...item,
tab: typeof item.tab === 'string' ? intl.formatMessage({ id: `menu.${item.tab}` }) : item.tab tab:
typeof item.tab === 'string'
? intl.formatMessage({ id: `menu.${item.tab}` })
: item.tab,
}))} }))}
tabProps={{ tabProps={{
type: 'editable-card', type: 'editable-card',

View File

@ -13,4 +13,5 @@ export default {
"login.register.tip": "还没有账号?", "login.register.tip": "还没有账号?",
"login.register.action": "立即注册", "login.register.action": "立即注册",
"login.back.home": "返回首页", "login.back.home": "返回首页",
"login.captcha.placeholder": "请输入验证码",
}; };

View File

@ -146,6 +146,8 @@ const TabModel: TabModelType = {
setup({ dispatch }) { setup({ dispatch }) {
return history.listen((location) => { return history.listen((location) => {
const { pathname } = location; const { pathname } = location;
// 需要排除的路由
const excludeRoutes = ['/login', '/register'];
// 首页不做特殊处理 // 首页不做特殊处理
if (pathname === '/') { if (pathname === '/') {
@ -161,7 +163,7 @@ const TabModel: TabModelType = {
// 查找当前路由对应的菜单项 // 查找当前路由对应的菜单项
const currentRoute = findRouteByPath(pathname, routes); const currentRoute = findRouteByPath(pathname, routes);
if (currentRoute) { if (currentRoute && !excludeRoutes.includes(pathname)) {
// 如果找到对应路由添加或激活对应的tab // 如果找到对应路由添加或激活对应的tab
dispatch({ dispatch({
type: 'addTab', type: 'addTab',

View File

@ -289,8 +289,8 @@ const FriendLinkManage: React.FC = () => {
}, },
{ {
title: intl.formatMessage({ id: 'friendLink.category' }), title: intl.formatMessage({ id: 'friendLink.category' }),
dataIndex: 'categoryName', dataIndex: 'classificationName',
key: 'categoryName', key: 'classificationName',
}, },
{ {
title: intl.formatMessage({ id: 'friendLink.url' }), title: intl.formatMessage({ id: 'friendLink.url' }),

View File

@ -1,17 +0,0 @@
import React from 'react';
const LinkComponent: React.FC = () => {
return (
<div className="link">
<div></div>
<div className="flex">
<a href="https://www.baidu.com"></a>
<a href="https://www.baidu.com"></a>
<a href="https://www.baidu.com"></a>
<a href="https://www.baidu.com"></a>
<a href="https://www.baidu.com"></a>
</div>
</div>
);
};
export default LinkComponent;

View File

@ -1,53 +0,0 @@
@import '../../baseStyle.less';
.lastDate {
color: @main-danger-color;
}
.tableAddress {
color: @main-color;
font-weight: 600;
}
.tableLoadMore {
margin-top: 15px;
text-align: center;
}
.blockTitle {
font-weight: 600;
font-size: 16px;
line-height: 30px;
}
.questionItem {
line-height: 40px;
.icon {
margin-right: 10px;
font-size: 18px;
}
}
.cardContent {
color: @main-text-color-2;
}
.card {
.ant-card-head {
min-height: 40px;
padding: 0 15px;
.ant-card-head-title,
.ant-card-extra {
padding: 10px 0;
}
}
.ant-card-body {
min-height: 150px;
text-indent: 26px;
}
}
.link {
display: flex;
margin-top: 10px;
.flex {
margin-left: 10px;
display: flex;
flex: 1;
a{
margin: 0 10px;
}
}
}

View File

@ -1,242 +1,10 @@
import React, { useState } from 'react'; import React from 'react';
import { Card, Row, Col, Tabs, Table } from 'antd'; import styles from './index.less';
import { useIntl, Link } from 'umi';
import './index.less';
import IconFont from '@/components/IconFont/IconFont';
import LinkComponent from './Link';
const IndexPage: React.FC = () => { const IndexPage: React.FC = () => {
const intl = useIntl();
const [noticeLoading, setNoticeLoading] = useState(false);
const [noticeList, setNoticeList] = useState([
{
title: 'CA使用通知',
id: '1',
content: '系统将于2022年5月27日期开始对全流程使用CA服务届时全部投标供应商需办理CA。',
},
{
title: '5月27日系统优化升级通知',
id: '2',
content:
'系统将于2022年5月27日周五22:00--2022年5月28日周六6:00进行系统优化升级届时系统将暂停服务。',
},
{
title: '测试标题123123',
id: '3',
content: '测试内容124145',
},
{
title: '测试标题45435',
id: '4',
content: '测试内容6666',
},
]);
const tabList = [
{
key: '1',
label: intl.formatMessage({ id: '采购需求公示' }),
},
{
key: '2',
label: intl.formatMessage({ id: '招标采购公告' }),
},
{
key: '3',
label: intl.formatMessage({ id: '非招标采购公告' }),
},
{
key: '4',
label: intl.formatMessage({ id: '资格预审公告' }),
},
{
key: '5',
label: intl.formatMessage({ id: '招募公告' }),
},
{
key: '6',
label: intl.formatMessage({ id: '变更公告' }),
},
{
key: '7',
label: intl.formatMessage({ id: '中标(中选)候选人公示' }),
},
{
key: '8',
label: intl.formatMessage({ id: '中标(中选)结果公示' }),
},
{
key: '9',
label: intl.formatMessage({ id: '采购失败(流标)公告' }),
},
];
//tab 切换事件
const tabChange = (key: string) => {
console.log(key);
};
const [tableLoading, setTableLoading] = useState(false);
const dataSource = [
{
key: '1',
title: '中远海运空运北方物流基地标识制作及安装服务',
address: '西湖区湖底公园1号',
date: '2025年01月23日',
lastDate: '剩余3天4小时',
},
{
key: '2',
title: '中远海运空运北方物流基地标识制作及安装服务',
address: '西湖区湖底公园1号',
date: '2025年01月23日',
lastDate: '剩余3天4小时',
},
{
key: '2',
title: '中远海运空运北方物流基地标识制作及安装服务',
address: '西湖区湖底公园1号',
date: '2025年01月23日',
lastDate: '剩余3天4小时',
},
{
key: '2',
title: '中远海运空运北方物流基地标识制作及安装服务',
address: '西湖区湖底公园1号',
date: '2025年01月23日',
lastDate: '剩余3天4小时',
},
{
key: '2',
title: '中远海运空运北方物流基地标识制作及安装服务',
address: '西湖区湖底公园1号',
date: '2025年01月23日',
lastDate: '剩余3天4小时',
},
{
key: '2',
title: '中远海运空运北方物流基地标识制作及安装服务',
address: '西湖区湖底公园1号',
date: '2025年01月23日',
lastDate: '剩余3天4小时',
},
];
const columns = [
{
title: '项目所在地',
dataIndex: 'address',
key: 'address',
},
{
title: '公告标题',
dataIndex: 'title',
key: 'title',
render: (text: string, record) => (
<Link
to={{
pathname: '/announce/announceInfo',
search: '?id=' + record.id,
}}
>
{text}
</Link>
),
},
{
title: '发布时间',
dataIndex: 'date',
key: 'date',
},
{
title: '文件购买截止时间',
dataIndex: 'lastDate',
key: 'lastDate',
render: (text: string) => <span className="lastDate">{text}</span>,
},
];
return ( return (
<div> <div className="common-container">
{/* 通知列表 */}
<Row gutter={20}>
{noticeList.map((item) => (
<Col span={6} key={item.id}>
<Card
title={item.title}
loading={noticeLoading}
hoverable
className="card"
bodyStyle={{ padding: 10 }}
extra={
<Link
to={{
pathname: '/notice/noticeInfo',
search: '?id=' + item.id,
}}
>
{intl.formatMessage({ id: '查看' })}
</Link>
}
>
<p className="cardContent">{item.content}</p>
</Card>
</Col>
))}
</Row>
<Tabs onChange={tabChange}>
{tabList.map((item) => (
<Tabs.TabPane tab={item.label} key={item.key} />
))}
</Tabs>
<Table loading={tableLoading} dataSource={dataSource} columns={columns} pagination={false} />
<div className="tableLoadMore">
<Link
to={{
pathname: '/announce',
}}
>
{intl.formatMessage({ id: '加载更多' })}
</Link>
</div>
<Row style={{ marginTop: '20px', backgroundColor: '#fff' }}>
<Col span={12}>
<div className="blockTitle"></div>
<img src="" alt="" />
<div className="questionItem">
<IconFont type="icon-dizhi" className="icon" />
173
</div>
<div className="questionItem">
<IconFont type="icon-dianhua" className="icon" />
17676373746
</div>
<div className="questionItem">
<IconFont type="icon-youxiang" className="icon" />
i723648723@383.com
</div>
</Col>
<Col span={12}>
<div className="blockTitle">CA服务</div>
<Row>
<Col span={6} offset={6}>
<span>CA办理</span>
</Col>
<Col span={6}>
<span>CA客服</span>
</Col>
</Row>
<div className="blockTitle"></div>
<p style={{ marginTop: 20 }}>客服1: 400-300-9989</p>
<p>客服1: 400-300-9989</p>
<p>客服1: 400-300-9989</p>
<p>客服1: 400-300-9989</p>
</Col>
</Row>
<div>
<LinkComponent />
</div>
</div> </div>
); );
}; };

View File

@ -1,28 +1,75 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Checkbox, Tabs, message } from 'antd'; import { Form, Input, Button, Checkbox, Tabs, message } from 'antd';
import { UserOutlined, LockOutlined, EyeInvisibleOutlined, EyeTwoTone, HomeOutlined } from '@ant-design/icons'; import { UserOutlined, LockOutlined, EyeInvisibleOutlined, EyeTwoTone, HomeOutlined } from '@ant-design/icons';
import { history, useIntl } from 'umi'; import { history, useIntl } from 'umi';
import './login.less'; import './login.less';
import { getCaptcha, supplierLogin, expertLogin, accountLogin } from '@/servers/api/login';
import { encryptWithRsa } from '@/utils/encryptWithRsa'
const { TabPane } = Tabs; const { TabPane } = Tabs;
const LoginPage: React.FC = () => { const LoginPage: React.FC = () => {
const [activeKey, setActiveKey] = useState('supplier'); const [activeKey, setActiveKey] = useState('supplierLogin');
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); 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 intl = useIntl();
const onFinish = (values: any) => {
const onFinish = async (values: any) => {
setLoading(true); setLoading(true);
console.log('登录信息:', values); try {
// 这里添加登录逻辑 const params = {
setTimeout(() => { ...values,
setLoading(false); password: encryptWithRsa(values.password, false),
encryptValue: encryptWithRsa(values.identifying)
};
const loginRes = await loginApiMap[activeKey](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('currentUser', JSON.stringify(loginRes.data));
message.success('登录成功'); message.success('登录成功');
history.push('/index'); history.push('/index');
}, 1000); } else {
message.error(loginRes.message || '登录失败');
}
} finally {
setLoading(false);
}
}; };
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) => { const handleTabChange = (key: string) => {
setActiveKey(key); setActiveKey(key);
form.resetFields(); form.resetFields();
@ -31,10 +78,10 @@ const LoginPage: React.FC = () => {
// 根据当前选中的Tab决定跳转到哪个注册页面 // 根据当前选中的Tab决定跳转到哪个注册页面
const handleRegister = () => { const handleRegister = () => {
switch(activeKey) { switch(activeKey) {
case 'supplier': case 'supplierLogin':
history.push('/register/supplier'); history.push('/register/supplier');
break; break;
case 'expert': case 'expertLogin':
history.push('/register/expert'); history.push('/register/expert');
break; break;
default: default:
@ -62,18 +109,19 @@ const LoginPage: React.FC = () => {
return ( return (
<div className='login-page'> <div className='login-page'>
<div className='login-container'> <div className='login-container'>
<div className='back-home'> {/* <div className='back-home'>
<a onClick={() => history.push('/index')}> <a onClick={() => history.push('/index')}>
<HomeOutlined /> {intl.formatMessage({ id: 'login.back.home' })} <HomeOutlined /> {intl.formatMessage({ id: 'login.back.home' })}
</a> </a>
</div> </div> */}
<div className='login-title'>{intl.formatMessage({ id: 'login.title' })}</div> <div className='login-title'>{intl.formatMessage({ id: 'login.title' })}</div>
<div className="login-tab-container"> <div className="login-tab-container">
<Tabs activeKey={activeKey} onChange={handleTabChange} className='login-tabs'> <Tabs activeKey={activeKey} onChange={handleTabChange} className='login-tabs'>
<TabPane tab={intl.formatMessage({ id: 'login.tab.manager' })} key="supplier" /> <TabPane tab={intl.formatMessage({ id: 'login.tab.supplier' })} key="supplierLogin" />
<TabPane tab={intl.formatMessage({ id: 'login.tab.agent' })} key="agent" /> <TabPane tab={intl.formatMessage({ id: 'login.tab.expert' })} key="expertLogin" />
<TabPane tab={intl.formatMessage({ id: 'login.tab.agent' })} key="accountLogin" />
</Tabs> </Tabs>
</div> </div>
@ -85,7 +133,7 @@ const LoginPage: React.FC = () => {
onFinish={onFinish} onFinish={onFinish}
> >
<Form.Item <Form.Item
name="username" name="account"
rules={[{ required: true, message: intl.formatMessage({ id: 'login.username.placeholder' }) + '!' }]} rules={[{ required: true, message: intl.formatMessage({ id: 'login.username.placeholder' }) + '!' }]}
> >
<Input <Input
@ -106,6 +154,26 @@ const LoginPage: React.FC = () => {
size="large" size="large"
/> />
</Form.Item> </Form.Item>
<Form.Item
name="identifying"
rules={[{ required: true, message: intl.formatMessage({ id: 'login.captcha.placeholder' }) + '!' }]}
>
<Input
placeholder={intl.formatMessage({ id: 'login.captcha.placeholder' })}
size="large"
maxLength={6}
autoComplete="off"
prefix={null}
suffix={
<img
src={`data:image/png;base64,${captchaImg}`}
alt="验证码"
style={{ cursor: 'pointer', height: 32, verticalAlign: 'middle' }}
onClick={fetchCaptcha}
/>
}
/>
</Form.Item>
<Form.Item> <Form.Item>
<div className='login-options'> <div className='login-options'>
@ -122,6 +190,7 @@ const LoginPage: React.FC = () => {
<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' })} {intl.formatMessage({ id: 'login.button' })}
</Button> </Button>
{activeKey !== 'accountLogin' && renderRegisterLink()}
</Form.Item> </Form.Item>
</Form> </Form>
</div> </div>

41
src/servers/api/login.ts Normal file
View File

@ -0,0 +1,41 @@
import request from '@/utils/request';
/**
* 验证码
*/
export async function getCaptcha() {
return request('/v1/login/getCaptcha', {
method: 'GET'
});
}
/**
* 招标代理
*/
export async function accountLogin (data: API.LoginSupplier) {
return request('/v1/login/accountLogin', {
method: 'POST',
data
});
}
/**
* 专家
*/
export async function expertLogin (data: API.LoginSupplier) {
return request('/v1/login/expertLogin', {
method: 'POST',
data
});
}
/**
* 供应商
*/
export async function supplierLogin (data: API.LoginSupplier) {
return request('/v1/login/accountLogin/supplier', {
method: 'POST',
data
});
}

View File

@ -5,7 +5,13 @@ declare namespace API {
message: string; message: string;
data: T; data: T;
} }
//登录
interface LoginSupplier {
account: string;
password: string;
identifying: string;
encryptValue: string;
}
export type PolicyRequest = { export type PolicyRequest = {
/** /**
* 内容 * 内容

View File

@ -0,0 +1,21 @@
import JSEncrypt from 'jsencrypt';
import dayjs from 'dayjs';
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvLBkALIYR/x9Rv5TiXQGWAXTzraN/He80r9gQovSQ5oTP8qllL9+Oc1LdTijPFRsddHWg37umvFliwhmukU1NT+o2loGcKpyMHFkc/UPNjQLvd+YFR4nYhgP8l+dmRNOtQWawOt5dbksRKTghMjA+FKT2+itMsawSs1+Ic+zoIwIDAQAB
-----END PUBLIC KEY-----`;
export function encryptWithRsa(value: string, type: boolean = true, publicKey: string = PUBLIC_KEY): string {
const nowStr = dayjs().format('YYYY-MM-DD HH:mm:ss');
const content = type? `${value}_${nowStr}`: value;
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
const encrypted = encryptor.encrypt(content);
if (!encrypted) {
throw new Error('RSA 加密失败');
}
return encrypted;
}