接下来写登录页面
把登录页面和导航栏什么的同步写一写
路由跳转
//导入antd
import { Layout, theme, Button,Menu } from 'antd'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import React, { useState } from 'react'
import { changeConfirmLocale } from 'antd/es/modal/locale'
import { Dropdown, Space } from 'antd'
import { DownOutlined, SmileOutlined } from '@ant-design/icons'
import { Avatar } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'//从Layout组件中解构Header组件
const { Header } = Layout
function TopHeader() {//v6的写法
const navigate = useNavigate()const items = [{key: '1',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.antgroup.com">帮我上模电实验</a>),},{key: '2',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.aliyun.com">帮我上电磁场课</a>),},{key: '3',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.luohanacademy.com">帮我辅助面试</a>),},{key: '4',danger: true,label: '要退出吗',onClick: () => {localStorage.removeItem('token')//使用navigate实现重定向navigate('/login')},},
]const [collapsed, setCollapsed] = useState(false)//定义changeCollapsed函数,用于展开/收起侧边栏,通过取反实现const changeCollapsed = () => {setCollapsed(!collapsed)}const { token } = theme.useToken() // 获取主题 tokenconst { colorBgContainer, borderRadiusLG } = tokenreturn (<Headerstyle={{padding: '0 16px',background: colorBgContainer,}}><div style={{ float: 'right' }}>{/* 定义欢迎语 */}<span>欢迎主人回来</span>{/* 定义下拉菜单 */}<Dropdownmenu={{items,}}placement="bottomLeft"arrow><Space size={16} wrap><Avatar src={'/头像.jpg'} /></Space>{/* <Button>🥺</Button> */}</Dropdown><Dropdownmenu={{items,}}placement="bottom"arrow></Dropdown></div><Buttontype="text"//展开/收起侧边栏,绑定onClick事件icon={collapsed ? (<MenuUnfoldOutlined onClick={changeCollapsed} />) : (<MenuFoldOutlined onClick={changeCollapsed} />)}onClick={() => setCollapsed(!collapsed)}style={{fontSize: '16px',width: 64,height: 64,}}/></Header>)
}export default TopHeader
点击退出让其退出,接下来实现登录页面的编写
登录页
登录页还是直接用antd组件库
好了,做了个登录页简直丑爆了
想要实现粒子效果怎么做呢?
在github上面找项目
经过弹幕的提醒以及我的实际验证,这个库已经弃用了,所以用了其他的,代码如下:
import React, { useCallback } from 'react';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Checkbox, Form, Input, Flex } from 'antd';
import Particles from "react-tsparticles";
import { loadSlim } from "tsparticles-slim";
import './Login.css';
import axios from 'axios'const onFinish = (values) => {console.log('Received values of form: ', values);
};const ParticlesBackground = () => {const particlesInit = useCallback(async (engine) => {await loadSlim(engine);}, []);return (<Particlesid="tsparticles"init={particlesInit}options={{background: { color: "#ffffff" },particles: {number: { value: 100 },size: { value: 3 },move: { enable: true, speed: 2 },color: { value: "#1890ff" },shape: { type: "circle" },opacity: { value: 0.5 },},interactivity: {events: {onHover: {enable: true,mode: "repulse",},},},}}/>);
};function Login() {return (<div style={{position: "relative",height: "100vh",backgroundImage: `url('/微信图片_20250331223117.jpg')`,backgroundSize: "cover",backgroundPosition: "center"}}><ParticlesBackground /><div className='formContainer' style={{ position: "relative", zIndex: 1 }}><div className='logintitle'>全球新闻发布管理</div><Formname="login"initialValues={{ remember: true }}style={{ maxWidth: 360 }}onFinish={onFinish}><Form.Itemname="username"rules={[{ required: true, message: '请输入用户名!' }]}><Input prefix={<UserOutlined />} placeholder="用户名" /></Form.Item><Form.Itemname="password"rules={[{ required: true, message: '请输入密码!' }]}><Input prefix={<LockOutlined />} type="password" placeholder="密码" /></Form.Item><Form.Item><Flex justify="space-between" align="center"><Form.Item name="remember" valuePropName="checked" noStyle><Checkbox>记住我</Checkbox></Form.Item><a href="#">忘记密码?</a></Flex></Form.Item><Form.Item><Button block type="primary" htmlType="submit">登录</Button></Form.Item></Form></div></div>);
}export default Login;
登录校验
登录首先要判定用户是否存在(用户状态关闭也是检测不到的)
其次就是看用户名和密码是否正确
如果错误的话理应加上提示错误的弹出框
import React, { useCallback } from 'react';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Checkbox, Form, Input, Flex, message } from 'antd';
import Particles from "react-tsparticles";
import { loadSlim } from "tsparticles-slim";
import './Login.css';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';const ParticlesBackground = () => {const particlesInit = useCallback(async (engine) => {await loadSlim(engine);}, []);return (<Particlesid="tsparticles"init={particlesInit}options={{background: { color: "#ffffff" },particles: {number: { value: 100 },size: { value: 3 },move: { enable: true, speed: 2 },color: { value: "#1890ff" },shape: { type: "circle" },opacity: { value: 0.5 },},interactivity: {events: {onHover: {enable: true,mode: "repulse",},},},}}/>);
};function Login() {const navigate = useNavigate(); //获取 navigateconst onFinish = (values) => {console.log(values);axios.get(`http://localhost:3000/users?username=${values.username}&password=${values.password}&roleState=true&_expand`).then(res => {console.log(res.data);if (res.data.length === 0) {message.error("用户名或密码错误!");} else {localStorage.setItem("token", JSON.stringify(res.data[0]));navigate("/"); //直接调用 navigate,无需传参}}).catch(error => {console.error("请求错误:", error);message.error("登录失败,请稍后再试!");});};return (<div style={{ position: "relative", height: "100vh" }}><ParticlesBackground /><div className='formContainer' style={{ position: "relative", zIndex: 1 }}><div className='logintitle'>全球新闻发布管理</div><Formname="login"initialValues={{ remember: true }}style={{ maxWidth: 360 }}onFinish={onFinish} //直接使用 onFinish><Form.Itemname="username"rules={[{ required: true, message: '请输入用户名!' }]}><Input prefix={<UserOutlined />} placeholder="用户名" /></Form.Item><Form.Itemname="password"rules={[{ required: true, message: '请输入密码!' }]}><Input prefix={<LockOutlined />} type="password" placeholder="密码" /></Form.Item><Form.Item><Flex justify="space-between" align="center"><Form.Item name="remember" valuePropName="checked" noStyle><Checkbox>记住我</Checkbox></Form.Item><a href="#">忘记密码?</a></Flex></Form.Item><Form.Item><Button block type="primary" htmlType="submit">登录</Button></Form.Item></Form></div></div>);
}export default Login;
这是加上错误的弹出框并且正确登录成功导航
侧边栏显示
不同身份做出来的侧边栏显示不一样
值得注意的是
axios.get(`http://localhost:3000/users?username=${values.username}&password=${values.password}&roleState=true&_expand=role`)
最好不要分行写(可能识别不出来)
末尾不要忘记_expand=role(否则过滤不出数据)
import React, { useState, useEffect } from 'react'
import { Layout, Menu } from 'antd'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import {UserOutlined,SettingOutlined,UploadOutlined,VideoCameraOutlined,AuditOutlined,FormOutlined,HomeOutlined,
} from '@ant-design/icons'
import './index.css'
import { useLocation } from 'react-router-dom'const { SubMenu } = Menu
const { Sider } = Layout// **手动映射菜单项对应的图标**
const iconMap = {首页: <HomeOutlined />,用户管理: <UserOutlined />,用户列表: <UserOutlined />,权限管理: <SettingOutlined />,新闻管理: <FormOutlined />,审核管理: <AuditOutlined />,发布管理: <UploadOutlined />,
}function SideMenu() {const [menu, setMenu] = useState([])const location = useLocation() // 获取当前的路径useEffect(() => {axios.get('http://localhost:3000/rights?_embed=children').then((res) => {setMenu(res.data)})}, [])const navigate = useNavigate()const {role:{rights}} = JSON.parse(localStorage.getItem('token')) || {}; // 确保 tokenData 是一个对象const checkPermission = (item) => {// 检查用户是否具有访问权限(还要当前登录的用户的权限列表)return item.pagepermisson && rights.includes(item.key)}const renderMenu = (menuList) => {return menuList.map((item) => {const icon = iconMap[item.title] || <VideoCameraOutlined /> // 默认图标if (item.children?.length > 0 && checkPermission(item)) {return (<SubMenu key={item.key} icon={icon} title={item.title}>{renderMenu(item.children)}</SubMenu>)}return (checkPermission(item) && (<Menu.Itemkey={item.key}icon={icon}onClick={() => navigate(item.key)}>{item.title}</Menu.Item>))})}//找到路径const selectKeys = [location.pathname]//分割字符串const openKeys = ['/' + location.pathname.split('/')[1]]return (<Sider trigger={null} collapsible><div style={{ display: 'flex', height: '100%', flexDirection: 'column' }}><div className="logo">新闻发布系统</div><div style={{ flex: 1, overflow: 'auto' }}><Menutheme="dark"mode="inline"selectedKeys={selectKeys}defaultOpenKeys={openKeys}>{renderMenu(menu)}</Menu></div></div></Sider>)
}export default SideMenu
这样以后可以成功显示
用户权限过滤
如果只完成了上面的部分的话,角色列表中任何人都可以给自己提权,但是这样如果遇到贱人就不好办了,所以需要再加一层权限的过滤
import React, { useState, useEffect, useRef } from 'react'
import { Button, Table, Modal, Switch, Form, Menu } from 'antd'
import {EditOutlined,DeleteOutlined,ExclamationCircleOutlined,
} from '@ant-design/icons'
import axios from 'axios'
import UserForm from '../../../components/user-manage/UserForm'const { confirm } = Modal
function UserList() {const [dataSource, setdataSource] = useState([])const [isAddVisible, setisAddVisible] = useState(false)const [roleList, setroleList] = useState([])const [regionList, setregionList] = useState([])const [isUpdateVisible, setisUpdateVisible] = useState(false)const [isUpdateDisabled, setisUpdateDisabled] = useState(false)const [form] = Form.useForm() //创建antd的form实例const [current,setCurrent] = useState(null)const updateForm = useRef(null)const addForm = useRef(null)//判断用户权限const {roleId,region,username} = JSON.parse(localStorage.getItem('token'))useEffect(() => {//提前设计映射const roleObj = {"1":"superadmin","2":"admin","3":"editor"}axios.get('http://localhost:3000/users?_expand=role').then(res => {// console.log(res.data);const list = res.data//如果是超级管理员,显示所有数据setdataSource(roleObj[roleId]==="superadmin"?list:[...list.filter(item=>item.username===username),...list.filter(item=>item.region===region && roleObj[item.roleId]==='editor')])})}, [roleId,region,username])useEffect(() => {axios.get('http://localhost:3000/regions').then((res) => {// console.log(res.data);const list = res.datasetregionList(list)})}, [])useEffect(() => {axios.get('http://localhost:3000/roles').then((res) => {// console.log(res.data);const list = res.datasetroleList(list)})}, [])const deleteMethod = (record) => {// console.log(record);//页面状态+后端setdataSource(dataSource.filter((item) => item.id !== record.id))axios.delete(`http://localhost:3000/users/${record.id}`)}const confirmMethod = (record) => {confirm({title: '确定要删除这个角色吗?',icon: <ExclamationCircleOutlined />,onOk() {deleteMethod(record)},onCancel() {console.log('取消删除')},})}const handleChange = (item) => {// console.log(item)item.roleState = !item.roleStatesetdataSource([...dataSource])axios.patch(`http://localhost:3000/users/${item.id}`, {roleState: item.roleState,})}const columns = [{title: '区域',dataIndex: 'region',filters:[...regionList.map((item) => ({text: item.value,value: item.value,})),{text: '全球',value: '全球',}],onFilter: (value, item) =>{if(value === '全球') {return item.region === ""}return item.region===value}, render: (region) => {return <b>{region === '' ? '全球' : region}</b>},},{title: '角色名称',dataIndex: 'role',render: (role) => {return role?.roleName},},{title: '用户名',dataIndex: 'username',},{title: '用户状态',dataIndex: 'roleState',render: (roleState, item) => {return (<Switchchecked={roleState}disabled={item.default}onChange={() => handleChange(item)}></Switch>)},},{title: '操作',render: (record) => {return (<div><Buttontype="primary"shape="circle"icon={<EditOutlined />}disabled={record.default}onClick={() => handleUpdate(record)}/><Buttondangertype="primary"shape="circle"icon={<DeleteOutlined />}disabled={record.default}onClick={() => confirmMethod(record)}/></div>)},},]const addFormOk = () => {addForm.current.validateFields().then((value) => {// console.log(value)setOpen(false)addForm.current.resetFields()//post到后端生成id,再设置dataSource,方便删除和更新// 修正: 确保 `region` 存储的是字符串,而不是 id// const selectedRegion = regionList.find(item => item.id === value.region)?.value || "";// 添加用户的代码逻辑const selectedRegion =regionList.find((item) => item.id === value.region)?.value || ''axios.post(`http://localhost:3000/users`, {...value,region: selectedRegion, // 存储的是区域的名称(如"华东")})// 获取所选角色的名称const selectedRole =roleList.find((item) => item.id === value.roleId)?.roleName || ''axios.post(`http://localhost:3000/users`, {...value,roleState: true,default: false,region: selectedRegion, // 修正 `region` 的值roleName: selectedRole, // 修正 `roleName` 的值}).then((res) => {console.log(res.data)setdataSource([...dataSource,{...res.data,role: roleList.filter((item) => item.id === value.roleId)[0],},])})}).catch((errInfo) => {console.log(errInfo)})}const [isUpdate, setisUpdate] = useState(false)const [updateform] = Form.useForm() //创建antd的updateform实例const handleUpdate = (item) => {setisUpdateVisible(true);setisUpdateDisabled(item.default);setTimeout(() => {//保证超级管理员的区域禁用if(item.roleId === 1) {setisUpdateDisabled(true)} else {setisUpdateDisabled(false)}updateform.setFieldsValue({username: item.username,password: item.password, // 确保密码字段被正确设置region: item.region,roleId: item.role?.id,});}, 100); // 增加一定的延迟确保 Modal 已渲染setCurrent(item)}const [open, setOpen] = useState(false)const updateFormOk = () => {updateform.validateFields().then((value) => {setisUpdateVisible(false);const selectedRegion = regionList.find(region => region.id === value.region)?.value || '';setdataSource(dataSource.map((item) => {if (item.id === current.id) {return {...item,...value,region: selectedRegion, // 将区域 ID 转换为区域名称role: roleList.filter((data) => data.id === value.roleId)[0],}}return item;}));// 同步后端数据axios.patch(`http://localhost:3000/users/${current.id}`, {...value,region: selectedRegion, // 确保后端也存储区域名称});});}return (<div><Button type="primary" onClick={() => setOpen(true)}>添加用户</Button><TabledataSource={dataSource}columns={columns}pagination={{pageSize: 5,}}>rowKey = {(item) => item.id}</Table><Modalopen={open}title="添加用户"okText="确定"cancelText="取消"okButtonProps={{ autoFocus: true, htmlType: 'submit' }}onCancel={() => setOpen(false)}onOk={() => addFormOk()}destroyOnClosemodalRender={(dom) => <Form layout="vertical">{dom}</Form>}><UserFormregionList={regionList}roleList={roleList}ref={addForm}></UserForm></Modal>{/* 更新 */}<Modalopen={isUpdateVisible}title="更新用户"okText="更新"cancelText="取消"okButtonProps={{ autoFocus: true, htmlType: 'submit' }}onCancel={() => {setisUpdateVisible(false)setisUpdateDisabled(!isUpdateDisabled)}}onOk={() => updateFormOk()}destroyOnClosemodalRender={(dom) => (<Form layout="vertical" form={updateform}>{/* //将form实例传递给Form组件 */}{dom}</Form>)}><UserFormregionList={regionList}roleList={roleList}form={updateform}isUpdateDisabled={isUpdateDisabled}isUpdate={true}></UserForm></Modal></div>)
}export default UserList
接下来要更细致的做权限的过滤,就是在编辑的时候不可以做提权
对区域做禁用:
角色禁用和区域禁用的逻辑差不多:
import React, {useState,forwardRef,useImperativeHandle,useEffect,
} from 'react'
import { Form, Input, Select } from 'antd'const UserForm = forwardRef((props, ref) => {const [form] = Form.useForm()const internalForm = props.form || form // 使用外部传递的 form 实例const [isDisabled, setIsDisabled] = useState(false)useEffect(() => {if (props.form && props.initialValues) {props.form.setFieldsValue(props.initialValues);}setIsDisabled(props.isUpdateDisabled);}, [props.isUpdateDisabled, props.form, props.initialValues]); // 确保依赖项正确useImperativeHandle(ref, () => ({validateFields: () => internalForm.validateFields(),resetFields: () => internalForm.resetFields(),setFieldsValue: (values) => internalForm.setFieldsValue(values),}))const handleRoleChange = (value) => {const isSuperAdmin = value === 1 // 假设角色ID=1是超级管理员setIsDisabled(isSuperAdmin)if (isSuperAdmin) {internalForm.setFieldsValue({ region: '' }) // 清空区域选择}}const handleRegionChange = (value) => {console.log('区域选择:', value)}const {roleId,region} = JSON.parse(localStorage.getItem('token')) || {};const roleObj = {"1":"superadmin","2":"admin","3":"editor"}const checkRegionDisabled = (item) => {//如果是更新状态if(props.isUpdate){if(roleObj[roleId] === "superadmin"){//禁用为假//超级管理员可以看到所有区域return false}else{return true}}else{//如果是添加状态if(roleObj[roleId] === "superadmin"){//禁用为假//超级管理员可以看到所有区域return false}else{return item.value !== region}}}const checkRoleDisabled = (item) => {//如果是更新状态if(props.isUpdate){if(roleObj[roleId] === "superadmin"){//禁用为假//超级管理员可以看到所有区域return false}else{return true}}else{//如果是添加状态if(roleObj[roleId] === "superadmin"){//禁用为假//超级管理员可以看到所有身份return false}else{return roleObj[item.id] !== "editor"}}}return (<Form form={internalForm} layout="vertical"><Form.Itemname="username"label="用户名"rules={[{ required: true, message: '请输入用户名' }]}><Input placeholder="请输入用户名" /></Form.Item><Form.Itemname="password"label="密码"rules={[{ required: true, message: '请输入密码' }]}><Input.Password placeholder="请输入密码" /></Form.Item><Form.Itemname="region"label="区域"rules={isDisabled ? [] : [{ required: true, message: '请选择区域' }]}><Selectdisabled={isDisabled}showSearchplaceholder="请选择区域"optionFilterProp="label"onChange={handleRegionChange}options={props.regionList.map((item) => ({value: item.id,label: item.value,disabled:checkRegionDisabled(item)}))}/></Form.Item><Form.Itemname="roleId"label="角色"rules={[{ required: true, message: '请选择角色' }]}><SelectshowSearchplaceholder="请选择角色"optionFilterProp="label"onChange={handleRoleChange}options={props.roleList.map((item) => ({value: item.id,label: item.roleName,disabled:checkRoleDisabled(item)}))}/></Form.Item></Form>)
})export default UserForm
超级管理员侧边栏显示不正常,改了一下:
import React, { useState, useEffect } from 'react'
import { Layout, Menu } from 'antd'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import {UserOutlined,SettingOutlined,UploadOutlined,VideoCameraOutlined,AuditOutlined,FormOutlined,HomeOutlined,
} from '@ant-design/icons'
import './index.css'
import { useLocation } from 'react-router-dom'const { SubMenu } = Menu
const { Sider } = Layout// **手动映射菜单项对应的图标**
const iconMap = {首页: <HomeOutlined />,用户管理: <UserOutlined />,用户列表: <UserOutlined />,权限管理: <SettingOutlined />,新闻管理: <FormOutlined />,审核管理: <AuditOutlined />,发布管理: <UploadOutlined />,
}function SideMenu() {const [menu, setMenu] = useState([])const location = useLocation() // 获取当前的路径useEffect(() => {axios.get('http://localhost:3000/rights?_embed=children').then((res) => {setMenu(res.data)})}, [])const navigate = useNavigate()// const {role:{rights}} = JSON.parse(localStorage.getItem('token')) || {}; // 确保 tokenData 是一个对象const tokenData = JSON.parse(localStorage.getItem('token')) || {}; // 确保 tokenData 是一个对象const { role = {} } = tokenData; // 确保 role 是一个对象const { rights = [] } = role; // 确保 rights 是一个数组const checkPermission = (item) => {// 检查用户是否具有访问权限(还要当前登录的用户的权限列表)if (role.roleName === '超级管理员') {return true}return item.pagepermisson && Array.isArray(rights) && rights.includes(item.key)}const renderMenu = (menuList) => {return menuList.map((item) => {const icon = iconMap[item.title] || <VideoCameraOutlined /> // 默认图标if (item.children?.length > 0 && checkPermission(item)) {return (<SubMenu key={item.key} icon={icon} title={item.title}>{renderMenu(item.children)}</SubMenu>)}return (checkPermission(item) && (<Menu.Itemkey={item.key}icon={icon}onClick={() => navigate(item.key)}>{item.title}</Menu.Item>))})}//找到路径const selectKeys = [location.pathname]//分割字符串const openKeys = ['/' + location.pathname.split('/')[1]]return (<Sider trigger={null} collapsible><div style={{ display: 'flex', height: '100%', flexDirection: 'column' }}><div className="logo">新闻发布系统</div><div style={{ flex: 1, overflow: 'auto' }}><Menutheme="dark"mode="inline"selectedKeys={selectKeys}defaultOpenKeys={openKeys}>{renderMenu(menu)}</Menu></div></div></Sider>)
}export default SideMenu