1. 前端页面代码
1. 前端修改密码业务逻辑
- 用户修改自己的密码:
- 在顶部导航栏添加了用户头像按钮
- 点击头像显示下拉菜单,包含"Change Password"和"Logout"选项
- 点击"Change Password"打开修改密码对话框
- 需要输入当前密码和新密码(带确认)
- 发送请求到 /api/users/change-password
- 管理员重置用户密码:
- 在用户管理页面的操作列添加了重置密码按钮(锁图标)
- 点击按钮打开重置密码对话框
- 只需要输入新密码
- 发送请求到 /api/users/{用户ID}/reset-password
2. 修改MainLayout.tsx添加用户菜单
import React, { useState } from 'react';
import {
Box,
Drawer,
AppBar,
Toolbar,
List,
Typography,
Divider,
IconButton,
ListItem,
ListItemIcon,
ListItemText,
ListItemButton,
useTheme,
useMediaQuery,
Button,
Menu,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField
} from '@mui/material';
import {
Menu as MenuIcon,
Dashboard as DashboardIcon,
Inventory as InventoryIcon,
ShoppingCart as ShoppingCartIcon,
LocalShipping as LocalShippingIcon,
People as PeopleIcon,
Person as PersonIcon,
Settings as SettingsIcon,
Assessment as AssessmentIcon,
Logout as LogoutIcon,
AccountCircle as AccountCircleIcon,
Lock as LockIcon
} from '@mui/icons-material';
import { useNavigate, useLocation } from 'react-router-dom';
const drawerWidth = 240;
const menuItems = [
{ text: 'Dashboard', icon: <DashboardIcon />, path: '/' },
{ text: 'Products', icon: <InventoryIcon />, path: '/products' },
{ text: 'Orders', icon: <ShoppingCartIcon />, path: '/orders' },
{ text: 'Inbound', icon: <LocalShippingIcon />, path: '/inbound' },
{ text: 'Customers', icon: <PeopleIcon />, path: '/customers' },
{ text: 'Delivery', icon: <LocalShippingIcon />, path: '/delivery' },
{ text: 'Users', icon: <PersonIcon />, path: '/users' },
{ text: 'Reports', icon: <AssessmentIcon />, path: '/reports' },
{ text: 'Settings', icon: <SettingsIcon />, path: '/settings' }
];
const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [mobileOpen, setMobileOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [openPasswordDialog, setOpenPasswordDialog] = useState(false);
const [passwordForm, setPasswordForm] = useState({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const navigate = useNavigate();
const location = useLocation();
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handlePasswordDialogOpen = () => {
setOpenPasswordDialog(true);
handleMenuClose();
};
const handlePasswordDialogClose = () => {
setOpenPasswordDialog(false);
setPasswordForm({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
setError('');
};
const handlePasswordChange = async () => {
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setError('New passwords do not match');
return;
}
try {
const response = await fetch('http://localhost:8080/api/users/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
oldPassword: passwordForm.oldPassword,
newPassword: passwordForm.newPassword
})
});
if (response.ok) {
setSuccessMessage('Password changed successfully');
handlePasswordDialogClose();
} else {
const data = await response.json();
setError(data.message || 'Failed to change password');
}
} catch (err) {
setError('Network error, please try again');
}
};
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
navigate('/login');
};
const drawer = (
<div>
<Toolbar>
<Typography variant="h6" noWrap component="div">
QuickStore
</Typography>
</Toolbar>
<Divider />
<List>
{menuItems.map((item) => (
<ListItem key={item.text} disablePadding>
<ListItemButton
selected={location.pathname === item.path}
onClick={() => {
navigate(item.path);
if (isMobile) {
setMobileOpen(false);
}
}}
sx={{
'&.Mui-selected': {
backgroundColor: 'rgba(25, 118, 210, 0.08)',
'&:hover': {
backgroundColor: 'rgba(25, 118, 210, 0.12)',
},
'& .MuiListItemIcon-root': {
color: 'primary.main',
},
'& .MuiListItemText-primary': {
color: 'primary.main',
fontWeight: 'bold',
},
},
}}
>
<ListItemIcon sx={{ minWidth: 40 }}>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.text} />
</ListItemButton>
</ListItem>
))}
</List>
</div>
);
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Box sx={{ flexGrow: 1 }} />
<IconButton
color="inherit"
onClick={handleMenuOpen}
sx={{ mr: 2 }}
>
<AccountCircleIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handlePasswordDialogOpen}>
<ListItemIcon>
<LockIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Change Password</ListItemText>
</MenuItem>
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<LogoutIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Logout</ListItemText>
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
>
<Drawer
variant={isMobile ? 'temporary' : 'permanent'}
open={isMobile ? mobileOpen : true}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
},
}}
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` },
mt: '64px',
}}
>
{children}
</Box>
<Dialog open={openPasswordDialog} onClose={handlePasswordDialogClose}>
<DialogTitle>Change Password</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="Current Password"
type="password"
fullWidth
value={passwordForm.oldPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, oldPassword: e.target.value })}
/>
<TextField
margin="dense"
label="New Password"
type="password"
fullWidth
value={passwordForm.newPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
/>
<TextField
margin="dense"
label="Confirm New Password"
type="password"
fullWidth
value={passwordForm.confirmPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
/>
{error && (
<Typography color="error" sx={{ mt: 1 }}>
{error}
</Typography>
)}
</DialogContent>
<DialogActions>
<Button onClick={handlePasswordDialogClose}>Cancel</Button>
<Button onClick={handlePasswordChange} variant="contained" color="primary">
Change Password
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default MainLayout;
3. 修改“用户管理”页面,添加“重置密码”功能
import React, { useState, useEffect } from 'react';
import {
Box,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Button,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
MenuItem,
Typography,
Alert,
Snackbar
} from '@mui/material';
import { Edit as EditIcon, Delete as DeleteIcon, Lock as LockIcon } from '@mui/icons-material';
interface User {
id: number;
username: string;
fullName: string;
role: string;
createdAt: string;
}
const roles = [
{ value: 'admin', label: '管理员' },
{ value: 'staff', label: '办公室人员' },
{ value: 'warehouse', label: '仓库人员' }
];
const Users: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [formData, setFormData] = useState({
fullName: '',
role: 'staff'
});
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [resetPasswordDialog, setResetPasswordDialog] = useState(false);
const [newPassword, setNewPassword] = useState('');
const [resetError, setResetError] = useState('');
// 获取用户列表
const fetchUsers = async () => {
try {
const response = await fetch('http://localhost:8080/api/users', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
const data = await response.json();
setUsers(data);
} else {
setError('获取用户列表失败');
}
} catch (err) {
setError('网络错误,请稍后重试');
}
};
useEffect(() => {
fetchUsers();
}, []);
// 处理编辑用户
const handleEdit = (user: User) => {
setSelectedUser(user);
setFormData({
fullName: user.fullName,
role: user.role
});
setOpenDialog(true);
};
// 处理删除用户
const handleDelete = async (userId: number) => {
if (window.confirm('确定要删除这个用户吗?')) {
try {
const response = await fetch(`http://localhost:8080/api/users/${userId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
setSuccessMessage('用户删除成功');
fetchUsers();
} else {
setError('删除用户失败');
}
} catch (err) {
setError('网络错误,请稍后重试');
}
}
};
// 处理表单提交
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedUser) return;
try {
const response = await fetch(`http://localhost:8080/api/users/${selectedUser.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(formData)
});
if (response.ok) {
setSuccessMessage('用户更新成功');
setOpenDialog(false);
fetchUsers();
} else {
const data = await response.json();
setError(data.message || '更新失败');
}
} catch (err) {
setError('网络错误,请稍后重试');
}
};
const handleResetPassword = async () => {
if (!selectedUser) return;
try {
const response = await fetch(`http://localhost:8080/api/users/${selectedUser.id}/reset-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
newPassword
})
});
if (response.ok) {
setResetPasswordDialog(false);
setNewPassword('');
setResetError('');
setSuccessMessage('Password reset successfully');
} else {
const data = await response.json();
setResetError(data.message || 'Failed to reset password');
}
} catch (err) {
setResetError('Network error, please try again');
}
};
const handleResetPasswordClick = (user: User) => {
setSelectedUser(user);
setResetPasswordDialog(true);
setResetError('');
};
const handleResetPasswordClose = () => {
setResetPasswordDialog(false);
setSelectedUser(null);
setNewPassword('');
setResetError('');
};
return (
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="h5">用户管理</Typography>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>用户名</TableCell>
<TableCell>姓名</TableCell>
<TableCell>角色</TableCell>
<TableCell>创建时间</TableCell>
<TableCell>操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>{user.fullName}</TableCell>
<TableCell>
{roles.find(r => r.value === user.role)?.label || user.role}
</TableCell>
<TableCell>{new Date(user.createdAt).toLocaleString()}</TableCell>
<TableCell>
<IconButton onClick={() => handleEdit(user)} color="primary">
<EditIcon />
</IconButton>
<IconButton onClick={() => handleResetPasswordClick(user)} color="secondary">
<LockIcon />
</IconButton>
<IconButton onClick={() => handleDelete(user.id)} color="error">
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
<DialogTitle>
编辑用户
</DialogTitle>
<DialogContent>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
<TextField
fullWidth
label="用户名"
value={selectedUser?.username || ''}
margin="normal"
disabled
/>
<TextField
fullWidth
label="姓名"
value={formData.fullName}
onChange={(e) => setFormData({ ...formData, fullName: e.target.value })}
margin="normal"
required
/>
<TextField
fullWidth
select
label="角色"
value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
margin="normal"
required
>
{roles.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>取消</Button>
<Button onClick={handleSubmit} variant="contained" color="primary">
保存
</Button>
</DialogActions>
</Dialog>
<Dialog open={resetPasswordDialog} onClose={handleResetPasswordClose}>
<DialogTitle>Reset Password</DialogTitle>
<DialogContent>
<Typography variant="body2" sx={{ mb: 2 }}>
Reset password for user: {selectedUser?.username}
</Typography>
<TextField
margin="dense"
label="New Password"
type="password"
fullWidth
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
{resetError && (
<Typography color="error" sx={{ mt: 1 }}>
{resetError}
</Typography>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleResetPasswordClose}>Cancel</Button>
<Button onClick={handleResetPassword} variant="contained" color="primary">
Reset Password
</Button>
</DialogActions>
</Dialog>
<Snackbar
open={!!error}
autoHideDuration={6000}
onClose={() => setError('')}
>
<Alert severity="error" onClose={() => setError('')}>
{error}
</Alert>
</Snackbar>
<Snackbar
open={!!successMessage}
autoHideDuration={6000}
onClose={() => setSuccessMessage('')}
>
<Alert severity="success" onClose={() => setSuccessMessage('')}>
{successMessage}
</Alert>
</Snackbar>
</Box>
);
};
export default Users;
2. 测试
1. 用户自己修改密码
username: testuser3
oldPassword: admin1234
newPassword: admin123
从“头像下拉框”中进入“CHANGE PASSWORD”功能
同时可以看到,testuser3无法查看用户列表。
点击“CHANGE PASSWORD”即可修改密码。
修改后用新密码,可以登录。成功!
2. 管理员重置密码
管理员账号为: admin
要修改的用户用户名为: testuser2
新密码为:admin123
进入Users页面:
右侧有“修改密码按钮”:
弹出“修改密码”弹窗。修改并确认:
成功后,左下角会有提示。
testuser2使用新密码登录,登录成功。
完成!
至此,“仓库管理系统-QuickStore”的用户相关功能就大致完成了。
从后端到前端,我们完成了:
- 项目数据库的设计
- ER图的绘制
- Postgresql数据库的搭建
- 使用script进行数据库各个表的创建
- 使用sql语句为数据库添加测试数据
- 搭建基于Java和Spring boot的后端系统
- 完成“用户登录”“新建用户”“更新用户”“删除用户”等功能(可以再多想点加上去)
- 完成基于Spring Security的后端接口(api)访问权限控制
- 为“用户登录”等功能添加JWT验证
- 完成基于React的项目前端框架搭建
- 为前端项目编写“登录页面”“Dashboard页面”“Users页面”等
- 实现了前端的“查询用户列表”“更新用户”“删除用户”等操作
- 添加了“用户更改密码”和“管理员重置密码”功能
还有些其他功能,也可以多想下,到时候写到简历上面。
后续的“入库管理”“出库管理”“客户管理”“订单管理”“产品热销情况统计”“客户订单数量(图表)统计/大客户标记”“上线云服务器”等功能可能就后续慢慢补上了。
写简历,找工作!
希望能早点重新上岸!