From 95a503fee52795bf50a45b4fc89f0b71fba6dc0a Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期五, 27 三月 2026 10:45:17 +0800
Subject: [PATCH] feat:首页帐号设计、重置密码
---
backend/src/main/java/com/tcm/training/admin/user/entity/SysUser.java | 2
backend/sql/init.sql | 1
tcm-training-admin/src/app.tsx | 25 +
backend/src/main/java/com/tcm/training/admin/auth/vo/UserVO.java | 3
backend/src/main/java/com/tcm/training/admin/auth/service/AuthService.java | 71 ++++++
tcm-training-admin/src/pages/Login/service.d.ts | 28 ++
backend/src/main/java/com/tcm/training/admin/auth/controller/AuthController.java | 14 +
tcm-training-admin/src/components/RightContent/AvatarDropdown.tsx | 335 +++++++++++++++++++++++++----
tcm-training-admin/src/pages/Login/index.tsx | 3
tcm-training-admin/src/utils/utils.js | 53 ++++
tcm-training-admin/src/pages/Login/service.js | 35 +++
backend/src/main/java/com/tcm/training/admin/auth/dto/ChangePasswordRequest.java | 24 ++
backend/src/main/java/com/tcm/training/admin/auth/dto/ProfileUpdateRequest.java | 21 +
backend/sql/update_admin_profile_schema.sql | 17 +
tcm-training-admin/src/requestErrorConfig.ts | 4
15 files changed, 563 insertions(+), 73 deletions(-)
diff --git a/backend/sql/init.sql b/backend/sql/init.sql
index 89cd4db..6e3f022 100644
--- a/backend/sql/init.sql
+++ b/backend/sql/init.sql
@@ -5,6 +5,7 @@
`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,
diff --git a/backend/sql/update_admin_profile_schema.sql b/backend/sql/update_admin_profile_schema.sql
new file mode 100644
index 0000000..893f501
--- /dev/null
+++ b/backend/sql/update_admin_profile_schema.sql
@@ -0,0 +1,17 @@
+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;
diff --git a/backend/src/main/java/com/tcm/training/admin/auth/controller/AuthController.java b/backend/src/main/java/com/tcm/training/admin/auth/controller/AuthController.java
index 86c05a8..9ad721f 100644
--- a/backend/src/main/java/com/tcm/training/admin/auth/controller/AuthController.java
+++ b/backend/src/main/java/com/tcm/training/admin/auth/controller/AuthController.java
@@ -1,6 +1,8 @@
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;
@@ -10,6 +12,7 @@
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;
@@ -49,4 +52,15 @@
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);
+ }
}
diff --git a/backend/src/main/java/com/tcm/training/admin/auth/dto/ChangePasswordRequest.java b/backend/src/main/java/com/tcm/training/admin/auth/dto/ChangePasswordRequest.java
new file mode 100644
index 0000000..0d968e3
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/auth/dto/ChangePasswordRequest.java
@@ -0,0 +1,24 @@
+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;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/auth/dto/ProfileUpdateRequest.java b/backend/src/main/java/com/tcm/training/admin/auth/dto/ProfileUpdateRequest.java
new file mode 100644
index 0000000..47fda00
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/auth/dto/ProfileUpdateRequest.java
@@ -0,0 +1,21 @@
+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;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/auth/service/AuthService.java b/backend/src/main/java/com/tcm/training/admin/auth/service/AuthService.java
index 3bd2481..b2ec3a4 100644
--- a/backend/src/main/java/com/tcm/training/admin/auth/service/AuthService.java
+++ b/backend/src/main/java/com/tcm/training/admin/auth/service/AuthService.java
@@ -1,7 +1,9 @@
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;
@@ -11,12 +13,18 @@
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;
@@ -66,12 +74,33 @@
}
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) {
@@ -83,8 +112,40 @@
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;
+ }
}
diff --git a/backend/src/main/java/com/tcm/training/admin/auth/vo/UserVO.java b/backend/src/main/java/com/tcm/training/admin/auth/vo/UserVO.java
index 0e88d44..378bea6 100644
--- a/backend/src/main/java/com/tcm/training/admin/auth/vo/UserVO.java
+++ b/backend/src/main/java/com/tcm/training/admin/auth/vo/UserVO.java
@@ -13,6 +13,7 @@
private String nickname;
private String realName;
private String phone;
+ private String avatar;
private List<String> roleCodes;
private List<String> permissionKeys;
@@ -25,6 +26,7 @@
String nickname,
String realName,
String phone,
+ String avatar,
List<String> roleCodes,
List<String> permissionKeys
) {
@@ -33,6 +35,7 @@
this.nickname = nickname;
this.realName = realName;
this.phone = phone;
+ this.avatar = avatar;
this.roleCodes = roleCodes;
this.permissionKeys = permissionKeys;
}
diff --git a/backend/src/main/java/com/tcm/training/admin/user/entity/SysUser.java b/backend/src/main/java/com/tcm/training/admin/user/entity/SysUser.java
index 0883fac..957fd06 100644
--- a/backend/src/main/java/com/tcm/training/admin/user/entity/SysUser.java
+++ b/backend/src/main/java/com/tcm/training/admin/user/entity/SysUser.java
@@ -31,6 +31,8 @@
private String phone;
+ private String avatar;
+
private String remark;
private Integer status;
diff --git a/tcm-training-admin/src/app.tsx b/tcm-training-admin/src/app.tsx
index 6db2086..f0c27c3 100644
--- a/tcm-training-admin/src/app.tsx
+++ b/tcm-training-admin/src/app.tsx
@@ -2,15 +2,21 @@
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 = {
@@ -29,22 +35,21 @@
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: {
@@ -74,7 +79,7 @@
);
},
...initialState?.settings,
- pageTitleRender: () => '管理后台'
+ pageTitleRender: () => '管理后台',
};
};
diff --git a/tcm-training-admin/src/components/RightContent/AvatarDropdown.tsx b/tcm-training-admin/src/components/RightContent/AvatarDropdown.tsx
index cd04fac..9c155eb 100644
--- a/tcm-training-admin/src/components/RightContent/AvatarDropdown.tsx
+++ b/tcm-training-admin/src/components/RightContent/AvatarDropdown.tsx
@@ -1,38 +1,48 @@
-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',
@@ -41,7 +51,7 @@
}),
});
}
- };
+ }, []);
const actionClassName = useEmotionCss(({ token }) => {
return {
display: 'flex',
@@ -57,22 +67,118 @@
},
};
});
+ 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 = (
@@ -91,30 +197,24 @@
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 />,
@@ -123,14 +223,141 @@
];
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>
+ </>
);
};
diff --git a/tcm-training-admin/src/pages/Login/index.tsx b/tcm-training-admin/src/pages/Login/index.tsx
index 5f1c3d6..dc25857 100644
--- a/tcm-training-admin/src/pages/Login/index.tsx
+++ b/tcm-training-admin/src/pages/Login/index.tsx
@@ -59,11 +59,10 @@
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;
diff --git a/tcm-training-admin/src/pages/Login/service.d.ts b/tcm-training-admin/src/pages/Login/service.d.ts
index d0d72cc..c424593 100644
--- a/tcm-training-admin/src/pages/Login/service.d.ts
+++ b/tcm-training-admin/src/pages/Login/service.d.ts
@@ -4,6 +4,8 @@
token: string;
user: {
username?: string;
+ nickname?: string;
+ avatar?: string;
permissionKeys?: string[];
[key: string]: unknown;
};
@@ -17,7 +19,33 @@
};
};
+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>;
diff --git a/tcm-training-admin/src/pages/Login/service.js b/tcm-training-admin/src/pages/Login/service.js
index 5655e80..10fb742 100644
--- a/tcm-training-admin/src/pages/Login/service.js
+++ b/tcm-training-admin/src/pages/Login/service.js
@@ -35,3 +35,38 @@
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,
+ });
+}
diff --git a/tcm-training-admin/src/requestErrorConfig.ts b/tcm-training-admin/src/requestErrorConfig.ts
index 1fe8042..5c889cb 100644
--- a/tcm-training-admin/src/requestErrorConfig.ts
+++ b/tcm-training-admin/src/requestErrorConfig.ts
@@ -4,13 +4,15 @@
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: [
diff --git a/tcm-training-admin/src/utils/utils.js b/tcm-training-admin/src/utils/utils.js
index ac3ef48..7d84455 100644
--- a/tcm-training-admin/src/utils/utils.js
+++ b/tcm-training-admin/src/utils/utils.js
@@ -1,6 +1,57 @@
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)
@@ -94,4 +145,4 @@
if (!cipherText) return null;
const bytes = CryptoJS.AES.decrypt(cipherText, 'your-secret-key-123');
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
-};
\ No newline at end of file
+};
--
Gitblit v1.7.1