Browse Source

fix: reset admin organization forms after async submit

bob 1 month ago
parent
commit
17b3affe44
2 changed files with 65 additions and 12 deletions
  1. 13 0
      AGENTS.md
  2. 52 12
      src/pages/admin/AdminOrganizations.tsx

+ 13 - 0
AGENTS.md

@@ -587,3 +587,16 @@
 - 未确认与风险:尚未用已登录真实用户验证 PPTX 备注解析、自动上传、课程编辑及后台流程;这些功能仍为部分确认。上次部署记录的 5 门课程与本次部署前备份的 4 门不同,变化发生在本次重部署前,原因未在本次调查。
 - 后续修改提示:若要追查课程数变化,检查应用审计和数据库备份,不要直接恢复整个库覆盖当前数据;后续部署仍须核对镜像、持久卷、域名及真实业务路径。
 - 关联:C-20260923-26、C-20260923-27;提交 `fcf1e84`。
+
+### C-20260923-29|修复管理后台组织管理创建组织及表单异步 reset 报错
+
+- 日期:2026-09-23
+- 类型:修复 / 前端
+- 需求与原因:在管理后台创建组织时,接口调用成功但页面抛出 `Cannot read properties of null (reading 'reset')` 并在错误栏显示红字;排查确认系 React 异步事件处理函数中在 `await` 之后直接访问 `e.currentTarget.reset()` 导致(恢复执行时 SyntheticEvent 的 `currentTarget` 已被清空为 `null`);同时全面排查项目中其他表单提交逻辑与异步事件处理。
+- 修改范围:`src/pages/admin/AdminOrganizations.tsx` 在 `await` 前将 `e.currentTarget` 赋值给本地变量 `formElement`,成功后调用 `formElement.reset()`;在 `create`、`setMember`、`points`、`rename`、`updateSlug`、`review` 等表单与操作提交前统一清空旧的 `error` 和 `message` 状态;优化成员与积分表单成功后的重置体验。
+- 保留/新增判断:遵守 D-001 至 D-010。前端表单处理中涉及异步操作并需要 `reset()` 时,必须在首个 `await` 之前保存 DOM 元素引用。
+- 功能确认状态:部分确认
+- 验证证据:已静态全量扫描 `src/` 下所有 `.reset()`、`currentTarget`、`target` 及 `onSubmit` 异步函数,确认无其他类似跨 await 访问空引用的隐患;`pnpm run check`(`tsc -b`)通过,`pnpm run build`(`vite build`)通过。
+- 未确认与风险:未在生产环境或浏览器真机模拟网络延迟端到端点击;未重新构建并推送 Docker 镜像至服务器。
+- 后续修改提示:编写表单异步提交函数时,需遵循在 `await` 之前提取数据和保存表单引用(或使用受控组件/React Hook Form)的范式。
+- 关联:C-20260923-25。

+ 52 - 12
src/pages/admin/AdminOrganizations.tsx

@@ -10,9 +10,19 @@ export function AdminOrganizations() {
   async function load() { try { setOrganizations((await api.admin.organizations()).organizations) } catch (e) { setError((e as Error).message) } }
   useEffect(() => { load() }, [])
   async function create(e: FormEvent<HTMLFormElement>) {
-    e.preventDefault(); const form = new FormData(e.currentTarget)
-    try { await api.admin.createOrganization(String(form.get('name')), String(form.get('slug'))); setMessage('组织已创建'); (e.currentTarget as HTMLFormElement).reset(); await load() }
-    catch (x) { setError((x as Error).message) }
+    e.preventDefault()
+    const formElement = e.currentTarget
+    const form = new FormData(formElement)
+    setError('')
+    setMessage('')
+    try {
+      await api.admin.createOrganization(String(form.get('name')), String(form.get('slug')))
+      setMessage('组织已创建')
+      formElement.reset()
+      await load()
+    } catch (x) {
+      setError((x as Error).message)
+    }
   }
   return <div className="page admin-page"><div className="admin-header-row"><div><h2>组织管理</h2><p>创建组织并管理成员与积分</p></div></div>
     {error && <div className="notice notice-error">{error}</div>}{message && <div className="notice notice-success">{message}</div>}
@@ -35,27 +45,57 @@ export function AdminOrganizationDetail() {
   async function load() { try { setData(await api.admin.organization(id)) } catch (x) { setError((x as Error).message) } }
   useEffect(() => { load() }, [id])
   async function setMember(e: FormEvent<HTMLFormElement>) {
-    e.preventDefault(); const f = new FormData(e.currentTarget)
+    e.preventDefault()
+    const formElement = e.currentTarget
+    const f = new FormData(formElement)
+    setError('')
+    setMessage('')
     const phone = String(f.get('phone'))
     try {
       const person = (await api.admin.listUsers(phone)).users.find((u) => u.phone === phone)
       if (!person) return setError('未找到该手机号对应的用户')
       await api.admin.setOrganizationMember(id, person.id, String(f.get('role')) as 'MEMBER' | 'MANAGER')
-      setMessage('成员关系已更新'); await load()
+      setMessage('成员关系已更新')
+      formElement.reset()
+      setUsers([])
+      await load()
     }
     catch (x) { setError((x as Error).message) }
   }
   async function search(phone: string) { try { setUsers((await api.admin.listUsers(phone)).users) } catch (x) { setError((x as Error).message) } }
-  async function review(applicationId: string, approve: boolean) { try { await api.admin.reviewOrganizationApplication(id, applicationId, approve); await load() } catch (x) { setError((x as Error).message) } }
+  async function review(applicationId: string, approve: boolean) {
+    setError('')
+    setMessage('')
+    try { await api.admin.reviewOrganizationApplication(id, applicationId, approve); await load() } catch (x) { setError((x as Error).message) }
+  }
   async function points(e: FormEvent<HTMLFormElement>) {
-    e.preventDefault(); const f = new FormData(e.currentTarget)
-    try { await api.admin.adjustOrganizationPoints(id, Number(f.get('amount')), String(f.get('reason'))); setMessage('积分已调整'); await load() }
+    e.preventDefault()
+    const formElement = e.currentTarget
+    const f = new FormData(formElement)
+    setError('')
+    setMessage('')
+    try {
+      await api.admin.adjustOrganizationPoints(id, Number(f.get('amount')), String(f.get('reason')))
+      setMessage('积分已调整')
+      formElement.reset()
+      await load()
+    }
     catch (x) { setError((x as Error).message) }
   }
-  async function rename(e: FormEvent<HTMLFormElement>) { e.preventDefault(); const f = new FormData(e.currentTarget)
-    try { await api.admin.renameOrganization(id, String(f.get('name'))); setMessage('组织名称已更新'); await load() } catch (x) { setError((x as Error).message) } }
-  async function updateSlug(e: FormEvent<HTMLFormElement>) { e.preventDefault(); const f = new FormData(e.currentTarget)
-    try { await api.admin.updateOrganizationSlug(id, String(f.get('slug'))); setMessage('公开页面访问后缀已更新'); await load() } catch (x) { setError((x as Error).message) } }
+  async function rename(e: FormEvent<HTMLFormElement>) {
+    e.preventDefault()
+    const f = new FormData(e.currentTarget)
+    setError('')
+    setMessage('')
+    try { await api.admin.renameOrganization(id, String(f.get('name'))); setMessage('组织名称已更新'); await load() } catch (x) { setError((x as Error).message) }
+  }
+  async function updateSlug(e: FormEvent<HTMLFormElement>) {
+    e.preventDefault()
+    const f = new FormData(e.currentTarget)
+    setError('')
+    setMessage('')
+    try { await api.admin.updateOrganizationSlug(id, String(f.get('slug'))); setMessage('公开页面访问后缀已更新'); await load() } catch (x) { setError((x as Error).message) }
+  }
   if (!data) return <div className="page admin-page">{error || '正在加载组织…'}</div>
   return <div className="page admin-page"><Link className="back-link" to="/admin/organizations">← 全部组织</Link><h2>{data.organization.name} · {data.organization.is_active ? '正常' : '已停用'}</h2>
     {error && <div className="notice notice-error">{error}</div>}{message && <div className="notice notice-success">{message}</div>}