项目需要加一个个人中心的编辑模块,也是差不多搞了一天下来,其中遇到了很多问题,也是来记录、分享一下。
技术栈: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)
}
)
-
鉴权处理:请求 URL 存在并且不在 whiteList 白名单中,请求头加 token
-
文件下载:设置响应类型与内容类型
-
文件上传:封装为 FormData
-
错误处理
组件的默认行为
完成后,发现了一个不知道哪里发出的 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,适合手动上传