feat: support batch register resources

This commit is contained in:
2025-11-15 16:36:29 +08:00
parent 8873c183e7
commit 63a813925f
3 changed files with 157 additions and 3 deletions

View File

@@ -0,0 +1,143 @@
import React from 'react'
import { Layout, Collapse, Form, Input, Button, Space, message, Spin } from 'antd'
import type { FormInstance } from 'antd'
import PeopleForm from './PeopleForm.tsx'
import { createPeoplesBatch, type People } from '../apis'
const { Panel } = Collapse as any
const { Content } = Layout
type FormItem = { id: string }
const BatchRegister: React.FC = () => {
const [commonForm] = Form.useForm()
const [items, setItems] = React.useState<FormItem[]>([{ id: `${Date.now()}-${Math.random()}` }])
const instancesRef = React.useRef<Record<string, FormInstance>>({})
const [loading, setLoading] = React.useState(false)
const addItem = () => {
if (loading) return
setItems((arr) => [...arr, { id: `${Date.now()}-${Math.random()}` }])
}
const removeItem = (id: string) => {
if (loading) return
setItems((arr) => arr.filter((x) => x.id !== id))
delete instancesRef.current[id]
}
const buildPeople = (values: any, common: any): People => {
return {
name: values.name,
contact: values.contact || common.contact || undefined,
gender: values.gender,
age: values.age,
height: values.height || undefined,
marital_status: values.marital_status || undefined,
introduction: values.introduction || {},
match_requirement: values.match_requirement || undefined,
cover: values.cover || undefined,
}
}
const handleSubmit = async () => {
if (loading) return
try {
setLoading(true)
const common = await commonForm.validateFields().catch(() => ({}))
const ids = items.map((x) => x.id)
const forms = ids.map((id) => instancesRef.current[id]).filter(Boolean)
if (forms.length !== ids.length) {
setLoading(false)
message.error('表单未就绪')
return
}
const allValues: any[] = []
for (const f of forms) {
try {
const v = await f.validateFields()
allValues.push(v)
} catch (err: any) {
setLoading(false)
message.error('请完善全部表单后再提交')
return
}
}
const payload: People[] = allValues.map((v) => buildPeople(v, common))
const res = await createPeoplesBatch(payload)
const failedIdx: number[] = []
res.forEach((r, i) => {
if (!r || r.error_code !== 0) failedIdx.push(i)
})
const success = res.length - failedIdx.length
if (success > 0) message.success(`成功提交 ${success}`)
if (failedIdx.length > 0) {
message.error(`${failedIdx.length} 条提交失败,请检查后重试`)
setItems((prev) => prev.filter((_, i) => failedIdx.includes(i)))
} else {
setItems([{ id: `${Date.now()}-${Math.random()}` }])
commonForm.resetFields()
}
} catch (e: any) {
message.error('提交失败')
} finally {
setLoading(false)
}
}
return (
<Content className="main-content">
<div className="content-body">
<Collapse defaultActiveKey={["common"]}>
<Panel header="公共属性" key="common">
<Form form={commonForm} layout="vertical" size="large">
<Form.Item name="contact" label="联系人">
<Input placeholder="请输入联系人(可留空)" />
</Form.Item>
</Form>
</Panel>
</Collapse>
<div style={{ height: 16 }} />
<Collapse defaultActiveKey={items.map((x) => x.id)}>
{items.map((item, idx) => (
<Panel
header={`注册表单 #${idx + 1}`}
key={item.id}
extra={
<Button danger size="small" onClick={() => removeItem(item.id)} disabled={loading}>
</Button>
}
>
<PeopleForm
hideSubmitButton
onFormReady={(f) => (instancesRef.current[item.id] = f)}
/>
</Panel>
))}
</Collapse>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16 }}>
<Space>
<Button onClick={addItem} disabled={loading}></Button>
</Space>
<Button type="primary" onClick={handleSubmit} loading={loading}>
{loading ? '提交中...' : '提交'}
</Button>
</div>
{loading && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.08)' }}>
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
<Spin size="large" />
</div>
</div>
)}
</div>
</Content>
)
}
export default BatchRegister

View File

@@ -4,6 +4,7 @@ import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import SiderMenu from './SiderMenu.tsx';
import MainContent from './MainContent.tsx';
import ResourceList from './ResourceList.tsx';
import BatchRegister from './BatchRegister.tsx';
import TopBar from './TopBar.tsx';
import '../styles/base.css';
import '../styles/layout.css';
@@ -21,6 +22,8 @@ const LayoutWrapper: React.FC = () => {
switch (path) {
case '/resources':
return 'menu1';
case '/batch-register':
return 'batch';
case '/menu2':
return 'menu2';
default:
@@ -35,6 +38,9 @@ const LayoutWrapper: React.FC = () => {
case 'home':
navigate('/');
break;
case 'batch':
navigate('/batch-register');
break;
case 'menu1':
navigate('/resources');
break;
@@ -77,6 +83,10 @@ const LayoutWrapper: React.FC = () => {
/>
}
/>
<Route
path="/batch-register"
element={<BatchRegister />}
/>
<Route
path="/resources"
element={

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Layout, Menu, Grid, Drawer, Button } from 'antd';
import { HeartOutlined, FormOutlined, UnorderedListOutlined, MenuOutlined } from '@ant-design/icons';
import { HeartOutlined, FormOutlined, UnorderedListOutlined, MenuOutlined, CopyOutlined } from '@ant-design/icons';
import './SiderMenu.css';
const { Sider } = Layout;
@@ -44,8 +44,9 @@ const SiderMenu: React.FC<Props> = ({ onNavigate, selectedKey, mobileOpen, onMob
}, [selectedKey]);
const items = [
{ key: 'home', label: '注册', icon: <FormOutlined /> },
{ key: 'menu1', label: '列表', icon: <UnorderedListOutlined /> },
{ key: 'home', label: '录入资源', icon: <FormOutlined /> },
{ key: 'batch', label: '批量录入', icon: <CopyOutlined /> },
{ key: 'menu1', label: '资源列表', icon: <UnorderedListOutlined /> },
];
// 移动端:使用 Drawer 覆盖主内容