diff --git a/src/apis/config.ts b/src/apis/config.ts index d16a397..343fe5d 100644 --- a/src/apis/config.ts +++ b/src/apis/config.ts @@ -17,4 +17,5 @@ export const API_ENDPOINTS = { // 新增单个资源路径 /people PEOPLE: '/people', PEOPLE_BY_ID: (id: string) => `/people/${id}`, + PEOPLE_REMARK_BY_ID: (id: string) => `/people/${id}/remark`, } as const; \ No newline at end of file diff --git a/src/apis/people.ts b/src/apis/people.ts index 1155989..2b5fa0a 100644 --- a/src/apis/people.ts +++ b/src/apis/people.ts @@ -121,6 +121,25 @@ export async function updatePeople(peopleId: string, people: People): Promise(API_ENDPOINTS.PEOPLE_BY_ID(peopleId), requestData); } +/** + * 添加或更新人员备注 + * @param peopleId 人员ID + * @param content 备注内容 + * @returns Promise + */ +export async function addOrUpdateRemark(peopleId: string, content: string): Promise { + return post(API_ENDPOINTS.PEOPLE_REMARK_BY_ID(peopleId), { content }); +} + +/** + * 删除人员备注 + * @param peopleId 人员ID + * @returns Promise + */ +export async function deleteRemark(peopleId: string): Promise { + return del(API_ENDPOINTS.PEOPLE_REMARK_BY_ID(peopleId)); +} + /** * 批量创建人员信息 * @param peopleList 人员信息数组 diff --git a/src/apis/types.ts b/src/apis/types.ts index 1b581c2..1792a8d 100644 --- a/src/apis/types.ts +++ b/src/apis/types.ts @@ -50,6 +50,7 @@ export interface People { age?: number; height?: number; marital_status?: string; + created_at?: number; [key: string]: any; cover?: string; } diff --git a/src/components/BatchRegister.tsx b/src/components/BatchRegister.tsx new file mode 100644 index 0000000..785aa95 --- /dev/null +++ b/src/components/BatchRegister.tsx @@ -0,0 +1,162 @@ +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 InputDrawer from './InputDrawer.tsx' +import { createPeoplesBatch, type People } from '../apis' + +const { Panel } = Collapse as any +const { Content } = Layout + +type FormItem = { id: string; initialData?: any } + +type Props = { inputOpen?: boolean; onCloseInput?: () => void; containerEl?: HTMLElement | null } + +const BatchRegister: React.FC = ({ inputOpen = false, onCloseInput, containerEl }) => { + const [commonForm] = Form.useForm() + const [items, setItems] = React.useState([]) + const instancesRef = React.useRef>({}) + 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 handleInputResult = (list: any) => { + const arr = Array.isArray(list) ? list : [list] + const next: FormItem[] = arr.map((data: any) => ({ id: `${Date.now()}-${Math.random()}`, initialData: data })) + setItems((prev) => [...prev, ...next]) + } + + 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 ( + +
+ + +
+ + + +
+
+
+ +
+ + x.id)}> + {items.map((item, idx) => ( + removeItem(item.id)} disabled={loading}> + 删除 + + } + > + (instancesRef.current[item.id] = f)} + /> + + ))} + + +
+ + + + +
+ + {loading && ( +
+
+ +
+
+ )} +
+ {/* 批量页右侧输入抽屉,挂载到标题栏下方容器 */} + {})} + onResult={handleInputResult} + containerEl={containerEl} + showUpload + mode={'batch-image'} + /> + + ) +} + +export default BatchRegister \ No newline at end of file diff --git a/src/components/HintText.tsx b/src/components/HintText.tsx index bf74f44..13534cd 100644 --- a/src/components/HintText.tsx +++ b/src/components/HintText.tsx @@ -5,9 +5,15 @@ type Props = { showUpload?: boolean }; const HintText: React.FC = ({ showUpload = true }) => { const text = showUpload - ? '提示:支持输入多行文本、上传图片或粘贴剪贴板图片。按 Enter 发送,Shift+Enter 换行。' - : '提示:支持输入多行文本。按 Enter 发送,Shift+Enter 换行。'; - return
{text}
; + ? ' · 支持多行输入文本、上传图片或粘贴剪贴板图片。' + : ' · 支持输入多行文本。'; + return ( +
+
Tips:
+
{text}
+
· 按 Enter 发送,Shift+Enter 换行。
+
+ ); }; export default HintText; \ No newline at end of file diff --git a/src/components/InputDrawer.tsx b/src/components/InputDrawer.tsx index 4863fd1..d478f2d 100644 --- a/src/components/InputDrawer.tsx +++ b/src/components/InputDrawer.tsx @@ -10,7 +10,7 @@ type Props = { onResult?: (data: any) => void; containerEl?: HTMLElement | null; // 抽屉挂载容器(用于放在标题栏下方) showUpload?: boolean; // 透传到输入面板,控制图片上传按钮 - mode?: 'input' | 'search'; // 透传到输入面板,控制工作模式 + mode?: 'input' | 'search' | 'batch-image'; // 透传到输入面板,控制工作模式 }; const InputDrawer: React.FC = ({ open, onClose, onResult, containerEl, showUpload = true, mode = 'input' }) => { diff --git a/src/components/InputPanel.css b/src/components/InputPanel.css index 5ff0b91..af49431 100644 --- a/src/components/InputPanel.css +++ b/src/components/InputPanel.css @@ -46,11 +46,8 @@ /* 左侧文件标签样式,保持短名及紧凑展示 */ .selected-image-tag { - max-width: 60%; - overflow: hidden; - text-overflow: ellipsis; + max-width: none; + overflow: visible; white-space: nowrap; - margin-right: auto; /* 保持标签在左侧,按钮在右侧 */ -} - -/* 移除右侧容器样式,按钮直接在 input-actions 中对齐 */ \ No newline at end of file + margin-right: 0; +} \ No newline at end of file diff --git a/src/components/InputPanel.tsx b/src/components/InputPanel.tsx index d63b4ae..9c151a9 100644 --- a/src/components/InputPanel.tsx +++ b/src/components/InputPanel.tsx @@ -9,27 +9,16 @@ const { TextArea } = Input; interface InputPanelProps { onResult?: (data: any) => void; showUpload?: boolean; // 是否显示图片上传按钮,默认显示 - mode?: 'input' | 'search'; // 输入面板工作模式,默认为表单填写(input) + mode?: 'input' | 'search' | 'batch-image'; // 输入面板工作模式,新增批量图片模式 } const InputPanel: React.FC = ({ onResult, showUpload = true, mode = 'input' }) => { const [value, setValue] = React.useState(''); const [fileList, setFileList] = React.useState([]); const [loading, setLoading] = React.useState(false); - const [savedText, setSavedText] = React.useState(''); + // 批量模式不保留文本内容 - // 统一显示短文件名:image.{ext} - const getImageExt = (file: any): string => { - const type = file?.type || ''; - if (typeof type === 'string' && type.startsWith('image/')) { - const sub = type.split('/')[1] || 'png'; - return sub.toLowerCase(); - } - const name = file?.name || ''; - const dot = name.lastIndexOf('.'); - const ext = dot >= 0 ? name.slice(dot + 1) : ''; - return (ext || 'png').toLowerCase(); - }; + // 不需要扩展名重命名,展示 image-序号 const send = async () => { const trimmed = value.trim(); @@ -66,6 +55,40 @@ const InputPanel: React.FC = ({ onResult, showUpload = true, mo return; } + // 批量图片模式:循环调用图片识别 API + if (mode === 'batch-image') { + if (!hasImage) { + message.info('请添加至少一张图片'); + return; + } + setLoading(true); + try { + const results: any[] = []; + for (let i = 0; i < fileList.length; i++) { + const f = fileList[i].originFileObj || fileList[i]; + if (!f) continue; + const resp = await postInputImage(f); + if (resp && resp.error_code === 0 && resp.data) { + results.push(resp.data); + } + } + if (results.length > 0) { + message.success(`已识别 ${results.length} 张图片`); + onResult?.(results); + } else { + message.error('识别失败,请检查图片后重试'); + } + setValue(''); + setFileList([]); + } catch (error) { + console.error('批量识别失败:', error); + message.error('网络错误,请稍后重试'); + } finally { + setLoading(false); + } + return; + } + setLoading(true); try { let response; @@ -96,7 +119,6 @@ const InputPanel: React.FC = ({ onResult, showUpload = true, mo // 清空输入 setValue(''); setFileList([]); - setSavedText(''); } else { message.error(response.error_info || '处理失败,请重试'); } @@ -129,39 +151,55 @@ const InputPanel: React.FC = ({ onResult, showUpload = true, mo const items = e.clipboardData?.items; if (!items || items.length === 0) return; - let pastedImage: File | null = null; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.kind === 'file') { - const file = item.getAsFile(); - if (file && file.type.startsWith('image/')) { - pastedImage = file; - break; // 只取第一张 + if (mode === 'batch-image') { + const newEntries: any[] = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.kind === 'file') { + const file = item.getAsFile(); + if (file && file.type.startsWith('image/')) { + const entry = { + uid: `${Date.now()}-${Math.random()}`, + name: 'image', + status: 'done', + originFileObj: file, + } as any; + newEntries.push(entry); + } } } - } - - if (pastedImage) { - // 避免图片内容以文本方式粘贴进输入框 - e.preventDefault(); - - const ext = getImageExt(pastedImage); - const name = `image.${ext}`; - - const entry = { - uid: `${Date.now()}-${Math.random()}`, - name, - status: 'done', - originFileObj: pastedImage, - } as any; - - // 仅保留一张:新图直接替换旧图 - if (fileList.length === 0) { - setSavedText(value); + if (newEntries.length > 0) { + e.preventDefault(); + setValue(''); + setFileList([...fileList, ...newEntries]); + message.success(`已添加 ${newEntries.length} 张剪贴板图片`); + } + } else { + // 单图模式:仅添加第一张并替换已有 + let firstImage: File | null = null; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.kind === 'file') { + const file = item.getAsFile(); + if (file && file.type.startsWith('image/')) { + firstImage = file; + break; + } + } + } + if (firstImage) { + e.preventDefault(); + setValue(''); + setFileList([ + { + uid: `${Date.now()}-${Math.random()}`, + name: 'image', + status: 'done', + originFileObj: firstImage, + } as any, + ]); + message.success('已添加剪贴板图片'); } - setValue(''); - setFileList([entry]); - message.success('已添加剪贴板图片'); } }; @@ -178,60 +216,95 @@ const InputPanel: React.FC = ({ onResult, showUpload = true, mo })()}