实习小记(个人中心的编辑模块)

项目需要加一个个人中心的编辑模块,也是差不多搞了一天下来,其中遇到了很多问题,也是来记录、分享一下。

技术栈:React、antd、TypeScript

需求 点击编辑,弹出编辑个人信息浮窗,调用后端接口渲染初始表单。其中头像是点击上传新头像,手机号是不可修改。

代码

ts 复制代码
import {
  Form,
  Modal,
  FormProps,
  Input,
  message,
  Avatar,
  Upload,
  Typography
} from 'antd'
import React, { useImperativeHandle, useState } from 'react'
import { UserOutlined } from '@ant-design/icons'
import { Users } from '@ysx-use/certificate-service'
import { useStateAsync } from '@ysx-use/hooks'
import { noop } from '@ysx-use/shared'
import { eventBus } from '@/components'
import { personalStore, logout } from '@/store'

const { Text } = Typography

export namespace PersonalModule {
  export interface FormModalInstance {
    show: (id?: number) => void
  }
  export interface FormModalProps {
    onSuccess?: () => void
  }

  export const FormModal = React.memo(
    React.forwardRef<FormModalInstance, FormModalProps>(({ onSuccess = noop }, ref) => {
      const [isModalOpen, setIsModalOpen] = useState(false)
      const [id, setId] = useState<number>()
      const [avatarUrl, setAvatarUrl] = useState<string>('') // 显示头像

      const [form] = Form.useForm<Users.Model>()

      useImperativeHandle(ref, () => ({
        show(id) {
          setId(id)
          form.resetFields()

          if (id) {
            Users.getMeInfo().then((res) => {
              const data = res.data || {}
              form.setFieldsValue({
                ...data,
                password: undefined // 密码不显示
              })
              setAvatarUrl(data.avatar || '')
            })
          }
          setIsModalOpen(true)
        }
      }))

      const handleCancel = () => {
        setIsModalOpen(false)
      }

      const submit = useStateAsync(
        async (form: Users.Model) => {
          await Users.updatePersonalInfo({
            ...form,
            password: form.password || undefined
          })
          personalStore.remove()
          message.success('修改成功,请重新登录')
          eventBus.emit('PersonalReload', id)
          logout()
        },
        { immediate: false }
      )

      const onFinish: FormProps<Users.Model>['onFinish'] = (values) => {
        submit.execute(values)
      }

      const onFinishFailed: FormProps<Users.Model>['onFinishFailed'] = (errorInfo) => {
        console.error('Failed:', errorInfo)
      }

      const onSubmit = () => {
        form.submit()
      }

      // 上传头像逻辑
      const importAvatar = useStateAsync(
        async (file: File) => {
          const res = await Users.importPersonalAvatar(file)
          const url = res?.data || ''
          if (url) {
            setAvatarUrl(url)
            form.setFieldsValue({ avatar: url })
            message.success('头像上传成功')
          } else {
            message.error('上传失败,请重试')
          }
        },
        {
          immediate: false
        }
      )

      return (
        <Modal
          title="编辑个人信息"
          open={isModalOpen}
          onCancel={handleCancel}
          onOk={onSubmit}
          closable>
          <Form
            style={{ marginTop: 16 }}
            form={form}
            layout="vertical"
            name="personal-form"
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}
            autoComplete="off">
            <Form.Item label="头像" name="avatar">
              <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
                <Upload
                  accept=""
                  customRequest={() => {}}
                  //beforeUpload={() => false} // 阻止默认上传
                  showUploadList={false}
                  onChange={(info) => {
                    if (info.file.originFileObj) {
                      importAvatar.execute(info.file.originFileObj)
                    }
                  }}
                  maxCount={1}
                  disabled={importAvatar.isLoading}
                  name="file">
                  <Avatar
                    size={64}
                    icon={<UserOutlined />}
                    src={avatarUrl}
                    style={{ cursor: 'pointer' }}
                  />
                </Upload>
                <Text type="secondary">点击头像上传更换</Text>
              </div>
            </Form.Item>

            <Form.Item<Users.Model>
              label="账户"
              name="username"
              rules={[{ required: true, message: '请输入账户名' }]}>
              <Input placeholder="请输入账户名" />
            </Form.Item>

            <Form.Item<Users.Model> label="手机号" name="mobile" extra="手机号一旦注册不可修改">
              <Input readOnly />
            </Form.Item>

            <Form.Item<Users.Model> label="昵称" name="nickname">
              <Input placeholder="请输入昵称" />
            </Form.Item>

            <Form.Item<Users.Model> label="密码" name="password">
              <Input.Password placeholder="如需修改请输入新密码" />
            </Form.Item>
          </Form>
        </Modal>
      )
    })
  )
}
  • 个人中心页(index.tsx) :用于展示用户信息。

  • 编辑弹窗模块(FormModal) :用于修改头像、昵称、密码等信息。

个人中心主页面

实现:

使用了antd pro(中后台管理)的组件PageContainer, ProCard, ProDescriptions

从状态管理中获取当前登录用户信息

通过 useRef 持有对弹窗组件的引用,以便调用 show(user_id) 弹出表单。

挂载 <PersonalModule.FormModal ref={formModal}></PersonalModule.FormModal>

点击编辑按钮时,调用 FormModal 暴露的 show 方法,打开弹窗。

通过 ProDescriptions 来渲染个人信息

个人中心的编辑弹窗

功能:

  • 支持修改昵称、密码、头像。

  • 上传头像后立即预览。

  • 保存后强制重新登录。

实现:

通过 useImperativeHandle 实现组件间命令式通信

ts 复制代码
useImperativeHandle(ref, () => ({
  show(id) {
    // 重置表单、加载用户数据
  }
}))

其中调用了获取用户信息接口去加载当前用户信息并填充到编辑表单中

头像上传 使用封装的 useStateAsync 来处理异步上传逻辑,增强可控性与状态追踪。 通过点击 Avatar,用户可直接上传图片替换头像,体验更友好。

更新个人信息后,清空用户状态并触发登出,强制用户重新登录以刷新身份数据。

然后遇到了很多问题

手机号不可修改

加下面注释

ini 复制代码
<Form.Item<Users.Model> label="手机号" name="mobile" extra="手机号一旦注册不可修改">
    <Input ></Input>
    {/* <div style={{ color: '  #C0C0C0', fontSize: '12px' }}>手机号一旦注册不可修改</div> */}
</Form.Item>

起初是直接给Input加了一个disabled 发现不行,不会被渲染初始值 因为: Ant Design 的 Form.Item + Input disabled 组合有个限制: 被 disabled 的 Input 不会响应 setFieldsValue 的值变化,AntD 默认不更新它(这在受控组件中属于正常行为)。

改成了 readOnly

还有一个问题:<Form.Item> 内的 div 干扰了渲染

Ant Design 的 <Form.Item> 不支持你在其中直接放多个非表单组件子元素(如 <Input /> 之外的 <div>)。

这种用法会导致:

  • 表单字段渲染混乱(尤其是 setFieldsValue 失效或渲染错位);
  • 特别是你用的 form 是受控的,value 绑定会丢失。

可以使用 Form.Item 的 extra 属性,会在下方显示提示信息,渲染安全,不会干扰 Input

头像上传

首先这是又用了一个独立的接口,然后在Axios实例的请求拦截器那里要进行配置,在请求真正发出前对 config 进行处理。

ts 复制代码
instance.interceptors.request.use(
  (config) => {
    // console.log(config)
    const { url } = config
    if (url && !whiteList.includes(url)) {
      const auth = accessStore.get().access_token
      if (auth) {
        config.headers.Authorization = `Bearer ${auth}`
      }
    } else {
      config.headers.Authorization = ''
    }
    if (url === Users.ApiUrls.ExportTemplate) {
      config.responseType = 'blob'
      config.headers['Content-Type'] = 'application/vnd.ms-csv'
    }
    if (url === Users.ApiUrls.ImportData|| url===Users.ApiUrls.ImportPersonalAvatar) {
      const formData = new FormData()
      formData.append('file', config.data.file)
      config.data = formData
      config.headers['Content-Type'] = 'multipart/form-data'
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)
  1. 鉴权处理:请求 URL 存在并且不在 whiteList 白名单中,请求头加 token

  2. 文件下载:设置响应类型与内容类型

  3. 文件上传:封装为 FormData

  4. 错误处理

组件的默认行为

完成后,发现了一个不知道哪里发出的 POST http://localhost:3000/personal 请求 发现了这个url是对应的个人中心页面,但应该是 GET 方法 所以应该是某个组件或 hook 在加载 /personal 页面时自动触发了一个不必要的 POST 请求。 然后发现是头像上传组件的 问题

tsx 复制代码
<Form.Item label="头像" name="avatar">
   <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
      <Upload
         accept=""
         customRequest={() => {}}
         //beforeUpload={() => false} // 阻止默认上传
         showUploadList={false}
         onChange={(info) => {
         if (info.file.originFileObj) {
             importAvatar.execute(info.file.originFileObj)
         }
         }}
         maxCount={1}
         disabled={importAvatar.isLoading}
         name="file">
           <Avatar
              size={64}
              icon={<UserOutlined />}
              src={avatarUrl}
              style={{ cursor: 'pointer' }}
            />
       </Upload>
       <Text type="secondary">点击头像上传更换</Text>
     </div>
</Form.Item>

如果 info.file.originFileObj 不存在,Upload 组件会默认走 action="/personal" 来上传! 你没有显式指定 action 参数,所以 Ant Design 的 组件默认会使用当前页面地址 /personal 作为上传地址。

使用beforeUpload={() => false} // 阻止默认上传 加这个就不能上传本地图片了(都没有发起网络请求)

✅ 阻止 Ant Design Upload 组件的 自动上传行为

❌ 但不会触发 onChange 中的 info.file.originFileObj → 因为根本没触发上传流程

最后用customRequest={() => {}} 会触发onChange,适合手动上传

相关推荐
讨厌吃蛋黄酥1 分钟前
#Zustand:轻量级状态管理的革命,告别Context与Reducer的痛点!
前端·javascript·react.js
阿飞5272 分钟前
CSS3 超实用属性:pointer-events (可穿透图层的鼠标事件)
前端
已读不回1432 分钟前
vue3 reactive响应式会丢失?
前端·vue.js
阿飞5272 分钟前
css —pointer-events属性_css pointer-events
前端
已读不回1433 分钟前
【透彻讲解】Proxy 和 Object.defineProperty 的区别:数据代理 vs 数据劫持
前端·vue.js
练习前端两年半4 分钟前
🚀 Vue3 源码深度解析:无状态组件的渲染机制与实现原理
前端·vue.js
tech_zjf5 分钟前
基于BroadcastChannel的前端多标签页同步方案
前端·react.js·面试
大葱白菜5 分钟前
JavaWeb 进阶:Vue.js 与 Spring Boot 全栈开发实战(Java 开发者视角)
前端·后端·程序员
李明卫杭州5 分钟前
浅谈 CSS 中 vmin 和 vmax
前端
前端缘梦6 分钟前
前端工程模块化:ESM与CommonJS深度解析与最佳实践
前端·前端工程化