feat: basic layout and pages

- add basic layout with sidebar and main container
- add three menus in sidebar
- add people form page for post people
- add text and image input for recognize people info
- add people table page for show peoples
This commit is contained in:
2025-10-21 11:47:28 +08:00
parent 7c28eda415
commit ddb04ff15e
20 changed files with 1284 additions and 102 deletions

View File

@@ -0,0 +1,113 @@
import React, { useEffect, useState, useRef } from 'react';
import { Row, Col, Input, Button } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import './KeyValueList.css';
export type DictValue = Record<string, string>;
type KeyValuePair = { id: string; k: string; v: string };
type Props = {
value?: DictValue;
onChange?: (value: DictValue) => void;
};
const KeyValueList: React.FC<Props> = ({ value, onChange }) => {
const [rows, setRows] = useState<KeyValuePair[]>([]);
useEffect(() => {
// 初始化时提供一行空输入;之后只合并父值,不再自动新增空行
const initializedRef = (KeyValueList as any)._initializedRef || { current: false };
(KeyValueList as any)._initializedRef = initializedRef;
setRows((prev) => {
const existingIdByKey = new Map(prev.filter((r) => r.k).map((r) => [r.k, r.id]));
const valuePairs: KeyValuePair[] = value
? Object.keys(value).map((key) => ({
id: existingIdByKey.get(key) || `${key}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
k: key,
v: value[key] ?? '',
}))
: [];
const blankRows = prev.filter((r) => !r.k);
let merged = [...valuePairs, ...blankRows];
if (!initializedRef.current && merged.length === 0) {
merged = [{ id: `row-${Date.now()}-${Math.random().toString(36).slice(2)}`, k: '', v: '' }];
}
initializedRef.current = true;
return merged;
});
}, [value]);
const emitChange = (nextRows: KeyValuePair[]) => {
const dict: DictValue = {};
nextRows.forEach((r) => {
if (r.k && r.k.trim() !== '') {
dict[r.k] = r.v ?? '';
}
});
onChange?.(dict);
};
const updateRow = (id: string, field: 'k' | 'v', val: string) => {
const next = rows.map((r) => (r.id === id ? { ...r, [field]: val } : r));
setRows(next);
emitChange(next);
};
const addRow = () => {
const next = [...rows, { id: `row-${Date.now()}-${Math.random().toString(36).slice(2)}`, k: '', v: '' }];
setRows(next);
// 不触发 onChange因为字典未变化空行不入字典
};
const removeRow = (id: string) => {
const removed = rows.find((r) => r.id === id);
const next = rows.filter((r) => r.id !== id);
setRows(next);
if (removed?.k && removed.k.trim() !== '') {
emitChange(next);
}
};
return (
<div className="kv-list">
{rows.map((r) => (
<div className="kv-row" key={r.id}>
<Row gutter={[12, 12]} align="middle">
<Col xs={24} md={10}>
<Input
size="large"
placeholder="键(例如:籍贯、职业)"
value={r.k}
onChange={(e) => updateRow(r.id, 'k', e.target.value)}
/>
</Col>
<Col xs={24} md={12}>
<Input
size="large"
placeholder="值(例如:北京、产品经理)"
value={r.v}
onChange={(e) => updateRow(r.id, 'v', e.target.value)}
/>
</Col>
<Col xs={24} md={2}>
<Button
className="kv-remove"
aria-label="删除"
icon={<DeleteOutlined />}
onClick={() => removeRow(r.id)}
/>
</Col>
</Row>
</div>
))}
<Button type="dashed" block icon={<PlusOutlined />} onClick={addRow}>
</Button>
</div>
);
};
export default KeyValueList;