feat: support batch register resources
This commit is contained in:
143
src/components/BatchRegister.tsx
Normal file
143
src/components/BatchRegister.tsx
Normal 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
|
||||
@@ -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={
|
||||
|
||||
@@ -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 覆盖主内容
|
||||
|
||||
Reference in New Issue
Block a user