From 5d56a4bbe9f091f54b1f104a80fb0f58aa088a48 Mon Sep 17 00:00:00 2001 From: mamamiyear Date: Thu, 13 Nov 2025 21:13:51 +0800 Subject: [PATCH 1/4] feat: dispaly creation time of people in detail --- src/apis/types.ts | 1 + src/components/ResourceList.tsx | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) 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/ResourceList.tsx b/src/components/ResourceList.tsx index b424309..3d7a094 100644 --- a/src/components/ResourceList.tsx +++ b/src/components/ResourceList.tsx @@ -32,9 +32,20 @@ function transformPeoples(list: People[] = []): Resource[] { introduction: person.introduction || {}, contact: person.contact || '', cover: person.cover || '', + created_at: person.created_at, })); } +// 格式化日期 +function formatDate(timestamp: number | null | undefined): string { + if (!timestamp) return ''; + const date = new Date(timestamp * 1000); + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + return `${year}-${month}-${day}`; +} + // 获取人员列表数据 async function fetchResources(): Promise { try { @@ -52,7 +63,12 @@ async function fetchResources(): Promise { } // 转换数据格式以匹配组件期望的结构 - return transformPeoples(response.data || []); + const transformed = transformPeoples(response.data || []); + + // 按 created_at 排序 + transformed.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); + + return transformed; } catch (error: any) { console.error('获取人员列表失败:', error); @@ -1031,6 +1047,11 @@ const ResourceList: React.FC = ({ inputOpen = false, onCloseInput, contai ) : (
暂无介绍
)} + {record.created_at && ( +
+ {record.created_at ? '录入于: ' + formatDate(record.created_at) : ''} +
+ )} ); }, From 8873c183e77f8133498d5e28501490e3a749c9d8 Mon Sep 17 00:00:00 2001 From: mamamiyear Date: Fri, 14 Nov 2025 16:17:37 +0800 Subject: [PATCH 2/4] feat: make remarks on people --- src/apis/config.ts | 1 + src/apis/people.ts | 19 +++++++ src/components/ResourceList.tsx | 87 ++++++++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 1 deletion(-) 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/components/ResourceList.tsx b/src/components/ResourceList.tsx index 3d7a094..34a906d 100644 --- a/src/components/ResourceList.tsx +++ b/src/components/ResourceList.tsx @@ -11,7 +11,7 @@ import ImageModal from './ImageModal.tsx'; import PeopleForm from './PeopleForm.tsx'; import { getPeoples } from '../apis'; import type { People } from '../apis'; -import { deletePeople, updatePeople } from '../apis/people'; +import { addOrUpdateRemark, deletePeople, deleteRemark, updatePeople } from '../apis/people'; const { Content } = Layout; @@ -30,6 +30,7 @@ function transformPeoples(list: People[] = []): Resource[] { height: person.height, marital_status: person.marital_status, introduction: person.introduction || {}, + comments: person.comments || {}, contact: person.contact || '', cover: person.cover || '', created_at: person.created_at, @@ -606,6 +607,10 @@ const ResourceList: React.FC = ({ inputOpen = false, onCloseInput, contai const [editingRecord, setEditingRecord] = React.useState(null); const editFormRef = React.useRef(null); + // 备注编辑模态框状态 + const [remarkModalVisible, setRemarkModalVisible] = React.useState(false); + const [editingRemark, setEditingRemark] = React.useState<{ recordId: string; content: string } | null>(null); + // 移动端编辑模式状态 const [mobileEditing, setMobileEditing] = React.useState(false); @@ -1052,6 +1057,48 @@ const ResourceList: React.FC = ({ inputOpen = false, onCloseInput, contai {record.created_at ? '录入于: ' + formatDate(record.created_at) : ''} )} + +
+ +
+ + 备注 + {record.comments && record.comments.remark ? ( + + + + + ) : ( + + )} + + {record.comments && record.comments.remark ? ( +
+
+ 更新于: {formatDate(record.comments.remark.updated_at)} +
+
{record.comments.remark.content}
+
+ ) : null} +
); }, @@ -1146,6 +1193,44 @@ const ResourceList: React.FC = ({ inputOpen = false, onCloseInput, contai /> )} + + { + setRemarkModalVisible(false); + setEditingRemark(null); + }} + onOk={async () => { + if (!editingRemark) return; + try { + const res = await addOrUpdateRemark(editingRemark.recordId, editingRemark.content); + if (res.error_code === 0) { + message.success(editingRemark.content ? '修改成功' : '添加成功'); + setRemarkModalVisible(false); + setEditingRemark(null); + await reloadResources(); + } else { + message.error(res.error_info || '操作失败'); + } + } catch (err) { + message.error('操作失败'); + } + }} + okText="确认" + cancelText="取消" + > + { + if (editingRemark) { + setEditingRemark({ ...editingRemark, content: e.target.value }); + } + }} + placeholder="请输入备注" + /> + ); }; From 63a813925fc764817c00cb56af04d5a78b9d3698 Mon Sep 17 00:00:00 2001 From: mamamiyear Date: Sat, 15 Nov 2025 16:36:29 +0800 Subject: [PATCH 3/4] feat: support batch register resources --- src/components/BatchRegister.tsx | 143 +++++++++++++++++++++++++++++++ src/components/LayoutWrapper.tsx | 10 +++ src/components/SiderMenu.tsx | 7 +- 3 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 src/components/BatchRegister.tsx diff --git a/src/components/BatchRegister.tsx b/src/components/BatchRegister.tsx new file mode 100644 index 0000000..a518f0e --- /dev/null +++ b/src/components/BatchRegister.tsx @@ -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([{ id: `${Date.now()}-${Math.random()}` }]) + 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 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 && ( +
+
+ +
+
+ )} +
+ + ) +} + +export default BatchRegister \ No newline at end of file diff --git a/src/components/LayoutWrapper.tsx b/src/components/LayoutWrapper.tsx index fb35b51..90d2fe1 100644 --- a/src/components/LayoutWrapper.tsx +++ b/src/components/LayoutWrapper.tsx @@ -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 = () => { /> } /> + } + /> = ({ onNavigate, selectedKey, mobileOpen, onMob }, [selectedKey]); const items = [ - { key: 'home', label: '注册', icon: }, - { key: 'menu1', label: '列表', icon: }, + { key: 'home', label: '录入资源', icon: }, + { key: 'batch', label: '批量录入', icon: }, + { key: 'menu1', label: '资源列表', icon: }, ]; // 移动端:使用 Drawer 覆盖主内容 From 83f8091e1567f105873e2ae1a7068f4ece02d75e Mon Sep 17 00:00:00 2001 From: mamamiyear Date: Sat, 15 Nov 2025 16:44:27 +0800 Subject: [PATCH 4/4] feat: support batch recognize images for batch register --- src/components/BatchRegister.tsx | 27 +++- src/components/HintText.tsx | 12 +- src/components/InputDrawer.tsx | 2 +- src/components/InputPanel.css | 11 +- src/components/InputPanel.tsx | 229 ++++++++++++++++++++----------- src/components/LayoutWrapper.tsx | 13 +- src/components/MainContent.tsx | 2 +- 7 files changed, 199 insertions(+), 97 deletions(-) diff --git a/src/components/BatchRegister.tsx b/src/components/BatchRegister.tsx index a518f0e..785aa95 100644 --- a/src/components/BatchRegister.tsx +++ b/src/components/BatchRegister.tsx @@ -2,16 +2,19 @@ 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 } +type FormItem = { id: string; initialData?: any } -const BatchRegister: React.FC = () => { +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([{ id: `${Date.now()}-${Math.random()}` }]) + const [items, setItems] = React.useState([]) const instancesRef = React.useRef>({}) const [loading, setLoading] = React.useState(false) @@ -40,6 +43,12 @@ const BatchRegister: React.FC = () => { } } + 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 { @@ -89,7 +98,7 @@ const BatchRegister: React.FC = () => {
- +
@@ -113,6 +122,7 @@ const BatchRegister: React.FC = () => { > (instancesRef.current[item.id] = f)} /> @@ -136,6 +146,15 @@ const BatchRegister: React.FC = () => {
)}
+ {/* 批量页右侧输入抽屉,挂载到标题栏下方容器 */} + {})} + onResult={handleInputResult} + containerEl={containerEl} + showUpload + mode={'batch-image'} + />
) } 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 })()}