“mitao”
2026-03-27 95a503fee52795bf50a45b4fc89f0b71fba6dc0a
feat:首页帐号设计、重置密码
3个文件已添加
12个文件已修改
636 ■■■■■ 已修改文件
backend/sql/init.sql 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/sql/update_admin_profile_schema.sql 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/auth/controller/AuthController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/auth/dto/ChangePasswordRequest.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/auth/dto/ProfileUpdateRequest.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/auth/service/AuthService.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/auth/vo/UserVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/user/entity/SysUser.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/app.tsx 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/components/RightContent/AvatarDropdown.tsx 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/Login/index.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/Login/service.d.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/Login/service.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/requestErrorConfig.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/utils/utils.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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,
backend/sql/update_admin_profile_schema.sql
New file
@@ -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;
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);
    }
}
backend/src/main/java/com/tcm/training/admin/auth/dto/ChangePasswordRequest.java
New file
@@ -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;
}
backend/src/main/java/com/tcm/training/admin/auth/dto/ProfileUpdateRequest.java
New file
@@ -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;
}
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;
    }
}
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;
    }
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;
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: () => '管理后台',
  };
};
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>
    </>
  );
};
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;
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>;
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,
  });
}
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: [
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));
};
};