feat: 样本管理

This commit is contained in:
2025-10-13 16:50:21 +08:00
parent 4e9ebc76f7
commit 9eb4f52f0e
25 changed files with 3066 additions and 344 deletions

View File

@@ -0,0 +1,83 @@
// 使用示例 - App.tsx
import { Modal, message, Space } from 'antd';
import React, { useEffect, useState } from 'react';
import RenameRule, {
type FileItem,
type RenameResult,
} from '@/components/RenameRule';
import { updateSamples } from '@/services/ai/sample';
const TagManager: React.FC<{
visible: boolean;
files: FileItem[];
onCancel?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onOk?: () => void;
}> = ({ visible, files: beforeFiles, onCancel, onOk }) => {
const [files, setFiles] = useState<FileItem[]>(beforeFiles);
useEffect(() => {
setFiles(beforeFiles);
}, [beforeFiles]);
const [previewResults, setPreviewResults] = useState<RenameResult[]>([]);
const [loading, setLoading] = useState(false);
// 预览回调o+
const handlePreview = (results: RenameResult[]) => {
setPreviewResults(results);
};
const handleOk = async () => {
try {
setLoading(true);
const results = previewResults.map((result) => ({
id: result.id,
sampleName: `${result.newName}`,
}));
await updateSamples(results);
onOk?.();
message.success('重命名成功');
} finally {
setLoading(false);
}
};
// // 重命名回调
// const handleRename = async (results: RenameResult[]) => {
// setLoading(true);
// try {
// console.log("执行预览:", results);
// // 模拟API调用
// await new Promise((resolve) => setTimeout(resolve, 1000));
// message.success(`成功重命名 ${results.length} 个文件`);
// } catch (error) {
// message.error("重命名失败");
// } finally {
// setLoading(false);
// }
// };
return (
<Modal
width={800}
onCancel={onCancel}
onOk={handleOk}
open={visible}
title="重命名文件"
>
<div style={{ padding: '24px' }}>
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* 独立的重命名规则组件 */}
<RenameRule
files={files}
onPreview={handlePreview}
// onRename={handleRename}
loading={loading}
/>
</Space>
</div>
</Modal>
);
};
export default TagManager;

View File

@@ -1,14 +1,21 @@
import type {
ProColumns,
ProFormColumnsType,
} from '@ant-design/pro-components';
import dayjs from 'dayjs';
import TagEditor from '@/components/TagEditor';
import TinyMCEEditor from '@/components/Tinymce';
export const baseTenantColumns: ProColumns<API.CategoryDO>[] = [
import type { ProColumns } from '@ant-design/pro-components';
import GroupTagSelect from '@/components/GroupTag/GroupTagSelect';
import {
type AiSampleRespVO,
createSampleTag,
createSampleTagGroup,
deleteSampleTag,
deleteSampleTagGroup,
getSampleTagGroup,
getSampleTagPage,
updateSampleTag,
updateSampleTagGroup,
} from '@/services/ai/sample';
export const baseTenantColumns: ProColumns<AiSampleRespVO>[] = [
{
title: '样本名称',
dataIndex: 'sample_name',
dataIndex: 'sampleName',
// width: 500,
},
{
title: '注释',
@@ -19,6 +26,36 @@ export const baseTenantColumns: ProColumns<API.CategoryDO>[] = [
title: '标签',
hideInTable: true,
dataIndex: 'tag_name',
valueType: 'select',
search: {
transform: (value) => {
console.log(value);
return value.join(',');
},
},
renderFormItem: (_) => {
return (
<GroupTagSelect
request={{
groupsApi: {
get: getSampleTagGroup,
create: createSampleTagGroup,
delete: deleteSampleTagGroup,
update: updateSampleTagGroup,
},
tagsApi: {
get: getSampleTagPage,
create: createSampleTag,
delete: deleteSampleTag,
update: updateSampleTag,
},
}}
editable
placeholder="请选择技术栈"
/>
);
},
},
{
title: '样本格式',
@@ -27,107 +64,107 @@ export const baseTenantColumns: ProColumns<API.CategoryDO>[] = [
},
];
export const formColumns = (data: {
type: string;
grade: number;
}): ProFormColumnsType[] => [
{
title: '类目',
dataIndex: 'grade',
valueType: 'radio',
fieldProps: {
value: data.grade || 1,
options: [
{ label: '一级类目', value: 1 },
{ label: '二级类目', value: 2 },
{ label: '三级类目', value: 3 },
],
disabled: data.type === 'create',
},
},
{
title: '类目名称',
dataIndex: 'username',
formItemProps: {
rules: [
{
required: true,
message: '请输入用户名',
},
],
},
},
{
title: '排序权重',
dataIndex: 'sort',
valueType: 'digit',
},
{
title: '类目描述',
dataIndex: 'description',
valueType: 'textarea',
renderFormItem: () => {
return <TinyMCEEditor />;
},
},
{
title: '关联父级',
dataIndex: 'parentId',
valueType: 'select',
hideInForm: data.grade - 1 === 0,
},
{
title: '类目icon',
dataIndex: 'icon',
},
{
title: '类目标签',
dataIndex: 'tages',
renderFormItem: () => {
return (
<TagEditor
placeholder="输入标签名称"
maxCount={10}
tagProps={{
color: 'blue',
}}
/>
);
},
},
{
title: '类目状态',
dataIndex: 'status',
hideInForm: data.type === 'create',
},
{
title: '类目ID',
dataIndex: 'categoryId',
hideInForm: data.type === 'create',
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateTime',
hideInForm: data.type === 'create',
},
{
title: '创建人',
dataIndex: 'creator',
hideInForm: data.type === 'create',
},
{
title: '更新时间',
dataIndex: 'updateTime',
valueType: 'dateTime',
hideInForm: data.type === 'create',
},
{
title: '更新人',
dataIndex: 'updator',
hideInForm: data.type === 'create',
},
];
// export const formColumns = (data: {
// type: string;
// grade: number;
// }): ProFormColumnsType[] => [
// {
// title: "类目",
// dataIndex: "grade",
// valueType: "radio",
// fieldProps: {
// value: data.grade || 1,
// options: [
// { label: "一级类目", value: 1 },
// { label: "二级类目", value: 2 },
// { label: "三级类目", value: 3 },
// ],
// disabled: data.type === "create",
// },
// },
// {
// title: "类目名称",
// dataIndex: "username",
// formItemProps: {
// rules: [
// {
// required: true,
// message: "请输入用户名",
// },
// ],
// },
// },
// {
// title: "排序权重",
// dataIndex: "sort",
// valueType: "digit",
// },
// {
// title: "类目描述",
// dataIndex: "description",
// valueType: "textarea",
// renderFormItem: () => {
// return <TinyMCEEditor />;
// },
// },
// {
// title: "关联父级",
// dataIndex: "parentId",
// valueType: "select",
// hideInForm: data.grade - 1 === 0,
// },
// {
// title: "类目icon",
// dataIndex: "icon",
// },
// {
// title: "类目标签",
// dataIndex: "tages",
// renderFormItem: () => {
// return (
// <TagEditor
// placeholder="输入标签名称"
// maxCount={10}
// tagProps={{
// color: "blue",
// }}
// />
// );
// },
// },
// {
// title: "类目状态",
// dataIndex: "status",
// hideInForm: data.type === "create",
// },
// {
// title: "类目ID",
// dataIndex: "categoryId",
// hideInForm: data.type === "create",
// },
// {
// title: "创建时间",
// dataIndex: "createTime",
// valueType: "dateTime",
// hideInForm: data.type === "create",
// },
// {
// title: "创建人",
// dataIndex: "creator",
// hideInForm: data.type === "create",
// },
// {
// title: "更新时间",
// dataIndex: "updateTime",
// valueType: "dateTime",
// hideInForm: data.type === "create",
// },
// {
// title: "更新人",
// dataIndex: "updator",
// hideInForm: data.type === "create",
// },
// ];
// {
// title: "模板内容",

View File

@@ -1,81 +1,332 @@
import {
ProForm,
ProFormCascader,
ProFormCheckbox,
ProFormColorPicker,
ProFormDigit,
ProFormDigitRange,
ProFormGroup,
ProFormRadio,
ProFormSelect,
ProFormSlider,
ProFormSwitch,
ProFormText,
} from '@ant-design/pro-components';
import { Switch } from 'antd';
import { ProForm, ProFormGroup, ProFormText } from '@ant-design/pro-components';
import { Button, message, Space, Tag } from 'antd';
import type { RowSelectionType } from 'antd/es/table/interface';
import type { FormInstance } from 'antd/lib';
import Mock from 'mockjs';
import { useRef, useState } from 'react';
import TagEditor from '@/components/TagEditor';
import dayjs from 'dayjs';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import GroupTagModal from '@/components/GroupTag/GroupTagModal';
import type { TagItem } from '@/components/GroupTag/types';
import type { FileItem } from '@/components/RenameRule';
import {
createSampleTag,
createSampleTagGroup,
deleteSample,
deleteSampleTag,
deleteSampleTagGroup,
deleteSampleTagRelate,
getSampleTagGroup,
getSampleTagPage,
relateSample,
updateSamples,
updateSampleTag,
updateSampleTagGroup,
} from '@/services/ai/sample';
import TagManager from './components/tag-manager';
export const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
interface SampleTagDetailProps<T> {
onRefresh?: (type?: string) => void;
type: RowSelectionType;
data?: T[];
}
const SampleTagDetail = () => {
const [readonly, setReadonly] = useState(false);
const SampleTagDetail = <T extends Record<string, any>>(
props: SampleTagDetailProps<T>,
) => {
const formRef = useRef<FormInstance>(null);
const { type = 'radio', data } = props;
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [tagManagerVisible, setTagManagerVisible] = useState<boolean>(false);
const [value, setValue] = useState<{
tagId?: number | number[];
id?: number;
remark?: string;
sampleIds?: number[];
tags?: TagItem[];
updateTime?: number;
createTime?: number;
sampleSize?: number;
sampleMineType?: string;
sampleTime?: number;
}>({});
const handleAddTag = useCallback(() => {
setModalVisible(true);
}, [modalVisible]);
// const radioData = data && data[0];
// // 当 radioData 改变时更新表单值
useEffect(() => {
if (type === 'radio') {
const item = data?.[0];
setValue({ ...item, sampleIds: [item?.id] });
} else {
const sampleIds = data?.map((sample) => sample.id) || [];
const tags = data?.map((sample) => {
return sample.tags;
});
const map = new Map();
const tags_total = tags
?.flat()
.filter((v) => !map.has(v.id) && map.set(v.id, v));
setValue({ tags: tags_total as TagItem[], sampleIds });
}
}, [type, data]);
useEffect(() => {
formRef.current?.setFieldsValue(value);
}, [value]);
const tagNames = useMemo<FileItem[]>(() => {
return (
data?.map((tag) => {
return {
id: tag.id,
originalName: tag.sampleName,
};
}) || []
);
}, [data]);
const onListAddTag = async (tags: TagItem[]) => {
const sampleIds = data?.map((sample) => sample.id);
const ids = tags?.map((tag) => tag.id);
await relateSample({ tagId: ids, sampleIds: sampleIds as number[] });
setModalVisible(false);
const map = new Map();
const newTags = [...(value.tags || []), ...tags];
const tags_total = newTags
?.flat()
.filter((v) => !map.has(v.id) && map.set(v.id, v));
setValue({ ...value, tags: tags_total });
// 执行刷新回调
message.success('添加标签成功');
if (props.onRefresh) {
props.onRefresh();
}
};
const handleDeleteAll = async () => {
const ids = data?.map((sample) => sample.id);
if (ids) {
await deleteSample(ids.join(','));
message.success('删除成功');
if (props.onRefresh) {
props.onRefresh('delete');
}
}
};
// 下载
const handleDownloadAll = () => {};
const handleTagManager = () => {
setTagManagerVisible(true);
};
const forMap = (tags: TagItem[]) => (
<div style={{ display: 'inline-block', marginBottom: 10 }}>
{tags.map((tag) => (
<Tag
closable
key={tag.id}
style={{ marginBottom: 10 }}
onClose={async (e) => {
e.preventDefault();
await deleteSampleTagRelate({
sampleIds: value.sampleIds || [],
tagId: [tag.id],
});
message.success('删除成功');
props?.onRefresh?.();
setValue({ ...value, tags: tags.filter((t) => t.id !== tag.id) });
}}
>
{tag.tagName}
</Tag>
))}
</div>
);
const onRename = () => {
props.onRefresh?.();
setTagManagerVisible(false);
};
return (
<>
{/* <Switch
style={{
marginBlockEnd: 16,
}}
checked={readonly}
checkedChildren="编辑"
unCheckedChildren="只读"
onChange={setReadonly}
/> */}
<ProForm
readonly={readonly}
name="validate_other"
formRef={formRef}
initialValues={{
sample_name: '1111',
}}
onValuesChange={(_, values) => {
console.log(values);
}}
onFinish={async (value) => console.log(value)}
>
<ProFormGroup title="预览">{/* <audio></audio> */}</ProFormGroup>
<ProForm name="validate_other" formRef={formRef} submitter={false}>
{type === 'radio' && (
<ProFormGroup title="预览">
{data?.[0].sampleFilePath && (
<audio
controls
preload="true"
crossOrigin="anonymous"
style={{ marginBottom: 24 }}
>
<source src={data[0].sampleFilePath} />
<track kind="captions" />
</audio>
)}
</ProFormGroup>
)}
<ProFormGroup title="基本信息">
{type === 'radio' && (
<ProFormText
width="md"
name="sampleName"
placeholder="请输入样本名称"
fieldProps={{
onBlur: async (e) => {
if (e.target.value) {
const newData =
data?.map((sample) => {
return {
id: sample.id,
sampleName: e.target.value,
};
}) || [];
await updateSamples(newData);
props?.onRefresh?.();
message.success('更新成功');
}
},
}}
rules={[{ required: true, message: '样本名称不能为空' }]}
/>
)}
<ProFormText
width="md"
name="sample_name"
placeholder="请输入样本名称"
rules={[{ required: true, message: '样本名称不能为空' }]}
fieldProps={{
onBlur: async (e) => {
if (e.target.value) {
const newData =
data?.map((sample) => {
return {
id: sample.id,
remark: e.target.value,
};
}) || [];
await updateSamples(newData);
props?.onRefresh?.();
}
},
}}
name="remark"
placeholder="请输入注释"
/>
<ProFormText width="md" name="remark" placeholder="请输入注释" />
</ProFormGroup>
<ProFormGroup
title="标签"
style={{
gap: '0 32px',
}}
<ProFormGroup title="标签">
{/* <Form.Item name="tag"> */}
{forMap(value.tags || [])}
</ProFormGroup>
<Button
type="dashed"
block
style={{ marginBottom: 24 }}
onClick={handleAddTag}
>
<ProForm.Item name="tags">
<TagEditor placeholder="输入标签名称" maxCount={10} />
</ProForm.Item>
</ProFormGroup>
</Button>
<ProFormGroup title="文本信息" block></ProFormGroup>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span>: </span>
<span>{dayjs(value.createTime).format('YYYY-MM-DD HH:mm:ss')}</span>
</Space>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span></span>
<span>{dayjs(value.updateTime).format('YYYY-MM-DD HH:mm:ss')}</span>
</Space>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span>: </span>
<span>{value.sampleSize}</span>
</Space>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span>: </span>
<span>{value.sampleMineType}</span>
</Space>
<Space size={10} style={{ width: '100%', marginBottom: 12 }}>
<span>: </span>
<span>{value.sampleTime}</span>
</Space>
{type === 'checkbox' && (
<>
<ProFormGroup title="其他"></ProFormGroup>
<Button
block
style={{ marginBottom: 24 }}
onClick={handleTagManager}
>
</Button>
<Button
block
style={{ marginBottom: 24 }}
onClick={handleDownloadAll}
>
</Button>
<Button
block
color="danger"
style={{ marginBottom: 24 }}
onClick={handleDeleteAll}
>
</Button>
</>
)}
</ProForm>
<GroupTagModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onChange={onListAddTag}
editable={false}
value={value?.tags}
request={{
groupsApi: {
get: getSampleTagGroup,
create: createSampleTagGroup,
delete: deleteSampleTagGroup,
update: updateSampleTagGroup,
},
tagsApi: {
get: getSampleTagPage,
create: createSampleTag,
delete: deleteSampleTag,
update: updateSampleTag,
},
}}
title="管理技术标签"
width={800}
height={500}
/>
<TagManager
visible={tagManagerVisible}
files={tagNames}
onOk={onRename}
onCancel={() => setTagManagerVisible(false)}
></TagManager>
<Space style={{ width: '100%', justifyContent: 'center', padding: 12 }}>
<Button color="danger" onClick={() => {}}>
</Button>
<Button color="danger" variant="solid" onClick={handleDeleteAll}>
</Button>
</Space>
</>
);
};
export default SampleTagDetail;
export default React.memo(SampleTagDetail) as <T extends Record<string, any>>(
props: SampleTagDetailProps<T>,
) => React.ReactElement;

View File

@@ -2,14 +2,20 @@
display: flex;
width: 100%;
background: #fff;
overflow: auto;
:global {
.ant-pro-table {
flex: 1 auto;
}
.detail {
display: flex;
flex-direction: column;
border-left: 1px solid #e8e8e8;
width: 400px;
padding: 16px;
form {
flex: 1;
}
}
}
}

View File

@@ -1,59 +1,125 @@
import type { ActionType } from '@ant-design/pro-components';
import type { RowSelectionType } from 'antd/es/table/interface';
import React, { useRef, useState } from 'react';
import EnhancedProTable from '@/components/EnhancedProTable';
import type { ToolbarAction } from '@/components/EnhancedProTable/types';
import GroupTagModal from '@/components/GroupTag/GroupTagModal';
import UploadCard from '@/components/Upload/UploadCard';
import {
type AiSampleRespVO,
createSampleTag,
createSampleTagGroup,
deleteSampleTag,
deleteSampleTagGroup,
getSamplePage,
getSampleTagGroup,
getSampleTagPage,
type SampleReqVo,
updateSampleTag,
updateSampleTagGroup,
} from '@/services/ai/sample';
import { baseTenantColumns } from './config';
import SampleTagDetail from './detail';
import styles from './index.module.less';
const SampleTag: React.FC = () => {
const tableRef = useRef<ActionType>(null);
// const [detail, setDetail] = useState<any>(null);
// const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const handleAll = () => {
// console.log(tableRef.current.getSelectedRowKeys());
const [modalVisible, setModalVisible] = useState(false);
const [selectedRows, setSelectedRows] = useState<AiSampleRespVO[]>([]);
const [selectTableType, setSelectTableType] =
useState<RowSelectionType>('radio');
const handleAll = (selectTableType: RowSelectionType) => {
setSelectedRows([]);
setSelectTableType(selectTableType === 'radio' ? 'checkbox' : 'radio');
};
const handleTags = () => {
setModalVisible(true);
};
const toolbarActions: ToolbarAction[] = [
{
key: 'add',
label: '批量编辑',
label: selectTableType === 'checkbox' ? '取消编辑' : '批量编辑',
onClick: () => handleAll(selectTableType),
},
{
key: 'tags',
label: '标签管理',
type: 'primary',
onClick: handleAll,
onClick: handleTags,
},
];
const onFetch = async (_params: API.getProductCategoryCategoryListParams) => {
// const data = await getCategoryList({
// ...params,
// });
const data: any = [
{ sample_name: 111, id: 1, remark: 222 },
{ sample_name: 22, id: 2, remark: 33 },
];
const onFetch = async (params: SampleReqVo) => {
const data = await getSamplePage({
...params,
});
return {
data: data,
data: data.list,
total: data.total,
success: true,
};
};
const onRefresh = (type?: string) => {
tableRef.current?.reload();
tableRef.current?.onValuesChange({}, {});
type && setSelectedRows([]);
};
return (
<>
<UploadCard />
<div className={styles['tag-content']}>
<EnhancedProTable<API.CategoryDO>
<EnhancedProTable<AiSampleRespVO>
ref={tableRef}
columns={baseTenantColumns}
request={onFetch}
toolbarActions={toolbarActions}
headerTitle="样本列表"
showIndex={false}
showSelection={true}
enableRowClick={true}
rowSelection={{
type: selectTableType,
selectedRowKeys: selectedRows.map((item) => item.id) as React.Key[],
onChange: (_, selectedRows) => {
setSelectedRows(selectedRows);
},
}}
/>
<div className="detail">
<SampleTagDetail />
</div>
{selectedRows.length > 0 && (
<div className="detail">
<SampleTagDetail<AiSampleRespVO>
type={selectTableType}
data={selectedRows}
onRefresh={onRefresh}
/>
</div>
)}
</div>
<GroupTagModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
editable={true}
request={{
groupsApi: {
get: getSampleTagGroup,
create: createSampleTagGroup,
delete: deleteSampleTagGroup,
update: updateSampleTagGroup,
},
tagsApi: {
get: getSampleTagPage,
create: createSampleTag,
delete: deleteSampleTag,
update: updateSampleTag,
},
}}
title="管理技术标签"
width={800}
height={500}
/>
</>
);
};