忘记密码与部门改为懒加载

This commit is contained in:
孙景学
2025-07-18 13:39:45 +08:00
parent e637f62d86
commit 505562892f
8 changed files with 480 additions and 61 deletions

View File

@ -2,7 +2,7 @@ import { history } from 'umi';
export function onRouteChange({ location }: any) { export function onRouteChange({ location }: any) {
const token = sessionStorage.getItem('token'); const token = sessionStorage.getItem('token');
const whiteList = ['/login', '/register/supplier', '/register/expert', '/403', '/404']; const whiteList = ['/login', '/forgot', '/403', '/404'];
if (!token && !whiteList.includes(location.pathname)) { if (!token && !whiteList.includes(location.pathname)) {
history.replace('/login'); history.replace('/login');
} }

View File

@ -1,17 +1,23 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { TreeSelect } from 'antd'; import { TreeSelect, Spin } from 'antd';
import { treeData } from './services'; // 你的接口
export interface AccessDepartmentSelectProps { export interface AccessDepartmentSelectProps {
value?: string | number; value?: string | number;
onChange?: (value: string | number) => void; onChange?: (value: string | number) => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
style?: React.CSSProperties;
} }
interface orgData { // 树节点类型
orgName: string; interface OrgNode {
orgId: string; orgId: string;
children?: orgData[]; orgName: string;
children?: OrgNode[];
isLeaf?: boolean;
key?: string;
title?: string;
} }
const AccessDepartmentSelect: React.FC<AccessDepartmentSelectProps> = ({ const AccessDepartmentSelect: React.FC<AccessDepartmentSelectProps> = ({
@ -19,36 +25,70 @@ const AccessDepartmentSelect: React.FC<AccessDepartmentSelectProps> = ({
onChange, onChange,
placeholder = '请选择准入部门', placeholder = '请选择准入部门',
disabled = false, disabled = false,
style = { width: 200 },
}) => { }) => {
const [userOrgAll, setUserOrgAll] = useState<orgData[]>([]); const [tree, setTree] = useState<OrgNode[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => { // 转换接口为 TreeSelect 结构
const userOrgAllStr = sessionStorage.getItem('userOrgAll'); function formatTree(data: OrgNode[]): any[] {
const userOrgAll = userOrgAllStr ? JSON.parse(userOrgAllStr) : [];
setUserOrgAll(userOrgAll);
}, []);
// TreeSelect 要的数据结构
function buildTree(data: orgData[]): any[] {
return data.map(item => ({ return data.map(item => ({
...item,
title: item.orgName, title: item.orgName,
value: item.orgId, value: item.orgId,
key: item.orgId, key: item.orgId,
children: item.children && item.children.length > 0 ? buildTree(item.children) : undefined, // isLeaf: item.isLeaf || false,
// children: item.children && item.children.length > 0 ? formatTree(item.children) : undefined,
})); }));
} }
// 懒加载节点
const onLoadData = async (treeNode: any) => {
if (treeNode.children && treeNode.children.length > 0) return;
setLoading(true);
const { code, data } = await treeData({ upOrgId: treeNode.value });
if (code === 200 && Array.isArray(data)) {
setTree(origin => updateNode(origin, treeNode.value, formatTree(data)));
}
setLoading(false);
};
// 递归插入children
function updateNode(list: OrgNode[], key: string, children: OrgNode[]): OrgNode[] {
return list.map(node => {
if (node.orgId === key) return { ...node, children };
if (node.children) return { ...node, children: updateNode(node.children, key, children) };
return node;
});
}
// 初始化
useEffect(() => {
setLoading(true);
treeData({}).then(res => {
if (res.code === 200 && Array.isArray(res.data)) {
setTree(formatTree(res.data));
}
setLoading(false);
});
}, []);
return ( return (
<TreeSelect <Spin spinning={loading}>
value={value} <TreeSelect
onChange={onChange} value={value}
placeholder={placeholder} treeData={tree}
disabled={disabled} onChange={onChange}
treeData={buildTree(userOrgAll)} placeholder={placeholder}
allowClear disabled={disabled}
style={{ width: '160px' }} allowClear
showSearch style={style}
/> showSearch
loadData={onLoadData}
treeDefaultExpandAll={false}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
/>
</Spin>
); );
}; };

View File

@ -4,8 +4,13 @@ import request from '@/utils/request';
* 部门 * 部门
*/ */
export const queryUserOrgAll = () => request.get(`/org/queryUserOrgAll`); export const queryUserOrgAll = () => request.get(`/org/queryUserOrgAll`);
/**
* 部门列表
*/
interface treeInterface {
upOrgId?: string
}
export const treeData = (params: treeInterface) => request.get('/org/list', { params });

View File

@ -1,3 +1,194 @@
.forgotContainer{ @import '~@/baseStyle.less';
background: red;
// 登录页面整体布局
.login-page {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 100vh;
padding-right: 10%;
background: #f0f2f5;
background-image: url('~@/assets/img/loginBg.jpg');
background-position: center;
background-size: cover;
}
// 登录容器
.login-container {
position: relative;
width: 400px;
padding: 30px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
// 标题样式
.login-title {
margin-bottom: 20px;
color: @main-color;
font-weight: bold;
font-size: 24px;
text-align: center;
}
// 返回首页链接
.back-home {
position: absolute;
top: 10px;
left: 10px;
a {
display: flex;
align-items: center;
color: @main-color;
font-size: 14px;
&:hover {
color: lighten(@main-color, 10%);
}
.anticon {
margin-right: 4px;
}
}
}
// Tab 容器
.login-tab-container {
margin-bottom: 30px;
}
// Tab 样式
.login-tabs {
border-radius: 15px;
.ant-tabs-nav {
margin: 0;
}
.ant-tabs-nav::before {
display: none;
}
.ant-tabs-tab {
flex: 1;
height: 44px;
margin: 0;
padding: 0;
font-weight: bold;
font-size: 16px;
line-height: 44px;
text-align: center;
background-color: #f5f5f5;
border: none;
transition: none;
.ant-tabs-tab-btn {
width: 100%;
height: 100%;
color: #333;
}
}
.ant-tabs-nav-list {
display: flex;
width: 100%;
overflow: hidden;
}
.ant-tabs-tab-active {
background-color: @main-color;
border-radius: 15px;
.ant-tabs-tab-btn {
color: #fff !important;
font-weight: bold;
}
}
.ant-tabs-ink-bar {
display: none;
}
}
// 登录表单
.login-form {
.ant-form-item {
margin-bottom: 20px;
}
.ant-input-affix-wrapper {
border-radius: 10px;
&:focus,
&-focused,
&:hover {
border-color: @main-color;
}
}
}
// 登录选项区域(记住密码和忘记密码)
.login-options {
display: flex;
align-items: center;
justify-content: space-between;
.ant-checkbox-checked .ant-checkbox-inner {
background-color: @main-color;
border-color: @main-color;
}
.login-form-forgot {
color: @main-color;
&:hover {
color: lighten(@main-color, 10%);
}
}
}
// 登录按钮
.login-form-button {
width: 100%;
height: 40px;
margin-top: 10px;
background-color: @main-color;
border-color: @main-color;
border-radius: 4px;
&:hover,
&:focus {
background-color: lighten(@main-color, 10%);
border-color: lighten(@main-color, 10%);
}
}
// 注册链接
.register-link {
margin-top: 15px;
text-align: center;
a {
margin-left: 5px;
color: @main-color;
&:hover {
color: lighten(@main-color, 10%);
}
}
}
// 响应式布局
@media (max-width: 768px) {
.login-page {
justify-content: center;
padding-right: 0;
}
.login-container {
width: 90%;
max-width: 400px;
padding: 20px;
}
} }

View File

@ -1,12 +1,177 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import styles from './forget.less'; import { Form, Input, Button, message } from 'antd';
import { useLocation } from 'umi'; import { UserOutlined, LockOutlined, EyeInvisibleOutlined, EyeTwoTone, HomeOutlined } from '@ant-design/icons';
const Forgot: React.FC = () => { import { history, useIntl } from 'umi';
const location = useLocation(); import './forget.less';
const search = location.search; import { sendCode, reset } from '@/servers/api/login';
const params = new URLSearchParams(search); import { encryptWithRsa } from '@/utils/encryptWithRsa';
console.log(params);
const type = params.get('type'); const LoginPage: React.FC = () => {
return <div className={styles.forgotContainer}></div>; const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [sending, setSending] = useState(false); // 是否在发送验证码
const [countdown, setCountdown] = useState<number>(0); // 倒计时
const intl = useIntl();
// 倒计时效果
useEffect(() => {
let timer: any;
if (countdown > 0) {
timer = setTimeout(() => setCountdown(countdown - 1), 1000);
}
return () => clearTimeout(timer);
}, [countdown]);
// 获取验证码
const handleSendCode = async () => {
const account = form.getFieldValue('account');
if (!account) {
message.warning('请先输入账号/手机号');
return;
}
setSending(true);
try {
const res = await sendCode({ account });
if (res.code === 200) {
message.success('验证码已发送');
setCountdown(60); // 60秒倒计时
} else {
message.error(res.message || '验证码发送失败');
}
} finally {
setSending(false);
}
};
// 表单提交
const onFinish = async (values: any) => {
setLoading(true);
try {
const params = {
...values,
password: encryptWithRsa(values.password, false),
encryptValue: encryptWithRsa(values.code) // 验证码
};
const loginRes = await reset(params);
if (loginRes.code === 200) {
message.success('修改成功');
history.push('/login');
} else {
message.error(loginRes.message || '修改失败');
}
} finally {
setLoading(false);
}
};
return (
<div className='login-page'>
<div className='login-container'>
<div className="back-home">
<a onClick={() => history.push('/login')}>
<HomeOutlined />
</a>
</div>
<div className='login-title'>{intl.formatMessage({ id: 'login.title', defaultMessage: '忘记密码' })}</div>
<Form
form={form}
name="forget"
className='login-form'
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
name="account"
rules={[{ required: true, message: '请输入账号/手机号!' }]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder="请输入账号/手机号"
size="large"
allowClear
/>
</Form.Item>
<Form.Item
name="password"
rules={[
{ required: true, message: '请输入新密码!' },
{ min: 6, message: '密码至少6位' }
]}
hasFeedback
>
<Input.Password
prefix={<LockOutlined className="site-form-item-icon" />}
placeholder="请输入新密码"
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
size="large"
allowClear
/>
</Form.Item>
<Form.Item
name="confirm"
dependencies={['password']}
hasFeedback
rules={[
{ required: true, message: '请再次输入新密码!' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致!'));
},
}),
]}
>
<Input.Password
prefix={<LockOutlined className="site-form-item-icon" />}
placeholder="请确认新密码"
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
size="large"
allowClear
/>
</Form.Item>
<Form.Item
>
<Input.Group compact>
<Form.Item
name="code"
noStyle
rules={[{ required: true, message: '请输入验证码!' }]}
>
<Input
placeholder="请输入验证码"
size="large"
maxLength={6}
autoComplete="off"
style={{ width: '65%' }}
/>
</Form.Item>
<Button
type="primary"
style={{ width: '33%', height: '40px' }}
disabled={countdown > 0 || sending}
onClick={handleSendCode}
>
{countdown > 0 ? `重新发送(${countdown}s)` : '发送验证码'}
</Button>
</Input.Group>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button" loading={loading} size="large" block>
</Button>
</Form.Item>
</Form>
</div>
</div>
);
}; };
export default Forgot;
export default LoginPage;

View File

@ -72,12 +72,12 @@ const LoginPage: React.FC = () => {
//存入招标代理用户id //存入招标代理用户id
sessionStorage.setItem('userId', loginRes.data.user.userId); sessionStorage.setItem('userId', loginRes.data.user.userId);
//部门 //部门
queryUserOrgAll().then((res) => { // queryUserOrgAll().then((res) => {
const { code, data } = res; // const { code, data } = res;
if (code == 200) { // if (code == 200) {
sessionStorage.setItem('userOrgAll', JSON.stringify(data) ); // sessionStorage.setItem('userOrgAll', JSON.stringify(data) );
} // }
}) // })
} }
sessionStorage.setItem('currentUser', JSON.stringify(loginRes.data)); sessionStorage.setItem('currentUser', JSON.stringify(loginRes.data));
@ -88,8 +88,8 @@ const LoginPage: React.FC = () => {
}) })
console.log(roleIdList, 'roleIdList'); console.log(roleIdList, 'roleIdList');
const menuList = await findMenuList({ roleIdList }); // const menuList = await findMenuList({ roleIdList });
sessionStorage.setItem('menuList', JSON.stringify(menuList.data)); // sessionStorage.setItem('menuList', JSON.stringify(menuList.data));
// } // }
}) })

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Button, Descriptions, Tooltip, Table } from 'antd'; import { Modal, Button, Descriptions, Tooltip, Table } from 'antd';
import { detail, treeList } from "../services"; import { detail } from "../services";
import type { DataNode } from 'antd/es/tree'; import type { DataNode } from 'antd/es/tree';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { useSupplierDetailModal } from '@/components/SupplierDetailModalContext/SupplierDetailModalContext'; import { useSupplierDetailModal } from '@/components/SupplierDetailModalContext/SupplierDetailModalContext';
@ -50,9 +50,9 @@ const ViewModal: React.FC<{
// 已勾选的供应商 id // 已勾选的供应商 id
// 品类库基本信息 // 品类库基本信息
const [categoryInfo, setCategoryInfo] = useState<CategoryInfo>({}); const [categoryInfo, setCategoryInfo] = useState<CategoryInfo>({});
const [treeData, setTreeData] = useState<LockNode[]>([]); // const [treeData, setTreeData] = useState<LockNode[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); // const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [autoExpandParent, setAutoExpandParent] = useState(true); // const [autoExpandParent, setAutoExpandParent] = useState(true);
const supplierDetailModal = useSupplierDetailModal(); const supplierDetailModal = useSupplierDetailModal();
// 拉取基本信息 // 拉取基本信息
@ -61,14 +61,6 @@ const ViewModal: React.FC<{
const res = await detail({ id: storeId }); const res = await detail({ id: storeId });
if (res.code === 200 && res.data) setCategoryInfo(res.data); if (res.code === 200 && res.data) setCategoryInfo(res.data);
// const treeRes = await treeList({ categoryLibraryId: storeId });
// if (treeRes.code === 200 && treeRes.data) {
// // categoryIds
// const formatted = formatData(res.data)
// // setTreeData();
// }
}; };
const formatData = (list: any[]): LockNode[] => { const formatData = (list: any[]): LockNode[] => {
return list.map(item => ({ return list.map(item => ({

View File

@ -70,4 +70,30 @@ export async function queryUserOrgAll() {
return request('/org/queryUserOrgAll', { return request('/org/queryUserOrgAll', {
method: 'GET' method: 'GET'
}); });
}
/**
* 发送验证码
*/
interface sendCodeData {
account: string;
}
export async function sendCode(data: sendCodeData) {
return request('/v1/login/forgotPassword/sendCode', {
method: 'POST',
data
});
}
/**
* 校验验证码并设置新密码
*/
interface resetData {
account: string;
identifying: string;
password: string;
}
export async function reset(data: resetData) {
return request('/v1/login/forgotPassword/reset', {
method: 'POST',
data
});
} }