實習小記(個人中心的編輯模塊)
項目需要加一個個人中心的編輯模塊,也是差不多搞了一天下來,其中遇到了很多問題,也是來記錄、分享一下。
技術棧:React、antd、TypeScript
需求
點擊編輯,彈出編輯個人信息浮窗,調用后端接口渲染初始表單。其中頭像是點擊上傳新頭像,手機號是不可修改。
代碼
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 } = Typographyexport 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 (<Modaltitle="編輯個人信息"open={isModalOpen}onCancel={handleCancel}onOk={onSubmit}closable><Formstyle={{ 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 }}><Uploadaccept=""customRequest={() => {}}//beforeUpload={() => false} // 阻止默認上傳showUploadList={false}onChange={(info) => {if (info.file.originFileObj) {importAvatar.execute(info.file.originFileObj)}}}maxCount={1}disabled={importAvatar.isLoading}name="file"><Avatarsize={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 實現組件間命令式通信
useImperativeHandle(ref, () => ({show(id) {// 重置表單、加載用戶數據}
}))
其中調用了獲取用戶信息接口去加載當前用戶信息并填充到編輯表單中
頭像上傳
使用封裝的 useStateAsync 來處理異步上傳邏輯,增強可控性與狀態追蹤。
通過點擊 Avatar,用戶可直接上傳圖片替換頭像,體驗更友好。
更新個人信息后,清空用戶狀態并觸發登出,強制用戶重新登錄以刷新身份數據。
然后遇到了很多問題
手機號不可修改
加下面注釋
<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 進行處理。
instance.interceptors.request.use((config) => {// console.log(config)const { url } = configif (url && !whiteList.includes(url)) {const auth = accessStore.get().access_tokenif (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 = formDataconfig.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 請求。
然后發現是頭像上傳組件的 問題
<Form.Item label="頭像" name="avatar"><div style={{ display: 'flex', alignItems: 'center', gap: 16 }}><Uploadaccept=""customRequest={() => {}}//beforeUpload={() => false} // 阻止默認上傳showUploadList={false}onChange={(info) => {if (info.file.originFileObj) {importAvatar.execute(info.file.originFileObj)}}}maxCount={1}disabled={importAvatar.isLoading}name="file"><Avatarsize={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,適合手動上傳