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