Преглед на файлове

feat: 修改个人信息和密码页面

BaiLuoYan преди 5 дни
родител
ревизия
3c395cd34a

+ 34 - 0
src/pages/Sys/ModifyPassword/components/PasswordFields.tsx

@@ -0,0 +1,34 @@
+import { ProFormText } from '@ant-design/pro-components';
+import { memo } from 'react';
+
+import { confirmPasswordRules, passwordRules } from '../lib/rules';
+
+export const PasswordFields = memo(function PasswordFields() {
+  return (
+    <div className="flex justify-center">
+      <div className="w-90">
+        <ProFormText
+          name="oldPassword"
+          label="旧密码"
+          rules={[{ required: true, message: '请输入旧密码' }]}
+          formItemProps={{ layout: 'horizontal' }}
+          fieldProps={{ type: 'password' }}
+        />
+        <ProFormText
+          name="newPassword"
+          label="新密码"
+          rules={passwordRules}
+          formItemProps={{ layout: 'horizontal' }}
+          fieldProps={{ type: 'password' }}
+        />
+        <ProFormText
+          name="confirmPassword"
+          label="确认密码"
+          rules={confirmPasswordRules}
+          formItemProps={{ layout: 'horizontal' }}
+          fieldProps={{ type: 'password' }}
+        />
+      </div>
+    </div>
+  );
+});

+ 32 - 0
src/pages/Sys/ModifyPassword/hooks/useModifyPassword.ts

@@ -0,0 +1,32 @@
+import * as api from '@/services/login';
+import { message } from '@/utils/antdAppInstance';
+import { Form } from 'antd';
+import { useCallback, useState } from 'react';
+
+export function useModifyPassword() {
+  const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
+
+  const handleSave = useCallback(async () => {
+    try {
+      const values = await form.validateFields();
+      setLoading(true);
+      const res = await api.fetchUserUpdatePassword({
+        oldPassword: values.oldPassword,
+        newPassword: values.newPassword,
+      });
+      if (res.success) {
+        message.success('修改成功');
+        form.resetFields();
+      } else {
+        message.error(res.errorMessage || '修改失败');
+      }
+    } catch {
+      // validation failed
+    } finally {
+      setLoading(false);
+    }
+  }, [form]);
+
+  return { form, loading, handleSave };
+}

+ 10 - 82
src/pages/Sys/ModifyPassword/index.tsx

@@ -1,91 +1,19 @@
-import * as api from '@/services/login';
-import { message } from '@/utils/antdAppInstance';
 import { SaveOutlined } from '@ant-design/icons';
-import { ProFormText } from '@ant-design/pro-components';
-import { useModel } from '@umijs/max';
-import { Button, Card, Form, FormInstance } from 'antd';
-import React, { useRef, useState } from 'react';
+import { Button, Card, Form } from 'antd';
 
-const ModifyPassword: React.FC = () => {
-  const { initialState } = useModel('@@initialState');
-  const formRef = useRef<FormInstance>(null);
-  const [loading, setLoading] = useState(false);
+import { PasswordFields } from './components/PasswordFields';
+import { useModifyPassword } from './hooks/useModifyPassword';
 
-  const handleSave = async () => {
-    try {
-      const values = await formRef.current?.validateFields();
-      setLoading(true);
-      const res = await api.fetchUserUpdatePassword({
-        username: initialState!.currentUser!.username!,
-        oldPassword: values.oldPassword,
-        newPassword: values.newPassword,
-      });
-      if (res.success) {
-        message.success('修改成功');
-        formRef.current?.resetFields();
-      } else {
-        message.error(res.errorMessage || '修改失败');
-      }
-    } catch (error) {
-      console.error(error);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  if (!initialState?.currentUser?.username) {
-    return <div>加载中...</div>;
-  }
+const ModifyPassword = () => {
+  const { form, loading, handleSave } = useModifyPassword();
 
   return (
-    <div style={{ padding: '24px' }}>
-      <Card title="修改密码" style={{ maxWidth: 800, margin: '0 auto' }}>
-        <Form ref={formRef} layout="vertical" labelCol={{ span: 8 }}>
-          <div className="flex justify-center">
-            <div className="w-90">
-              <ProFormText
-                name="oldPassword"
-                label="旧密码"
-                rules={[{ required: true, message: '请输入旧密码' }]}
-                formItemProps={{ layout: 'horizontal' }}
-                fieldProps={{ type: 'password' }}
-              />
-              <ProFormText
-                name="newPassword"
-                label="新密码"
-                rules={[
-                  { required: true, message: '请输入新密码' },
-                  { min: 8, message: '密码长度不能少于8位' },
-                  {
-                    pattern:
-                      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?])/,
-                    message: '密码必须包含大小写字母、数字和特殊字符',
-                  },
-                ]}
-                formItemProps={{ layout: 'horizontal' }}
-                fieldProps={{ type: 'password' }}
-              />
-              <ProFormText
-                name="confirmPassword"
-                label="确认密码"
-                rules={[
-                  { required: true, message: '请输入确认密码' },
-                  ({ getFieldValue }) => ({
-                    validator(_, value) {
-                      if (!value || getFieldValue('newPassword') === value) {
-                        return Promise.resolve();
-                      }
-                      return Promise.reject(new Error('两次输入的密码不一致'));
-                    },
-                  }),
-                ]}
-                formItemProps={{ layout: 'horizontal' }}
-                fieldProps={{ type: 'password' }}
-              />
-            </div>
-          </div>
+    <div className="p-6">
+      <Card title="修改密码" className="max-w-200 mx-auto">
+        <Form form={form} layout="vertical" labelCol={{ span: 8 }}>
+          <PasswordFields />
         </Form>
-        <div style={{ textAlign: 'center', marginTop: 24 }}>
+        <div className="text-center mt-6">
           <Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={loading}>
             保存修改
           </Button>

+ 22 - 0
src/pages/Sys/ModifyPassword/lib/rules.ts

@@ -0,0 +1,22 @@
+import type { Rule } from 'antd/es/form';
+
+export const passwordRules: Rule[] = [
+  { required: true, message: '请输入新密码' },
+  { min: 8, message: '密码长度不能少于8位' },
+  {
+    pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?])/,
+    message: '密码必须包含大小写字母、数字和特殊字符',
+  },
+];
+
+export const confirmPasswordRules: Rule[] = [
+  { required: true, message: '请输入确认密码' },
+  ({ getFieldValue }) => ({
+    validator(_, value) {
+      if (!value || getFieldValue('newPassword') === value) {
+        return Promise.resolve();
+      }
+      return Promise.reject(new Error('两次输入的密码不一致'));
+    },
+  }),
+];

+ 30 - 0
src/pages/Sys/UserInfo/components/AvatarUpload.tsx

@@ -0,0 +1,30 @@
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
+import { Avatar, Button, Upload } from 'antd';
+import { memo } from 'react';
+
+interface AvatarUploadProps {
+  avatar?: string;
+  isEditing: boolean;
+  uploading: boolean;
+  onUpload: (options: any) => void;
+}
+
+export const AvatarUpload = memo(function AvatarUpload({
+  avatar,
+  isEditing,
+  uploading,
+  onUpload,
+}: AvatarUploadProps) {
+  return (
+    <div className="text-center">
+      <div className="mb-4">
+        <Avatar size={120} src={avatar} className="border-2 border-[#f0f0f0]" />
+      </div>
+      {isEditing && (
+        <Upload showUploadList={false} customRequest={onUpload} accept="image/*">
+          <Button icon={uploading ? <LoadingOutlined /> : <PlusOutlined />}>上传头像</Button>
+        </Upload>
+      )}
+    </div>
+  );
+});

+ 152 - 0
src/pages/Sys/UserInfo/hooks/useUserInfo.ts

@@ -0,0 +1,152 @@
+import { postForm } from '@/request';
+import * as api from '@/services/login';
+import { message } from '@/utils/antdAppInstance';
+import { userKey } from '@/utils/authUtils';
+import { secureLocalStorage as ls } from '@/utils/localUtils';
+import { useModel } from '@umijs/max';
+import { Form } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+
+import type { UserFormValues } from '../lib/types';
+
+export function useUserInfo() {
+  const { initialState, setInitialState } = useModel('@@initialState');
+  const [form] = Form.useForm<UserFormValues>();
+  const [isEditing, setIsEditing] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [uploading, setUploading] = useState(false);
+  const [userInfo, setUserInfo] = useState<API.UserInfo | null>(null);
+
+  useEffect(() => {
+    if (initialState?.currentUser) {
+      setUserInfo(initialState.currentUser);
+      form.setFieldsValue({
+        username: initialState.currentUser.username ?? '',
+        nickname: initialState.currentUser.nickname ?? '',
+        avatar: initialState.currentUser.avatar ?? '',
+        email: initialState.currentUser.email ?? '',
+        phone: initialState.currentUser.phone ?? '',
+      });
+    }
+  }, [initialState?.currentUser, form]);
+
+  const handleEdit = useCallback(() => {
+    setIsEditing(true);
+  }, []);
+
+  const handleCancel = useCallback(() => {
+    setIsEditing(false);
+    if (userInfo) {
+      form.setFieldsValue({
+        username: userInfo.username ?? '',
+        nickname: userInfo.nickname ?? '',
+        avatar: userInfo.avatar ?? '',
+        email: userInfo.email ?? '',
+        phone: userInfo.phone ?? '',
+      });
+    }
+  }, [userInfo, form]);
+
+  const handleSave = useCallback(async () => {
+    try {
+      const values = await form.validateFields();
+      setLoading(true);
+
+      const payload: API.UserUpdateInfoReq = {};
+      if (values.nickname !== userInfo?.nickname) payload.nickname = values.nickname;
+      if (values.avatar !== userInfo?.avatar) payload.avatar = values.avatar;
+      if (values.email !== userInfo?.email) payload.email = values.email;
+      if (values.phone !== userInfo?.phone) payload.phone = values.phone;
+
+      if (Object.keys(payload).length === 0) {
+        message.info('未修改任何信息');
+        setIsEditing(false);
+        setLoading(false);
+        return;
+      }
+
+      const result = await api.fetchUserUpdateInfo(payload);
+
+      if (result.success) {
+        message.success('修改成功');
+        setIsEditing(false);
+
+        const updatedFields = {
+          nickname: values.nickname,
+          avatar: values.avatar,
+          email: values.email,
+          phone: values.phone,
+        };
+
+        setInitialState((prev) => ({
+          ...prev,
+          currentUser: {
+            ...prev?.currentUser,
+            ...updatedFields,
+          },
+        }));
+
+        const localUserInfo = ls.getLocal<API.UserInfo>(userKey);
+        const newUserInfo = { ...localUserInfo, ...updatedFields };
+        ls.setLocal<API.UserInfo>(userKey, newUserInfo);
+        setUserInfo(newUserInfo);
+      } else {
+        message.error(result.errorMessage || '修改失败');
+      }
+    } catch {
+      message.error('修改失败');
+    } finally {
+      setLoading(false);
+    }
+  }, [form, userInfo, setInitialState]);
+
+  const handleAvatarUpload = useCallback(
+    async (options: any) => {
+      const { file } = options;
+      const isImage = file.type.startsWith('image/');
+      if (!isImage) {
+        message.error('只能上传图片文件!');
+        return;
+      }
+      if (file.size / 1024 / 1024 > 1) {
+        message.error('头像大小不能超过1MB');
+        return;
+      }
+
+      setUploading(true);
+      try {
+        const formData = new FormData();
+        formData.append('file', file);
+        formData.append('fileType', 'avatar');
+        const res = await postForm<API.Result<{ url: string; path: string }>>(
+          '/minio/upload',
+          formData,
+        );
+        if (res.success && res.data) {
+          form.setFieldValue('avatar', res.data.path);
+          setUserInfo((prev) => (prev ? { ...prev, avatar: res.data!.url } : prev));
+          message.success('上传成功');
+        } else {
+          message.error(res.errorMessage || '上传失败');
+        }
+      } catch {
+        message.error('上传失败');
+      } finally {
+        setUploading(false);
+      }
+    },
+    [form],
+  );
+
+  return {
+    form,
+    isEditing,
+    loading,
+    uploading,
+    userInfo,
+    handleEdit,
+    handleCancel,
+    handleSave,
+    handleAvatarUpload,
+  };
+}

+ 25 - 151
src/pages/Sys/UserInfo/index.tsx

@@ -1,142 +1,30 @@
-import { postForm } from '@/request';
-import * as api from '@/services/login';
-import { message } from '@/utils/antdAppInstance';
-import { userKey } from '@/utils/authUtils';
-import { secureLocalStorage as ls } from '@/utils/localUtils';
-import {
-  CloseOutlined,
-  EditOutlined,
-  LoadingOutlined,
-  PlusOutlined,
-  SaveOutlined,
-} from '@ant-design/icons';
+import { CloseOutlined, EditOutlined, SaveOutlined } from '@ant-design/icons';
 import { ProFormText } from '@ant-design/pro-components';
-import { useModel } from '@umijs/max';
-import { Avatar, Button, Card, Col, Form, Row, Space, Upload } from 'antd';
-import React, { useEffect, useState } from 'react';
+import { Button, Card, Col, Form, Row, Space } from 'antd';
 
-const UserInfo: React.FC = () => {
-  const { initialState, setInitialState } = useModel('@@initialState');
-  const [form] = Form.useForm();
-  const [isEditing, setIsEditing] = useState(false);
-  const [loading, setLoading] = useState(false);
-  const [uploading, setUploading] = useState(false);
-  const [userInfo, setUserInfo] = useState<API.UserInfo | null>(null);
+import { AvatarUpload } from './components/AvatarUpload';
+import { useUserInfo } from './hooks/useUserInfo';
 
-  useEffect(() => {
-    if (initialState?.currentUser) {
-      setUserInfo(initialState.currentUser);
-      form.setFieldsValue({
-        username: initialState.currentUser.username,
-        nickname: initialState.currentUser.nickname,
-        avatar: initialState.currentUser.avatar,
-        email: initialState.currentUser.email,
-        phone: initialState.currentUser.phone,
-      });
-    }
-  }, [initialState?.currentUser, form]);
-
-  const handleEdit = () => {
-    setIsEditing(true);
-  };
-
-  const handleCancel = () => {
-    setIsEditing(false);
-    if (userInfo) {
-      form.setFieldsValue({
-        username: userInfo.username,
-        nickname: userInfo.nickname,
-        avatar: userInfo.avatar,
-        email: userInfo.email,
-        phone: userInfo.phone,
-      });
-    }
-  };
-
-  const handleSave = async () => {
-    try {
-      const values = await form.validateFields();
-      setLoading(true);
-
-      const result = await api.fetchUserUpdateInfo({
-        username: values.username,
-        nickname: values.nickname,
-        avatar: values.avatar,
-        email: values.email,
-        phone: values.phone,
-      });
-
-      if (result.success) {
-        message.success('修改成功');
-        setIsEditing(false);
-
-        setInitialState((prev) => ({
-          ...prev,
-          currentUser: {
-            ...prev?.currentUser,
-            ...values,
-          },
-        }));
-
-        const localUserInfo = ls.getLocal<API.UserInfo>(userKey);
-        const newUserInfo = {
-          ...localUserInfo,
-          ...values,
-        };
-        ls.setLocal<API.UserInfo>(userKey, newUserInfo);
-        setUserInfo(newUserInfo);
-      } else {
-        message.error(result.errorMessage || '修改失败');
-      }
-    } catch (error) {
-      message.error('修改失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const handleAvatarUpload = async (options: any) => {
-    const { file } = options;
-    const isImage = file.type.startsWith('image/');
-    if (!isImage) {
-      message.error('只能上传图片文件!');
-      return;
-    }
-    if (file.size / 1024 / 1024 > 1) {
-      message.error('头像大小不能超过1MB');
-      return;
-    }
-
-    setUploading(true);
-    try {
-      const formData = new FormData();
-      formData.append('file', file);
-      formData.append('fileType', 'avatar');
-      const res = await postForm<API.Result<{ url: string; path: string }>>(
-        '/minio/upload',
-        formData,
-      );
-      if (res.success && res.data) {
-        form.setFieldValue('avatar', res.data.path);
-        setUserInfo((prev) => (prev ? { ...prev, avatar: res.data!.url } : prev));
-        message.success('上传成功');
-      } else {
-        message.error(res.errorMessage || '上传失败');
-      }
-    } catch {
-      message.error('上传失败');
-    } finally {
-      setUploading(false);
-    }
-  };
+const UserInfo = () => {
+  const {
+    form,
+    isEditing,
+    loading,
+    uploading,
+    userInfo,
+    handleEdit,
+    handleCancel,
+    handleSave,
+    handleAvatarUpload,
+  } = useUserInfo();
 
   if (!userInfo) {
     return <div>加载中...</div>;
   }
 
   return (
-    <div style={{ padding: '24px' }}>
-      <Card title="用户信息" style={{ maxWidth: 800, margin: '0 auto' }}>
+    <div className="p-6">
+      <Card title="用户信息" className="max-w-200 mx-auto">
         <Form form={form} layout="vertical">
           <Row gutter={24}>
             <Col span={12}>
@@ -174,30 +62,16 @@ const UserInfo: React.FC = () => {
             </Col>
             <Col span={12}>
               <Form.Item name="avatar" hidden />
-              <div style={{ textAlign: 'center' }}>
-                <div style={{ marginBottom: 16 }}>
-                  <Avatar
-                    size={120}
-                    src={userInfo.avatar}
-                    style={{ border: '2px solid #f0f0f0' }}
-                  />
-                </div>
-                {isEditing && (
-                  <Upload
-                    showUploadList={false}
-                    customRequest={handleAvatarUpload}
-                    accept="image/*"
-                  >
-                    <Button icon={uploading ? <LoadingOutlined /> : <PlusOutlined />}>
-                      上传头像
-                    </Button>
-                  </Upload>
-                )}
-              </div>
+              <AvatarUpload
+                avatar={userInfo.avatar}
+                isEditing={isEditing}
+                uploading={uploading}
+                onUpload={handleAvatarUpload}
+              />
             </Col>
           </Row>
         </Form>
-        <div style={{ textAlign: 'center', marginTop: 24 }}>
+        <div className="text-center mt-6">
           {!isEditing ? (
             <Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>
               修改信息

+ 7 - 0
src/pages/Sys/UserInfo/lib/types.ts

@@ -0,0 +1,7 @@
+export interface UserFormValues {
+  username: string;
+  nickname: string;
+  avatar: string;
+  email: string;
+  phone: string;
+}

+ 1 - 1
src/services/login/index.ts

@@ -61,7 +61,7 @@ export async function fetchUserUpdateInfo(
   body: API.UserUpdateInfoReq,
   options?: { [key: string]: any },
 ) {
-  return request<API.UserUpdateInfoResult>('/user/updateInfo', {
+  return request<API.UserUpdateInfoResult>('/auth/updateInfo', {
     method: 'POST',
     data: body,
     ...(options || {}),

+ 4 - 6
src/services/login/typings.d.ts

@@ -76,16 +76,14 @@ declare namespace API {
   type CapEndpointResult = Result<CapEndpointData>;
 
   interface UserUpdateInfoReq {
-    username: string;
-    nickname: string;
-    avatar: string;
-    email: string;
-    phone: string;
+    nickname?: string;
+    avatar?: string;
+    email?: string;
+    phone?: string;
   }
   type UserUpdateInfoResult = Result<Empty>;
 
   interface UserUpdatePasswordReq {
-    username: string;
     oldPassword: string;
     newPassword: string;
   }