| | |
| | | `nickname` VARCHAR(64) NULL, |
| | | `real_name` VARCHAR(64) NULL COMMENT '人员名称', |
| | | `phone` VARCHAR(20) NULL COMMENT '联系电话', |
| | | `avatar` VARCHAR(255) NULL COMMENT '头像', |
| | | `remark` VARCHAR(255) NULL COMMENT '备注', |
| | | `status` TINYINT NOT NULL DEFAULT 1, |
| | | `deleted` TINYINT(1) NOT NULL DEFAULT 0, |
| New file |
| | |
| | | SET @avatar_column_exists := ( |
| | | SELECT COUNT(*) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = DATABASE() |
| | | AND TABLE_NAME = 'sys_user' |
| | | AND COLUMN_NAME = 'avatar' |
| | | ); |
| | | |
| | | SET @avatar_column_sql := IF( |
| | | @avatar_column_exists = 0, |
| | | 'ALTER TABLE `sys_user` ADD COLUMN `avatar` VARCHAR(255) NULL COMMENT ''头像'' AFTER `phone`', |
| | | 'SELECT ''sys_user.avatar already exists''' |
| | | ); |
| | | |
| | | PREPARE avatar_column_stmt FROM @avatar_column_sql; |
| | | EXECUTE avatar_column_stmt; |
| | | DEALLOCATE PREPARE avatar_column_stmt; |
| | |
| | | package com.tcm.training.admin.auth.controller; |
| | | |
| | | import com.tcm.training.admin.auth.dto.ChangePasswordRequest; |
| | | import com.tcm.training.admin.auth.dto.LoginRequest; |
| | | import com.tcm.training.admin.auth.dto.ProfileUpdateRequest; |
| | | import com.tcm.training.admin.auth.service.AuthService; |
| | | import com.tcm.training.admin.auth.service.CaptchaService; |
| | | import com.tcm.training.admin.auth.vo.CaptchaResponse; |
| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.PutMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | |
| | | public ApiResponse<UserVO> me() { |
| | | return ApiResponse.ok(authService.me()); |
| | | } |
| | | |
| | | @PutMapping("/profile") |
| | | public ApiResponse<UserVO> updateProfile(@Valid @RequestBody ProfileUpdateRequest req) { |
| | | return ApiResponse.ok(authService.updateProfile(req)); |
| | | } |
| | | |
| | | @PostMapping("/change-password") |
| | | public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest req) { |
| | | authService.changePassword(req); |
| | | return ApiResponse.ok(null); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.auth.dto; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | import javax.validation.constraints.NotBlank; |
| | | import javax.validation.constraints.Size; |
| | | |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class ChangePasswordRequest { |
| | | |
| | | @NotBlank(message = "请输入原密码") |
| | | private String oldPassword; |
| | | |
| | | @NotBlank(message = "请输入新密码") |
| | | @Size(min = 12, max = 64, message = "新密码长度需为12~64位") |
| | | private String newPassword; |
| | | |
| | | @NotBlank(message = "请输入确认密码") |
| | | private String confirmPassword; |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.auth.dto; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | import javax.validation.constraints.NotBlank; |
| | | import javax.validation.constraints.Size; |
| | | |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class ProfileUpdateRequest { |
| | | |
| | | @NotBlank(message = "请输入用户名") |
| | | @Size(max = 64, message = "用户名长度不能超过64个字符") |
| | | private String nickname; |
| | | |
| | | @Size(max = 255, message = "头像地址长度不能超过255个字符") |
| | | private String avatar; |
| | | } |
| | |
| | | package com.tcm.training.admin.auth.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.tcm.training.admin.auth.dto.ChangePasswordRequest; |
| | | import com.tcm.training.admin.auth.dto.LoginRequest; |
| | | import com.tcm.training.admin.auth.dto.ProfileUpdateRequest; |
| | | import com.tcm.training.admin.auth.vo.LoginResponse; |
| | | import com.tcm.training.admin.auth.vo.UserVO; |
| | | import com.tcm.training.admin.user.entity.SysUser; |
| | |
| | | import cn.dev33.satoken.stp.StpUtil; |
| | | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import java.util.regex.Pattern; |
| | | |
| | | @Service |
| | | public class AuthService { |
| | | |
| | | private static final Pattern PASSWORD_PATTERN = Pattern.compile( |
| | | "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d]).{12,64}$" |
| | | ); |
| | | |
| | | private final SysUserService sysUserService; |
| | | private final BCryptPasswordEncoder passwordEncoder; |
| | |
| | | } |
| | | |
| | | public UserVO me() { |
| | | Long userId = StpUtil.getLoginIdAsLong(); |
| | | SysUser user = sysUserService.getById(userId); |
| | | if (user == null || (user.getDeleted() != null && user.getDeleted() == 1)) { |
| | | throw new IllegalArgumentException("用户不存在"); |
| | | } |
| | | return buildUserVO(requireCurrentUser()); |
| | | } |
| | | |
| | | @Transactional |
| | | public UserVO updateProfile(ProfileUpdateRequest req) { |
| | | SysUser user = requireCurrentUser(); |
| | | user.setNickname(normalizeRequired(req.getNickname(), "请输入用户名")); |
| | | user.setAvatar(trimToNull(req.getAvatar())); |
| | | user.setUpdatedAt(LocalDateTime.now()); |
| | | sysUserService.updateById(user); |
| | | return buildUserVO(user); |
| | | } |
| | | |
| | | @Transactional |
| | | public void changePassword(ChangePasswordRequest req) { |
| | | SysUser user = requireCurrentUser(); |
| | | if (!passwordEncoder.matches(req.getOldPassword(), user.getPassword())) { |
| | | throw new IllegalArgumentException("原密码错误"); |
| | | } |
| | | if (!req.getNewPassword().equals(req.getConfirmPassword())) { |
| | | throw new IllegalArgumentException("两次密码输入不一致"); |
| | | } |
| | | validatePassword(req.getNewPassword()); |
| | | user.setPassword(passwordEncoder.encode(req.getNewPassword())); |
| | | user.setUpdatedAt(LocalDateTime.now()); |
| | | sysUserService.updateById(user); |
| | | StpUtil.logout(); |
| | | } |
| | | |
| | | private UserVO buildUserVO(SysUser user) { |
| | |
| | | user.getNickname(), |
| | | user.getRealName(), |
| | | user.getPhone(), |
| | | user.getAvatar(), |
| | | roleCodes, |
| | | permissionKeys |
| | | ); |
| | | } |
| | | |
| | | private SysUser requireCurrentUser() { |
| | | Long userId = StpUtil.getLoginIdAsLong(); |
| | | SysUser user = sysUserService.getById(userId); |
| | | if (user == null || (user.getDeleted() != null && user.getDeleted() == 1)) { |
| | | throw new IllegalArgumentException("用户不存在"); |
| | | } |
| | | return user; |
| | | } |
| | | |
| | | private void validatePassword(String password) { |
| | | if (!PASSWORD_PATTERN.matcher(password).matches()) { |
| | | throw new IllegalArgumentException("密码需至少12位,且包含大小写字母、数字和特殊字符"); |
| | | } |
| | | } |
| | | |
| | | private String normalizeRequired(String value, String message) { |
| | | String normalized = trimToNull(value); |
| | | if (normalized == null) { |
| | | throw new IllegalArgumentException(message); |
| | | } |
| | | return normalized; |
| | | } |
| | | |
| | | private String trimToNull(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | String trimmed = value.trim(); |
| | | return trimmed.isEmpty() ? null : trimmed; |
| | | } |
| | | } |
| | |
| | | private String nickname; |
| | | private String realName; |
| | | private String phone; |
| | | private String avatar; |
| | | private List<String> roleCodes; |
| | | private List<String> permissionKeys; |
| | | |
| | |
| | | String nickname, |
| | | String realName, |
| | | String phone, |
| | | String avatar, |
| | | List<String> roleCodes, |
| | | List<String> permissionKeys |
| | | ) { |
| | |
| | | this.nickname = nickname; |
| | | this.realName = realName; |
| | | this.phone = phone; |
| | | this.avatar = avatar; |
| | | this.roleCodes = roleCodes; |
| | | this.permissionKeys = permissionKeys; |
| | | } |
| | |
| | | |
| | | private String phone; |
| | | |
| | | private String avatar; |
| | | |
| | | private String remark; |
| | | |
| | | private Integer status; |
| | |
| | | import { SettingDrawer } from '@ant-design/pro-components'; |
| | | import type { RunTimeLayoutConfig } from '@umijs/max'; |
| | | import { history } from '@umijs/max'; |
| | | import defaultSettings from '../config/defaultSettings'; |
| | | import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown'; |
| | | import { errorConfig } from './requestErrorConfig'; |
| | | import { decryptData } from '@/utils/utils'; |
| | | import defaultSettings from '../config/defaultSettings'; |
| | | import '../public/font.css'; |
| | | import { AvatarDropdown } from './components/RightContent/AvatarDropdown'; |
| | | import { errorConfig } from './requestErrorConfig'; |
| | | |
| | | const loginPath = '/login'; |
| | | import '../public/font.css' |
| | | |
| | | type CurrentUser = { |
| | | username?: string; |
| | | nickname?: string; |
| | | avatar?: string; |
| | | realName?: string; |
| | | phone?: string; |
| | | permissionKeys?: string[]; |
| | | }; |
| | | |
| | | type InitialState = { |
| | |
| | | const { location } = history; |
| | | |
| | | if (location.pathname !== loginPath && !decryptData(localStorage.getItem('userInfo'))) { |
| | | history.replace(loginPath) |
| | | history.replace(loginPath); |
| | | } |
| | | return { |
| | | permission: decryptData(localStorage.getItem('access')) || {}, |
| | | currentUser: decryptData(localStorage.getItem('userInfo')) || {}, |
| | | settings: { ...defaultSettings, fixedHeader: true } as Partial<LayoutSettings>, |
| | | }; |
| | | } |
| | | }; |
| | | |
| | | // ProLayout 支持的api https://procomponents.ant.design/components/layout |
| | | export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { |
| | | return { |
| | | avatarProps: { |
| | | title: <AvatarName />, |
| | | render: (_, avatarChildren) => { |
| | | return <AvatarDropdown>{avatarChildren}</AvatarDropdown>; |
| | | render: () => { |
| | | return <AvatarDropdown />; |
| | | }, |
| | | }, |
| | | waterMarkProps: { |
| | |
| | | ); |
| | | }, |
| | | ...initialState?.settings, |
| | | pageTitleRender: () => '管理后台' |
| | | pageTitleRender: () => '管理后台', |
| | | }; |
| | | }; |
| | | |
| | |
| | | import { outLogin } from '@/services/ant-design-pro/api'; |
| | | import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; |
| | | import { |
| | | changeCurrentUserPassword, |
| | | logout, |
| | | updateCurrentUserProfile, |
| | | uploadCurrentUserAvatar, |
| | | } from '@/pages/Login/service'; |
| | | import { decryptData, encryptData } from '@/utils/utils'; |
| | | import { LockOutlined, LogoutOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons'; |
| | | import { useEmotionCss } from '@ant-design/use-emotion-css'; |
| | | import { history, useModel } from '@umijs/max'; |
| | | import { Spin } from 'antd'; |
| | | import { Avatar, Form, Image, Input, Modal, Spin, Upload, message } from 'antd'; |
| | | import { stringify } from 'querystring'; |
| | | import type { MenuInfo } from 'rc-menu/lib/interface'; |
| | | import React, { useCallback } from 'react'; |
| | | import React, { useCallback, useMemo, useState } from 'react'; |
| | | import { flushSync } from 'react-dom'; |
| | | import HeaderDropdown from '../HeaderDropdown'; |
| | | |
| | | export type GlobalHeaderRightProps = { |
| | | menu?: boolean; |
| | | children?: React.ReactNode; |
| | | type ProfileValues = { |
| | | nickname: string; |
| | | avatar?: string; |
| | | }; |
| | | |
| | | export const AvatarName = () => { |
| | | const { initialState } = useModel('@@initialState'); |
| | | // |
| | | |
| | | const { currentUser } = initialState || {}; |
| | | return <span className="anticon">{currentUser?.username || '超级管理员'}</span>; |
| | | type PasswordValues = { |
| | | oldPassword: string; |
| | | newPassword: string; |
| | | confirmPassword: string; |
| | | }; |
| | | |
| | | export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, children }) => { |
| | | export const AvatarDropdown: React.FC = () => { |
| | | const [profileForm] = Form.useForm<ProfileValues>(); |
| | | const [passwordForm] = Form.useForm<PasswordValues>(); |
| | | const [profileOpen, setProfileOpen] = useState(false); |
| | | const [passwordOpen, setPasswordOpen] = useState(false); |
| | | const [profileSubmitting, setProfileSubmitting] = useState(false); |
| | | const [passwordSubmitting, setPasswordSubmitting] = useState(false); |
| | | const [avatarUploading, setAvatarUploading] = useState(false); |
| | | const [avatarPreview, setAvatarPreview] = useState(''); |
| | | |
| | | /** |
| | | * 退出登录,并且将当前的 url 保存 |
| | | */ |
| | | const loginOut = async () => { |
| | | // await outLogin(); |
| | | const redirectToLogin = useCallback(() => { |
| | | const { search, pathname } = window.location; |
| | | const urlParams = new URL(window.location.href).searchParams; |
| | | /** 此方法会跳转到 redirect 参数所在的位置 */ |
| | | const redirect = urlParams.get('redirect'); |
| | | // Note: There may be security issues, please note |
| | | if (window.location.pathname !== '/login' && !redirect) { |
| | | history.replace({ |
| | | pathname: '/login', |
| | |
| | | }), |
| | | }); |
| | | } |
| | | }; |
| | | }, []); |
| | | const actionClassName = useEmotionCss(({ token }) => { |
| | | return { |
| | | display: 'flex', |
| | |
| | | }, |
| | | }; |
| | | }); |
| | | const avatarNameClassName = useEmotionCss(() => ({ |
| | | marginLeft: 8, |
| | | color: '#303133', |
| | | fontSize: 14, |
| | | })); |
| | | const uploadBoxClassName = useEmotionCss(() => ({ |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center', |
| | | width: 72, |
| | | height: 72, |
| | | overflow: 'hidden', |
| | | background: '#fafafa', |
| | | border: '1px dashed #d9d9d9', |
| | | borderRadius: 4, |
| | | cursor: 'pointer', |
| | | })); |
| | | const { initialState, setInitialState } = useModel('@@initialState'); |
| | | const { currentUser } = initialState || {}; |
| | | const displayName = useMemo( |
| | | () => currentUser?.nickname || currentUser?.username || '超级管理员', |
| | | [currentUser?.nickname, currentUser?.username], |
| | | ); |
| | | |
| | | const syncCurrentUser = useCallback( |
| | | (user: Record<string, unknown>) => { |
| | | localStorage.setItem('userInfo', encryptData(user)); |
| | | flushSync(() => { |
| | | setInitialState((state) => ({ |
| | | ...state, |
| | | currentUser: user, |
| | | permission: decryptData(localStorage.getItem('access')) || state?.permission, |
| | | })); |
| | | }); |
| | | }, |
| | | [setInitialState], |
| | | ); |
| | | |
| | | const openProfileModal = useCallback(() => { |
| | | profileForm.setFieldsValue({ |
| | | nickname: currentUser?.nickname || currentUser?.username || '', |
| | | avatar: currentUser?.avatar || '', |
| | | }); |
| | | setAvatarPreview(currentUser?.avatar || ''); |
| | | setProfileOpen(true); |
| | | }, [currentUser?.avatar, currentUser?.nickname, currentUser?.username, profileForm]); |
| | | |
| | | const openPasswordModal = useCallback(() => { |
| | | passwordForm.resetFields(); |
| | | setPasswordOpen(true); |
| | | }, [passwordForm]); |
| | | |
| | | const clearSessionAndRedirect = useCallback(() => { |
| | | localStorage.clear(); |
| | | flushSync(() => { |
| | | setInitialState((state) => ({ ...state, currentUser: undefined, permission: undefined })); |
| | | }); |
| | | redirectToLogin(); |
| | | }, [redirectToLogin, setInitialState]); |
| | | |
| | | const handleLogout = useCallback(async () => { |
| | | try { |
| | | await logout(); |
| | | } catch {} |
| | | clearSessionAndRedirect(); |
| | | }, [clearSessionAndRedirect]); |
| | | |
| | | const handleProfileSubmit = useCallback(async () => { |
| | | const values = await profileForm.validateFields(); |
| | | setProfileSubmitting(true); |
| | | try { |
| | | const response = await updateCurrentUserProfile({ |
| | | nickname: values.nickname?.trim(), |
| | | avatar: values.avatar || undefined, |
| | | }); |
| | | syncCurrentUser(response.data || {}); |
| | | message.success('帐号设置保存成功'); |
| | | setProfileOpen(false); |
| | | } finally { |
| | | setProfileSubmitting(false); |
| | | } |
| | | }, [profileForm, syncCurrentUser]); |
| | | |
| | | const handlePasswordSubmit = useCallback(async () => { |
| | | const values = await passwordForm.validateFields(); |
| | | setPasswordSubmitting(true); |
| | | try { |
| | | await changeCurrentUserPassword(values); |
| | | message.success('密码修改成功,请重新登录'); |
| | | setPasswordOpen(false); |
| | | clearSessionAndRedirect(); |
| | | } finally { |
| | | setPasswordSubmitting(false); |
| | | } |
| | | }, [clearSessionAndRedirect, passwordForm]); |
| | | |
| | | const onMenuClick = useCallback( |
| | | (event: MenuInfo) => { |
| | | const { key } = event; |
| | | if (key === 'logout') { |
| | | localStorage.clear(); |
| | | flushSync(() => { |
| | | setInitialState((s) => ({ ...s, currentUser: undefined })); |
| | | }); |
| | | loginOut(); |
| | | handleLogout(); |
| | | return; |
| | | } |
| | | history.push(`/account/${key}`); |
| | | if (key === 'profile') { |
| | | openProfileModal(); |
| | | return; |
| | | } |
| | | if (key === 'password') { |
| | | openPasswordModal(); |
| | | } |
| | | }, |
| | | [setInitialState], |
| | | [handleLogout, openPasswordModal, openProfileModal], |
| | | ); |
| | | |
| | | const loading = ( |
| | |
| | | return loading; |
| | | } |
| | | |
| | | const { currentUser } = initialState; |
| | | |
| | | if (!currentUser || !currentUser.username) { |
| | | return loading; |
| | | } |
| | | |
| | | const menuItems = [ |
| | | ...(menu |
| | | ? [ |
| | | { |
| | | key: 'center', |
| | | icon: <UserOutlined />, |
| | | label: '个人中心', |
| | | }, |
| | | { |
| | | key: 'settings', |
| | | icon: <SettingOutlined />, |
| | | label: '个人设置', |
| | | }, |
| | | { |
| | | type: 'divider' as const, |
| | | }, |
| | | ] |
| | | : []), |
| | | { |
| | | key: 'profile', |
| | | icon: <SettingOutlined />, |
| | | label: '帐号设置', |
| | | }, |
| | | { |
| | | key: 'password', |
| | | icon: <LockOutlined />, |
| | | label: '重置密码', |
| | | }, |
| | | { |
| | | type: 'divider' as const, |
| | | }, |
| | | { |
| | | key: 'logout', |
| | | icon: <LogoutOutlined />, |
| | |
| | | ]; |
| | | |
| | | return ( |
| | | <HeaderDropdown |
| | | menu={{ |
| | | selectedKeys: [], |
| | | onClick: onMenuClick, |
| | | items: menuItems, |
| | | }} |
| | | > |
| | | {children} |
| | | </HeaderDropdown> |
| | | <> |
| | | <HeaderDropdown |
| | | menu={{ |
| | | selectedKeys: [], |
| | | onClick: onMenuClick, |
| | | items: menuItems, |
| | | }} |
| | | > |
| | | <span className={actionClassName}> |
| | | <Avatar src={currentUser.avatar} size={32}> |
| | | {displayName.slice(0, 1)} |
| | | </Avatar> |
| | | <span className={avatarNameClassName}>{displayName}</span> |
| | | </span> |
| | | </HeaderDropdown> |
| | | |
| | | <Modal |
| | | centered |
| | | destroyOnClose |
| | | open={profileOpen} |
| | | title="个人信息设置" |
| | | width={560} |
| | | onCancel={() => setProfileOpen(false)} |
| | | onOk={handleProfileSubmit} |
| | | okText="保存" |
| | | cancelText="取消" |
| | | confirmLoading={profileSubmitting} |
| | | > |
| | | <Form form={profileForm} layout="vertical"> |
| | | <Form.Item |
| | | label="用户名" |
| | | name="nickname" |
| | | rules={[{ required: true, whitespace: true, message: '请输入用户名' }]} |
| | | > |
| | | <Input placeholder="请输入" maxLength={64} /> |
| | | </Form.Item> |
| | | <Form.Item label="头像"> |
| | | <Form.Item name="avatar" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Upload |
| | | accept="image/*" |
| | | showUploadList={false} |
| | | customRequest={async (options) => { |
| | | setAvatarUploading(true); |
| | | try { |
| | | const result = await uploadCurrentUserAvatar(options.file as File); |
| | | const url = result.data?.url; |
| | | if (!url) { |
| | | throw new Error('上传失败'); |
| | | } |
| | | profileForm.setFieldsValue({ avatar: url }); |
| | | setAvatarPreview(url); |
| | | options.onSuccess?.(result); |
| | | } catch (error) { |
| | | message.error('头像上传失败'); |
| | | options.onError?.(error as Error); |
| | | } finally { |
| | | setAvatarUploading(false); |
| | | } |
| | | }} |
| | | > |
| | | <div className={uploadBoxClassName}> |
| | | {avatarPreview ? ( |
| | | <Image src={avatarPreview} width={72} height={72} preview={false} /> |
| | | ) : avatarUploading ? ( |
| | | <Spin size="small" /> |
| | | ) : ( |
| | | <PlusOutlined /> |
| | | )} |
| | | </div> |
| | | </Upload> |
| | | </Form.Item> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Modal |
| | | centered |
| | | destroyOnClose |
| | | open={passwordOpen} |
| | | title="重置密码" |
| | | width={560} |
| | | onCancel={() => setPasswordOpen(false)} |
| | | onOk={handlePasswordSubmit} |
| | | okText="保存" |
| | | cancelText="取消" |
| | | confirmLoading={passwordSubmitting} |
| | | > |
| | | <Form form={passwordForm} layout="vertical"> |
| | | <Form.Item |
| | | label="原密码" |
| | | name="oldPassword" |
| | | rules={[{ required: true, message: '请输入原密码' }]} |
| | | > |
| | | <Input.Password placeholder="请输入原密码" maxLength={64} /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="新密码" |
| | | name="newPassword" |
| | | rules={[ |
| | | { required: true, message: '请输入新密码' }, |
| | | { |
| | | pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z\d]).{12,64}$/, |
| | | message: '请输入至少12位含大小写字母、数字和特殊字符的密码', |
| | | }, |
| | | ]} |
| | | > |
| | | <Input.Password |
| | | placeholder="请输入至少12位含大小写字母、数字和特殊字符的密码" |
| | | maxLength={64} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="确认密码" |
| | | name="confirmPassword" |
| | | dependencies={['newPassword']} |
| | | rules={[ |
| | | { required: true, message: '请输入确认密码' }, |
| | | ({ getFieldValue }) => ({ |
| | | validator(_, value) { |
| | | if (!value || getFieldValue('newPassword') === value) { |
| | | return Promise.resolve(); |
| | | } |
| | | return Promise.reject(new Error('两次密码输入不一致')); |
| | | }, |
| | | }), |
| | | ]} |
| | | > |
| | | <Input.Password |
| | | placeholder="请输入至少12位含大小写字母、数字和特殊字符的密码" |
| | | maxLength={64} |
| | | /> |
| | | </Form.Item> |
| | | </Form> |
| | | </Modal> |
| | | </> |
| | | ); |
| | | }; |
| | |
| | | localStorage.setItem('access', encryptData(permission)); |
| | | localStorage.setItem('token', encryptData(authToken)); |
| | | localStorage.setItem('userInfo', encryptData(res.data.user)); |
| | | setInitialState((state: any) => ({ |
| | | setInitialState((state) => ({ |
| | | ...state, |
| | | permission, |
| | | currentUser: res.data.user, |
| | | settings: Settings, |
| | | })); |
| | | message.success('登录成功'); |
| | | const urlParams = new URL(window.location.href).searchParams; |
| | |
| | | token: string; |
| | | user: { |
| | | username?: string; |
| | | nickname?: string; |
| | | avatar?: string; |
| | | permissionKeys?: string[]; |
| | | [key: string]: unknown; |
| | | }; |
| | |
| | | }; |
| | | }; |
| | | |
| | | export type CurrentUserResponse = { |
| | | data: { |
| | | id?: number; |
| | | username?: string; |
| | | nickname?: string; |
| | | realName?: string; |
| | | phone?: string; |
| | | avatar?: string; |
| | | roleCodes?: string[]; |
| | | permissionKeys?: string[]; |
| | | [key: string]: unknown; |
| | | }; |
| | | }; |
| | | |
| | | export type UploadImageResponse = { |
| | | data?: { |
| | | url?: string; |
| | | fileName?: string; |
| | | }; |
| | | }; |
| | | |
| | | export function updatePwd(params: Record<string, unknown>): Promise<unknown>; |
| | | export function sendMessage(params: Record<string, unknown>): Promise<unknown>; |
| | | export function login(data: Record<string, unknown>): Promise<LoginResponse>; |
| | | export function getCaptcha(): Promise<CaptchaResponse>; |
| | | export function logout(): Promise<unknown>; |
| | | export function getCurrentUser(): Promise<CurrentUserResponse>; |
| | | export function updateCurrentUserProfile(data: Record<string, unknown>): Promise<CurrentUserResponse>; |
| | | export function changeCurrentUserPassword(data: Record<string, unknown>): Promise<unknown>; |
| | | export function uploadCurrentUserAvatar(file: File): Promise<UploadImageResponse>; |
| | |
| | | method: 'GET', |
| | | }); |
| | | } |
| | | |
| | | export async function logout() { |
| | | return request('/auth/logout', { |
| | | method: 'POST', |
| | | }); |
| | | } |
| | | |
| | | export async function getCurrentUser() { |
| | | return request('/auth/me', { |
| | | method: 'GET', |
| | | }); |
| | | } |
| | | |
| | | export async function updateCurrentUserProfile(data) { |
| | | return request('/auth/profile', { |
| | | method: 'PUT', |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export async function changeCurrentUserPassword(data) { |
| | | return request('/auth/change-password', { |
| | | method: 'POST', |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export async function uploadCurrentUserAvatar(file) { |
| | | const formData = new FormData(); |
| | | formData.append('file', file); |
| | | return request('/upload/image', { |
| | | method: 'POST', |
| | | data: formData, |
| | | }); |
| | | } |
| | |
| | | import { history } from '@umijs/max'; |
| | | import { decryptData } from '@/utils/utils'; |
| | | |
| | | const runtimeBaseUrl = typeof BASE_URL === 'undefined' ? '' : BASE_URL; |
| | | |
| | | /** |
| | | * @name 错误处理 |
| | | * pro 自带的错误处理, 可以在这里做自己的改动 |
| | | * @doc https://umijs.org/docs/max/request#配置 |
| | | */ |
| | | export const errorConfig: RequestConfig = { |
| | | baseURL: BASE_URL, |
| | | baseURL: runtimeBaseUrl, |
| | | |
| | | // 请求拦截器 |
| | | requestInterceptors: [ |
| | |
| | | import { uploadFile } from './service' |
| | | import CryptoJS from 'crypto-js'; |
| | | |
| | | const normalizePath = (path) => { |
| | | if (typeof path !== 'string') { |
| | | return ''; |
| | | } |
| | | if (path.length > 1 && path.endsWith('/')) { |
| | | return path.replace(/\/+$/, ''); |
| | | } |
| | | return path; |
| | | }; |
| | | |
| | | const isRouteMatch = (targetPath, routePath) => { |
| | | const normalizedTargetPath = normalizePath(targetPath); |
| | | const normalizedRoutePath = normalizePath(routePath); |
| | | return ( |
| | | normalizedTargetPath === normalizedRoutePath || |
| | | normalizedTargetPath.startsWith(`${normalizedRoutePath}/`) |
| | | ); |
| | | }; |
| | | |
| | | const flattenRoutes = (routes = [], parentAuthority) => { |
| | | return routes.reduce((accumulator, route) => { |
| | | const routeAuthority = route.authority || parentAuthority; |
| | | accumulator.push({ |
| | | path: normalizePath(route.path), |
| | | authority: routeAuthority, |
| | | }); |
| | | if (Array.isArray(route.routes) && route.routes.length > 0) { |
| | | accumulator.push(...flattenRoutes(route.routes, routeAuthority)); |
| | | } |
| | | return accumulator; |
| | | }, []); |
| | | }; |
| | | |
| | | export const isUrl = (path) => { |
| | | if (typeof path !== 'string' || !path) { |
| | | return false; |
| | | } |
| | | return /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(:\d+)?(\/[^\s]*)?$/i.test( |
| | | path, |
| | | ); |
| | | }; |
| | | |
| | | export const getRouteAuthority = (path, routeData = []) => { |
| | | const matchedRoute = flattenRoutes(routeData) |
| | | .filter((route) => route.path && isRouteMatch(path, route.path)) |
| | | .sort((prev, next) => next.path.length - prev.path.length) |
| | | .find((route) => route.authority !== undefined); |
| | | |
| | | return matchedRoute?.authority; |
| | | }; |
| | | |
| | | export const customRequest = (params, type) => { |
| | | const { file, onSuccess, onError } = params; |
| | | uploadFile(file) |
| | |
| | | if (!cipherText) return null; |
| | | const bytes = CryptoJS.AES.decrypt(cipherText, 'your-secret-key-123'); |
| | | return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); |
| | | }; |
| | | }; |