1 Commits

Author SHA1 Message Date
2c7dbada25 Release v0.1 2025-11-13 00:06:48 +08:00
12 changed files with 97 additions and 480 deletions

View File

@@ -17,5 +17,4 @@ export const API_ENDPOINTS = {
// 新增单个资源路径 /people // 新增单个资源路径 /people
PEOPLE: '/people', PEOPLE: '/people',
PEOPLE_BY_ID: (id: string) => `/people/${id}`, PEOPLE_BY_ID: (id: string) => `/people/${id}`,
PEOPLE_REMARK_BY_ID: (id: string) => `/people/${id}/remark`,
} as const; } as const;

View File

@@ -121,25 +121,6 @@ export async function updatePeople(peopleId: string, people: People): Promise<Ap
return put<ApiResponse>(API_ENDPOINTS.PEOPLE_BY_ID(peopleId), requestData); return put<ApiResponse>(API_ENDPOINTS.PEOPLE_BY_ID(peopleId), requestData);
} }
/**
* 添加或更新人员备注
* @param peopleId 人员ID
* @param content 备注内容
* @returns Promise<ApiResponse>
*/
export async function addOrUpdateRemark(peopleId: string, content: string): Promise<ApiResponse> {
return post<ApiResponse>(API_ENDPOINTS.PEOPLE_REMARK_BY_ID(peopleId), { content });
}
/**
* 删除人员备注
* @param peopleId 人员ID
* @returns Promise<ApiResponse>
*/
export async function deleteRemark(peopleId: string): Promise<ApiResponse> {
return del<ApiResponse>(API_ENDPOINTS.PEOPLE_REMARK_BY_ID(peopleId));
}
/** /**
* 批量创建人员信息 * 批量创建人员信息
* @param peopleList 人员信息数组 * @param peopleList 人员信息数组

View File

@@ -50,7 +50,6 @@ export interface People {
age?: number; age?: number;
height?: number; height?: number;
marital_status?: string; marital_status?: string;
created_at?: number;
[key: string]: any; [key: string]: any;
cover?: string; cover?: string;
} }

View File

@@ -1,162 +0,0 @@
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<Props> = ({ inputOpen = false, onCloseInput, containerEl }) => {
const [commonForm] = Form.useForm()
const [items, setItems] = React.useState<FormItem[]>([])
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 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 (
<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
initialData={item.initialData}
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>
{/* 批量页右侧输入抽屉,挂载到标题栏下方容器 */}
<InputDrawer
open={inputOpen || false}
onClose={onCloseInput || (() => {})}
onResult={handleInputResult}
containerEl={containerEl}
showUpload
mode={'batch-image'}
/>
</Content>
)
}
export default BatchRegister

View File

@@ -5,15 +5,9 @@ type Props = { showUpload?: boolean };
const HintText: React.FC<Props> = ({ showUpload = true }) => { const HintText: React.FC<Props> = ({ showUpload = true }) => {
const text = showUpload const text = showUpload
? ' · 支持多行输入文本、上传图片或粘贴剪贴板图片。' ? '提示:支持输入多行文本、上传图片或粘贴剪贴板图片。按 Enter 发送Shift+Enter 换行。'
: ' · 支持输入多行文本。'; : '提示:支持输入多行文本。按 Enter 发送Shift+Enter 换行。';
return ( return <div className="hint-text">{text}</div>;
<div>
<div className="hint-text">Tips:</div>
<div className="hint-text">{text}</div>
<div className="hint-text"> · Enter Shift+Enter </div>
</div>
);
}; };
export default HintText; export default HintText;

View File

@@ -10,7 +10,7 @@ type Props = {
onResult?: (data: any) => void; onResult?: (data: any) => void;
containerEl?: HTMLElement | null; // 抽屉挂载容器(用于放在标题栏下方) containerEl?: HTMLElement | null; // 抽屉挂载容器(用于放在标题栏下方)
showUpload?: boolean; // 透传到输入面板,控制图片上传按钮 showUpload?: boolean; // 透传到输入面板,控制图片上传按钮
mode?: 'input' | 'search' | 'batch-image'; // 透传到输入面板,控制工作模式 mode?: 'input' | 'search'; // 透传到输入面板,控制工作模式
}; };
const InputDrawer: React.FC<Props> = ({ open, onClose, onResult, containerEl, showUpload = true, mode = 'input' }) => { const InputDrawer: React.FC<Props> = ({ open, onClose, onResult, containerEl, showUpload = true, mode = 'input' }) => {

View File

@@ -46,8 +46,11 @@
/* 左侧文件标签样式,保持短名及紧凑展示 */ /* 左侧文件标签样式,保持短名及紧凑展示 */
.selected-image-tag { .selected-image-tag {
max-width: none; max-width: 60%;
overflow: visible; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
margin-right: 0; margin-right: auto; /* 保持标签在左侧,按钮在右侧 */
} }
/* 移除右侧容器样式,按钮直接在 input-actions 中对齐 */

View File

@@ -9,16 +9,27 @@ const { TextArea } = Input;
interface InputPanelProps { interface InputPanelProps {
onResult?: (data: any) => void; onResult?: (data: any) => void;
showUpload?: boolean; // 是否显示图片上传按钮,默认显示 showUpload?: boolean; // 是否显示图片上传按钮,默认显示
mode?: 'input' | 'search' | 'batch-image'; // 输入面板工作模式,新增批量图片模式 mode?: 'input' | 'search'; // 输入面板工作模式,默认为表单填写input
} }
const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mode = 'input' }) => { const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mode = 'input' }) => {
const [value, setValue] = React.useState(''); const [value, setValue] = React.useState('');
const [fileList, setFileList] = React.useState<any[]>([]); const [fileList, setFileList] = React.useState<any[]>([]);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
// 批量模式不保留文本内容 const [savedText, setSavedText] = React.useState<string>('');
// 不需要扩展名重命名,展示 image-序号 // 统一显示短文件名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();
};
const send = async () => { const send = async () => {
const trimmed = value.trim(); const trimmed = value.trim();
@@ -55,40 +66,6 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
return; 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); setLoading(true);
try { try {
let response; let response;
@@ -119,6 +96,7 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
// 清空输入 // 清空输入
setValue(''); setValue('');
setFileList([]); setFileList([]);
setSavedText('');
} else { } else {
message.error(response.error_info || '处理失败,请重试'); message.error(response.error_info || '处理失败,请重试');
} }
@@ -151,56 +129,40 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
const items = e.clipboardData?.items; const items = e.clipboardData?.items;
if (!items || items.length === 0) return; if (!items || items.length === 0) return;
if (mode === 'batch-image') { let pastedImage: File | null = null;
const newEntries: any[] = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
if (item.kind === 'file') { if (item.kind === 'file') {
const file = item.getAsFile(); const file = item.getAsFile();
if (file && file.type.startsWith('image/')) { if (file && file.type.startsWith('image/')) {
pastedImage = file;
break; // 只取第一张
}
}
}
if (pastedImage) {
// 避免图片内容以文本方式粘贴进输入框
e.preventDefault();
const ext = getImageExt(pastedImage);
const name = `image.${ext}`;
const entry = { const entry = {
uid: `${Date.now()}-${Math.random()}`, uid: `${Date.now()}-${Math.random()}`,
name: 'image', name,
status: 'done', status: 'done',
originFileObj: file, originFileObj: pastedImage,
} as any; } as any;
newEntries.push(entry);
// 仅保留一张:新图直接替换旧图
if (fileList.length === 0) {
setSavedText(value);
} }
}
}
if (newEntries.length > 0) {
e.preventDefault();
setValue(''); setValue('');
setFileList([...fileList, ...newEntries]); setFileList([entry]);
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('已添加剪贴板图片'); message.success('已添加剪贴板图片');
} }
}
}; };
return ( return (
@@ -216,17 +178,9 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
})()} })()}
<TextArea <TextArea
value={value} value={value}
onChange={(e) => { onChange={(e) => setValue(e.target.value)}
if (mode === 'batch-image') {
setValue('');
return;
}
setValue(e.target.value);
}}
placeholder={ placeholder={
mode === 'batch-image' showUpload && fileList.length > 0
? '批量识别不支持输入文本,可添加或粘贴多张图片...'
: showUpload && fileList.length > 0
? '不可在添加图片时输入信息...' ? '不可在添加图片时输入信息...'
: (showUpload ? '请输入个人信息描述,或上传图片…' : '请输入个人信息描述…') : (showUpload ? '请输入个人信息描述,或上传图片…' : '请输入个人信息描述…')
} }
@@ -234,77 +188,50 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
style={{ fontSize: 16 }} style={{ fontSize: 16 }}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onPaste={onPaste} onPaste={onPaste}
disabled={loading || (mode !== 'batch-image' && showUpload && fileList.length > 0)} disabled={loading || (showUpload && fileList.length > 0)}
/> />
</Spin> </Spin>
<div className="input-actions"> <div className="input-actions">
{/* 左侧文件标签显示 */} {/* 左侧文件标签显示 */}
{showUpload && fileList.length > 0 && ( {showUpload && fileList.length > 0 && (
mode === 'batch-image' ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{fileList.map((f, idx) => (
<Tag
key={f.uid || idx}
className="selected-image-tag"
color="processing"
closable
onClose={() => {
const next = fileList.filter((x) => x !== f)
setFileList(next)
}}
bordered={false}
>
{`image-${idx + 1}`}
</Tag>
))}
</div>
) : (
<Tag <Tag
className="selected-image-tag" className="selected-image-tag"
color="processing" color="processing"
closable closable
onClose={() => { setFileList([]) }} onClose={() => { setFileList([]); setValue(savedText); setSavedText(''); }}
bordered={false} bordered={false}
> >
{'image'} {`image.${new Date().getSeconds()}.${getImageExt(fileList[0]?.originFileObj || fileList[0])}`}
</Tag> </Tag>
)
)} )}
{showUpload && ( {showUpload && (
<Upload <Upload
accept="image/*" accept="image/*"
multiple={mode === 'batch-image'} multiple={false}
beforeUpload={() => false} beforeUpload={() => false}
fileList={fileList} fileList={fileList}
onChange={({ fileList: nextFileList }) => { onChange={({ file, fileList: nextFileList }) => {
if (mode === 'batch-image') { // 只保留最新一个,并重命名为 image.{ext}
const normalized = nextFileList.map((entry: any) => {
const raw = entry.originFileObj || entry;
return { ...entry, name: 'image', originFileObj: raw };
});
setValue('');
setFileList(normalized);
} else {
if (nextFileList.length === 0) { if (nextFileList.length === 0) {
setFileList([]); setFileList([]);
return; return;
} }
// 仅添加第一张 const latest = nextFileList[nextFileList.length - 1] as any;
const first = nextFileList[0] as any; const raw = latest.originFileObj || file; // UploadFile 或原始 File
const raw = first.originFileObj || first; const ext = getImageExt(raw);
const renamed = { ...first, name: 'image', originFileObj: raw }; const renamed = { ...latest, name: `image.${ext}` };
if (fileList.length === 0) {
setSavedText(value);
}
setValue(''); setValue('');
setFileList([renamed]); setFileList([renamed]);
}
}}
onRemove={(file) => {
setFileList((prev) => prev.filter((x) => x.uid !== (file as any).uid));
return true;
}} }}
onRemove={() => { setFileList([]); setValue(savedText); setSavedText(''); return true; }}
maxCount={1}
showUploadList={false} showUploadList={false}
disabled={loading || (mode !== 'batch-image' && fileList.length >= 1)} disabled={loading}
> >
<Button type="text" icon={<PictureOutlined />} disabled={loading || (mode !== 'batch-image' && fileList.length >= 1)} /> <Button type="text" icon={<PictureOutlined />} disabled={loading} />
</Upload> </Upload>
)} )}
<Button <Button

View File

@@ -4,7 +4,6 @@ import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import SiderMenu from './SiderMenu.tsx'; import SiderMenu from './SiderMenu.tsx';
import MainContent from './MainContent.tsx'; import MainContent from './MainContent.tsx';
import ResourceList from './ResourceList.tsx'; import ResourceList from './ResourceList.tsx';
import BatchRegister from './BatchRegister.tsx';
import TopBar from './TopBar.tsx'; import TopBar from './TopBar.tsx';
import '../styles/base.css'; import '../styles/base.css';
import '../styles/layout.css'; import '../styles/layout.css';
@@ -16,15 +15,12 @@ const LayoutWrapper: React.FC = () => {
const [inputOpen, setInputOpen] = React.useState(false); const [inputOpen, setInputOpen] = React.useState(false);
const isHome = location.pathname === '/'; const isHome = location.pathname === '/';
const isList = location.pathname === '/resources'; const isList = location.pathname === '/resources';
const isBatch = location.pathname === '/batch-register';
const layoutShellRef = React.useRef<HTMLDivElement>(null); const layoutShellRef = React.useRef<HTMLDivElement>(null);
const pathToKey = (path: string) => { const pathToKey = (path: string) => {
switch (path) { switch (path) {
case '/resources': case '/resources':
return 'menu1'; return 'menu1';
case '/batch-register':
return 'batch';
case '/menu2': case '/menu2':
return 'menu2'; return 'menu2';
default: default:
@@ -39,9 +35,6 @@ const LayoutWrapper: React.FC = () => {
case 'home': case 'home':
navigate('/'); navigate('/');
break; break;
case 'batch':
navigate('/batch-register');
break;
case 'menu1': case 'menu1':
navigate('/resources'); navigate('/resources');
break; break;
@@ -61,8 +54,8 @@ const LayoutWrapper: React.FC = () => {
{/* 顶部标题栏,位于左侧菜单栏之上 */} {/* 顶部标题栏,位于左侧菜单栏之上 */}
<TopBar <TopBar
onToggleMenu={() => {setInputOpen(false); setMobileMenuOpen((v) => !v);}} onToggleMenu={() => {setInputOpen(false); setMobileMenuOpen((v) => !v);}}
onToggleInput={() => {if (isHome || isList || isBatch) {setMobileMenuOpen(false); setInputOpen((v) => !v);}}} onToggleInput={() => {if (isHome || isList) {setMobileMenuOpen(false); setInputOpen((v) => !v);}}}
showInput={isHome || isList || isBatch} showInput={isHome || isList}
/> />
{/* 下方为主布局:左侧菜单 + 右侧内容 */} {/* 下方为主布局:左侧菜单 + 右侧内容 */}
<Layout ref={layoutShellRef as any} className="layout-shell"> <Layout ref={layoutShellRef as any} className="layout-shell">
@@ -84,16 +77,6 @@ const LayoutWrapper: React.FC = () => {
/> />
} }
/> />
<Route
path="/batch-register"
element={
<BatchRegister
inputOpen={inputOpen}
onCloseInput={() => setInputOpen(false)}
containerEl={layoutShellRef.current}
/>
}
/>
<Route <Route
path="/resources" path="/resources"
element={ element={

View File

@@ -21,7 +21,7 @@ const MainContent: React.FC<Props> = ({ inputOpen = false, onCloseInput, contain
? ?
</Typography.Title> </Typography.Title>
<Typography.Paragraph style={{ color: 'var(--muted)', marginBottom: 0 }}> <Typography.Paragraph style={{ color: 'var(--muted)', marginBottom: 0 }}>
TA的信息
</Typography.Paragraph> </Typography.Paragraph>
<PeopleForm initialData={formData} /> <PeopleForm initialData={formData} />

View File

@@ -11,7 +11,7 @@ import ImageModal from './ImageModal.tsx';
import PeopleForm from './PeopleForm.tsx'; import PeopleForm from './PeopleForm.tsx';
import { getPeoples } from '../apis'; import { getPeoples } from '../apis';
import type { People } from '../apis'; import type { People } from '../apis';
import { addOrUpdateRemark, deletePeople, deleteRemark, updatePeople } from '../apis/people'; import { deletePeople, updatePeople } from '../apis/people';
const { Content } = Layout; const { Content } = Layout;
@@ -30,23 +30,11 @@ function transformPeoples(list: People[] = []): Resource[] {
height: person.height, height: person.height,
marital_status: person.marital_status, marital_status: person.marital_status,
introduction: person.introduction || {}, introduction: person.introduction || {},
comments: person.comments || {},
contact: person.contact || '', contact: person.contact || '',
cover: person.cover || '', 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<Resource[]> { async function fetchResources(): Promise<Resource[]> {
try { try {
@@ -64,12 +52,7 @@ async function fetchResources(): Promise<Resource[]> {
} }
// 转换数据格式以匹配组件期望的结构 // 转换数据格式以匹配组件期望的结构
const transformed = transformPeoples(response.data || []); return transformPeoples(response.data || []);
// 按 created_at 排序
transformed.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
return transformed;
} catch (error: any) { } catch (error: any) {
console.error('获取人员列表失败:', error); console.error('获取人员列表失败:', error);
@@ -607,10 +590,6 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
const [editingRecord, setEditingRecord] = React.useState<Resource | null>(null); const [editingRecord, setEditingRecord] = React.useState<Resource | null>(null);
const editFormRef = React.useRef<FormInstance | null>(null); const editFormRef = React.useRef<FormInstance | null>(null);
// 备注编辑模态框状态
const [remarkModalVisible, setRemarkModalVisible] = React.useState(false);
const [editingRemark, setEditingRemark] = React.useState<{ recordId: string; content: string } | null>(null);
// 移动端编辑模式状态 // 移动端编辑模式状态
const [mobileEditing, setMobileEditing] = React.useState(false); const [mobileEditing, setMobileEditing] = React.useState(false);
@@ -1052,53 +1031,6 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
) : ( ) : (
<div style={{ color: '#9ca3af' }}></div> <div style={{ color: '#9ca3af' }}></div>
)} )}
{record.created_at && (
<div style={{ fontSize: '12px', color: '#999', marginTop: '12px' }}>
{record.created_at ? '录入于: ' + formatDate(record.created_at) : ''}
</div>
)}
<div style={{ borderTop: '1px solid #f0f0f0', margin: '12px 0' }} />
<div>
<Typography.Title level={5} style={{ color: 'var(--text-primary)', display: 'flex', alignItems: 'center', gap: 8 }}>
<span></span>
{record.comments && record.comments.remark ? (
<Space>
<Button size="small" onClick={() => {
setEditingRemark({ recordId: record.id, content: record.comments.remark.content });
setRemarkModalVisible(true);
}}></Button>
<Button size="small" danger onClick={async () => {
try {
const res = await deleteRemark(record.id);
if (res.error_code === 0) {
message.success('清空成功');
await reloadResources();
} else {
message.error(res.error_info || '清空失败');
}
} catch (err) {
message.error('清空失败');
}
}}></Button>
</Space>
) : (
<Button size="small" onClick={() => {
setEditingRemark({ recordId: record.id, content: '' });
setRemarkModalVisible(true);
}}></Button>
)}
</Typography.Title>
{record.comments && record.comments.remark ? (
<div>
<div style={{ fontSize: '12px', color: '#999', marginBottom: '8px' }}>
: {formatDate(record.comments.remark.updated_at)}
</div>
<div>{record.comments.remark.content}</div>
</div>
) : null}
</div>
</div> </div>
); );
}, },
@@ -1193,44 +1125,6 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
/> />
</Modal> </Modal>
)} )}
<Modal
open={remarkModalVisible}
title={editingRemark?.content ? "修改备注" : "添加备注"}
onCancel={() => {
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="取消"
>
<Input.TextArea
rows={4}
value={editingRemark?.content}
onChange={(e) => {
if (editingRemark) {
setEditingRemark({ ...editingRemark, content: e.target.value });
}
}}
placeholder="请输入备注"
/>
</Modal>
</Content> </Content>
); );
}; };

View File

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