From 4e9ebc76f7c1d97a00c81e1db6cd7d2050bd1c9d Mon Sep 17 00:00:00 2001 From: wuxichen <17301714657@163.com> Date: Thu, 25 Sep 2025 13:51:24 +0800 Subject: [PATCH] feat: upload --- .env.local | 2 + mock/ap.ts | 14 ++ mock/notices.ts | 113 +---------- src/components/DrawerForm/index.tsx | 3 +- src/components/EnhancedProTable/index.tsx | 82 +++----- .../Upload/UploadCard/index.module.less | 8 + src/components/Upload/UploadCard/index.tsx | 176 ++++++++++++++++++ src/pages/ai/sample-tag/config.tsx | 136 ++++++++++++++ src/pages/ai/sample-tag/detail.tsx | 81 ++++++++ src/pages/ai/sample-tag/index.module.less | 15 ++ src/pages/ai/sample-tag/index.tsx | 61 ++++++ src/services/ai/sample/index.ts | 161 ++++++++++++++++ 12 files changed, 687 insertions(+), 165 deletions(-) create mode 100644 mock/ap.ts create mode 100644 src/components/Upload/UploadCard/index.module.less create mode 100644 src/components/Upload/UploadCard/index.tsx create mode 100644 src/pages/ai/sample-tag/config.tsx create mode 100644 src/pages/ai/sample-tag/detail.tsx create mode 100644 src/pages/ai/sample-tag/index.module.less create mode 100644 src/pages/ai/sample-tag/index.tsx create mode 100644 src/services/ai/sample/index.ts diff --git a/.env.local b/.env.local index 9b994ea..2ffb9c3 100644 --- a/.env.local +++ b/.env.local @@ -14,6 +14,8 @@ VITE_UPLOAD_TYPE=server # 接口地址 VITE_API_URL=/admin-api +VITE_API_URL_PREFIX=/ai/sample/create + # 是否删除debugger VITE_DROP_DEBUGGER=false diff --git a/mock/ap.ts b/mock/ap.ts new file mode 100644 index 0000000..a4d2b8e --- /dev/null +++ b/mock/ap.ts @@ -0,0 +1,14 @@ +import type { Request, Response } from "express"; +import mockjs from "mockjs"; + +const getTags = (_: Request, res: Response) => { + return res.json({ + data: mockjs.mock({ + "list|100": [{ name: "@city", "value|1-100": 150, "type|0-2": 1 }], + }), + }); +}; + +export default { + "GET /api/tags": getTags, +}; diff --git a/mock/notices.ts b/mock/notices.ts index d896311..62522d3 100644 --- a/mock/notices.ts +++ b/mock/notices.ts @@ -1,117 +1,20 @@ -import type { Request, Response } from 'express'; +import type { Request, Response } from "express"; -const getNotices = (_req: Request, res: Response) => { +const getSampleTag = (_req: Request, res: Response) => { res.json({ data: [ { - id: '000000001', + id: "000000001", avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/MSbDR4FR2MUAAAAAAAAAAAAAFl94AQBr', - title: '你收到了 14 份新周报', - datetime: '2017-08-09', - type: 'notification', - }, - { - id: '000000002', - avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/hX-PTavYIq4AAAAAAAAAAAAAFl94AQBr', - title: '你推荐的 曲妮妮 已通过第三轮面试', - datetime: '2017-08-08', - type: 'notification', - }, - { - id: '000000003', - avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/jHX5R5l3QjQAAAAAAAAAAAAAFl94AQBr', - title: '这种模板可以区分多种通知类型', - datetime: '2017-08-07', - read: true, - type: 'notification', - }, - { - id: '000000004', - avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Wr4mQqx6jfwAAAAAAAAAAAAAFl94AQBr', - title: '左侧图标用于区分不同的类型', - datetime: '2017-08-07', - type: 'notification', - }, - { - id: '000000005', - avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Mzj_TbcWUj4AAAAAAAAAAAAAFl94AQBr', - title: '内容不要超过两行字,超出时自动截断', - datetime: '2017-08-07', - type: 'notification', - }, - { - id: '000000006', - avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/eXLzRbPqQE4AAAAAAAAAAAAAFl94AQBr', - title: '曲丽丽 评论了你', - description: '描述信息描述信息描述信息', - datetime: '2017-08-07', - type: 'message', - clickClose: true, - }, - { - id: '000000007', - avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/w5mRQY2AmEEAAAAAAAAAAAAAFl94AQBr', - title: '朱偏右 回复了你', - description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', - datetime: '2017-08-07', - type: 'message', - clickClose: true, - }, - { - id: '000000008', - avatar: - 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/wPadR5M9918AAAAAAAAAAAAAFl94AQBr', - title: '标题', - description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', - datetime: '2017-08-07', - type: 'message', - clickClose: true, - }, - { - id: '000000009', - title: '任务名称', - description: '任务需要在 2017-01-12 20:00 前启动', - extra: '未开始', - status: 'todo', - type: 'event', - }, - { - id: '000000010', - title: '第三方紧急代码变更', - description: - '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', - extra: '马上到期', - status: 'urgent', - type: 'event', - }, - { - id: '000000011', - title: '信息安全考试', - description: '指派竹尔于 2017-01-09 前完成更新并发布', - extra: '已耗时 8 天', - status: 'doing', - type: 'event', - }, - { - id: '000000012', - title: 'ABCD 版本发布', - description: - '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', - extra: '进行中', - status: 'processing', - type: 'event', + "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/MSbDR4FR2MUAAAAAAAAAAAAAFl94AQBr", + title: "你收到了 14 份新周报", + datetime: "2017-08-09", + type: "notification", }, ], }); }; export default { - 'GET /api/notices': getNotices, + "GET /api/notices": getSampleTag, }; diff --git a/src/components/DrawerForm/index.tsx b/src/components/DrawerForm/index.tsx index ec7f714..3a2965d 100644 --- a/src/components/DrawerForm/index.tsx +++ b/src/components/DrawerForm/index.tsx @@ -1,6 +1,7 @@ import type { ProFormColumnsType } from '@ant-design/pro-components'; import { BetaSchemaForm } from '@ant-design/pro-components'; import { Button, type ColProps, Drawer, Space, Typography } from 'antd'; +import type { FormInstance } from 'antd/lib'; import React, { forwardRef, useImperativeHandle } from 'react'; interface ConfigurableDrawerFormProps { @@ -38,7 +39,7 @@ const ConfigurableDrawerForm = forwardRef< const [formData, setFormData] = React.useState(initialValues || {}); const [loading, setLoading] = React.useState(false); // 添加表单实例引用 - const formRef = React.useRef(null); + const formRef = React.useRef(null); useImperativeHandle(ref, () => ({ open: (data) => { if (data) { diff --git a/src/components/EnhancedProTable/index.tsx b/src/components/EnhancedProTable/index.tsx index dfefa53..16c7016 100644 --- a/src/components/EnhancedProTable/index.tsx +++ b/src/components/EnhancedProTable/index.tsx @@ -6,7 +6,7 @@ import { type ParamsType, ProTable, } from '@ant-design/pro-components'; -import { Button, Space } from 'antd'; +import { Button, Space, Table } from 'antd'; import React, { forwardRef, useCallback, useMemo, useState } from 'react'; import { formatPaginationTotal } from '@/utils/antd/tableHelpers'; import type { BaseRecord, EnhancedProTableProps } from './types'; @@ -33,7 +33,7 @@ function EnhancedProTable( // onExport, // customToolbarRender, customActionRender, - rowKey, + rowKey = 'id', ...restProps } = props; @@ -42,84 +42,48 @@ function EnhancedProTable( // 行选择配置 const rowSelection = useMemo(() => { if (!showSelection) return undefined; - return { selectedRowKeys, + selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT], onChange: (keys: React.Key[], rows: T[]) => { setSelectedRowKeys(keys); setSelectedRows(rows); }, getCheckboxProps: (record: T) => ({ - name: record.id?.toString(), + name: record[rowKey]?.toString(), }), }; }, [showSelection, selectedRowKeys]); - // 表格提醒渲染 - const tableAlertRender = useCallback( - ({ selectedRowKeys, onCleanSelected }: any) => { - if (!showSelection || selectedRowKeys.length === 0) return false; - - return ( - - - 已选 {selectedRowKeys.length} 项 - - 取消选择 - - - - ); - }, - [showSelection], - ); - - const toolBarRender = useCallback(() => - // action: ActionType | undefined, - // rows: { - // selectedRowKeys?: (string | number)[] | undefined; - // selectedRows?: T[] | undefined; - // } - { - const toolbarElements = - toolbarActions?.map((action) => { - return ( - - ); - }) || []; - // return [ - // , - // ]; - return toolbarElements; - }, [toolbarActions]); + const toolBarRender = useCallback(() => { + const toolbarElements = + toolbarActions?.map((action) => { + return ( + + ); + }) || []; + return toolbarElements; + }, [toolbarActions]); return ( {...restProps} columns={columns} actionRef={ref} request={request} - rowKey={rowKey || 'id'} + rowKey={rowKey} rowSelection={rowSelection} toolBarRender={toolBarRender} manualRequest={false} showSorterTooltip - tableAlertRender={tableAlertRender} scroll={{ x: 'max-content' }} search={{ labelWidth: 'auto', diff --git a/src/components/Upload/UploadCard/index.module.less b/src/components/Upload/UploadCard/index.module.less new file mode 100644 index 0000000..295e693 --- /dev/null +++ b/src/components/Upload/UploadCard/index.module.less @@ -0,0 +1,8 @@ +.uploader-card { + background-color: #fff; + :global { + .ant-upload-drag { + background-color: #fff; + } + } +} diff --git a/src/components/Upload/UploadCard/index.tsx b/src/components/Upload/UploadCard/index.tsx new file mode 100644 index 0000000..e8297ed --- /dev/null +++ b/src/components/Upload/UploadCard/index.tsx @@ -0,0 +1,176 @@ +import { + DeleteOutlined, + FileTextOutlined, + PlayCircleOutlined, +} from '@ant-design/icons'; +import { message, Upload } from 'antd'; +import type { RcFile, UploadFile, UploadProps } from 'antd/lib/upload'; +import React, { useState } from 'react'; +import { createSample, getSample } from '@/services/ai/sample'; +import styles from './index.module.less'; + +const { Dragger } = Upload; +interface AudioUploaderProps { + value?: UploadFile[] | UploadFile | null; + onChange?: (file: UploadFile[] | UploadFile | null) => void; + maxSize?: number; + accept?: string; + disabled?: boolean; + maxCount?: number; + placeholder?: string; +} + +const AudioUploader: React.FC = ({ + value, + onChange, + maxSize = 10, + accept = '.mp3,.wav,.flac,.aac,.ogg', + disabled = false, + maxCount = 10, + placeholder = '点击或拖拽音频文件到此区域上传', +}) => { + const [fileList, setFileList] = useState(() => { + if (Array.isArray(value)) { + return value; + } else if (value) { + return [value]; + } + return []; + }); + const [uploading, setUploading] = useState(false); + + const beforeUpload = (file: RcFile): boolean => { + const isAudio = file.type.startsWith('audio/'); + if (!isAudio) { + message.error('只能上传音频文件!'); + return false; + } + + // 检查文件大小 + const isLtMaxSize = file.size / 1024 / 1024 < maxSize; + if (!isLtMaxSize) { + message.error(`音频文件大小不能超过 ${maxSize}MB!`); + return false; + } + + // 检查文件数量限制 + if (fileList.length >= maxCount) { + message.error(`最多只能上传 ${maxCount} 个文件!`); + return false; + } + + return true; + }; + + // 实际的后端接口上传 + const uploadToServer = async (file: File) => { + const formData = new FormData(); + formData.append('file', file); + // formData.append("type", "audio"); // 可以添加额外参数 + + const response = await createSample(formData); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }; + + const customRequest: UploadProps['customRequest'] = async (options) => { + const { file, onSuccess, onError, onProgress } = options; + + try { + setUploading(true); + + // 模拟进度更新 + onProgress?.({ percent: 10 }); + + // 调用后端接口 + const result = await uploadToServer(file as File); + + onProgress?.({ percent: 100 }); + + if (result.success && result.data) { + // 构造返回数据 + const responseData = { + url: result.data.url, + name: result.data.filename || (file as File).name, + uid: (file as any).uid, + status: 'done' as const, + response: result.data, + }; + + onSuccess?.(responseData); + message.success('音频上传成功!'); + } else { + throw new Error(result.message || '上传失败'); + } + } catch (error) { + console.error('Upload error:', error); + onError?.(error as Error); + message.error(`上传失败: ${(error as Error).message}`); + } finally { + setUploading(false); + } + }; + + const handleChange: UploadProps['onChange'] = ({ + fileList: newFileList, + file, + }) => { + setFileList(newFileList); + onChange?.(newFileList); + }; + + const handleRemove = (file: UploadFile): boolean => { + const newFileList = fileList.filter((item) => item.uid !== file.uid); + setFileList(newFileList); + onChange?.(newFileList); + return true; + }; + + const handlePreview = (file: UploadFile): void => { + const audioUrl = file.url || (file.response as any)?.url; + if (audioUrl) { + const audio = new Audio(audioUrl); + audio.play().catch(() => { + message.error('音频播放失败'); + }); + } + }; + + const uploadProps: UploadProps = { + name: 'file', + multiple: true, + fileList, + accept, + disabled: disabled || uploading, + beforeUpload, + customRequest, + // onChange: handleChange, + onRemove: handleRemove, + onPreview: handlePreview, + showUploadList: { + showPreviewIcon: true, + showRemoveIcon: true, + previewIcon: , + removeIcon: , + }, + }; + + return ( +
+ +

+ +

+

{placeholder}

+

+ 支持格式:{accept},单次最多同时上传 {maxCount} 个文件 +

+
+
+ ); +}; + +export default AudioUploader; diff --git a/src/pages/ai/sample-tag/config.tsx b/src/pages/ai/sample-tag/config.tsx new file mode 100644 index 0000000..3212101 --- /dev/null +++ b/src/pages/ai/sample-tag/config.tsx @@ -0,0 +1,136 @@ +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[] = [ + { + title: '样本名称', + dataIndex: 'sample_name', + }, + { + title: '注释', + dataIndex: 'remark', + hideInSearch: true, + }, + { + title: '标签', + hideInTable: true, + dataIndex: 'tag_name', + }, + { + title: '样本格式', + hideInTable: true, + dataIndex: 'sample_mine_type', + }, +]; + +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 ; + }, + }, + { + title: '关联父级', + dataIndex: 'parentId', + valueType: 'select', + hideInForm: data.grade - 1 === 0, + }, + { + title: '类目icon', + dataIndex: 'icon', + }, + { + title: '类目标签', + dataIndex: 'tages', + renderFormItem: () => { + return ( + + ); + }, + }, + { + 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: "模板内容", +// dataIndex: "content", +// valueType: "textarea", +// }, diff --git a/src/pages/ai/sample-tag/detail.tsx b/src/pages/ai/sample-tag/detail.tsx new file mode 100644 index 0000000..75b1261 --- /dev/null +++ b/src/pages/ai/sample-tag/detail.tsx @@ -0,0 +1,81 @@ +import { + ProForm, + ProFormCascader, + ProFormCheckbox, + ProFormColorPicker, + ProFormDigit, + ProFormDigitRange, + ProFormGroup, + ProFormRadio, + ProFormSelect, + ProFormSlider, + ProFormSwitch, + ProFormText, +} from '@ant-design/pro-components'; +import { Switch } from 'antd'; +import type { FormInstance } from 'antd/lib'; +import Mock from 'mockjs'; +import { useRef, useState } from 'react'; +import TagEditor from '@/components/TagEditor'; + +export const waitTime = (time: number = 100) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, time); + }); +}; + +const SampleTagDetail = () => { + const [readonly, setReadonly] = useState(false); + const formRef = useRef(null); + + return ( + <> + {/* */} + { + console.log(values); + }} + onFinish={async (value) => console.log(value)} + > + {/* */} + + + + + + + + + + + + ); +}; + +export default SampleTagDetail; diff --git a/src/pages/ai/sample-tag/index.module.less b/src/pages/ai/sample-tag/index.module.less new file mode 100644 index 0000000..5508333 --- /dev/null +++ b/src/pages/ai/sample-tag/index.module.less @@ -0,0 +1,15 @@ +.tag-content { + display: flex; + width: 100%; + background: #fff; + :global { + .ant-pro-table { + flex: 1 auto; + } + .detail { + border-left: 1px solid #e8e8e8; + width: 400px; + padding: 16px; + } + } +} diff --git a/src/pages/ai/sample-tag/index.tsx b/src/pages/ai/sample-tag/index.tsx new file mode 100644 index 0000000..88463e2 --- /dev/null +++ b/src/pages/ai/sample-tag/index.tsx @@ -0,0 +1,61 @@ +import type { ActionType } from '@ant-design/pro-components'; +import React, { useRef, useState } from 'react'; +import EnhancedProTable from '@/components/EnhancedProTable'; +import type { ToolbarAction } from '@/components/EnhancedProTable/types'; +import UploadCard from '@/components/Upload/UploadCard'; +import { baseTenantColumns } from './config'; +import SampleTagDetail from './detail'; +import styles from './index.module.less'; + +const SampleTag: React.FC = () => { + const tableRef = useRef(null); + // const [detail, setDetail] = useState(null); + // const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const handleAll = () => { + // console.log(tableRef.current.getSelectedRowKeys()); + }; + const toolbarActions: ToolbarAction[] = [ + { + key: 'add', + label: '批量编辑', + type: 'primary', + + onClick: handleAll, + }, + ]; + 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 }, + ]; + return { + data: data, + success: true, + }; + }; + + return ( + <> + +
+ + ref={tableRef} + columns={baseTenantColumns} + request={onFetch} + toolbarActions={toolbarActions} + headerTitle="样本列表" + showIndex={false} + showSelection={true} + /> +
+ +
+
+ + ); +}; + +export default SampleTag; diff --git a/src/services/ai/sample/index.ts b/src/services/ai/sample/index.ts new file mode 100644 index 0000000..b0e88d3 --- /dev/null +++ b/src/services/ai/sample/index.ts @@ -0,0 +1,161 @@ +import { request } from "@umijs/max"; + +export interface SampleVo { + /** + * 创建时间 + */ + createTime?: string[]; + /** + * 页码,从 1 开始", example = "1 + */ + remark?: string; + /** + * 样本文件id + */ + sampleFileId?: number; + /** + * 样本格式 + */ + sampleMineType?: string; + /** + * 样本名称 + */ + sampleName?: string; + /** + * 样本大小 + */ + sampleSize?: string; + /** + * 样本时长 + */ + sampleTime?: string[]; +} + +/** + * 返回数据 + * + * PageResultAiSampleRespVO + */ +export interface PageResultAiSampleRespVO { + /** + * 数据 + */ + list?: AiSampleRespVO[]; + /** + * 总量 + */ + total?: number; +} + +/** + * com.tashow.cloud.ai.controller.admin.aisample.vo.AiSampleRespVO + * + * AiSampleRespVO + */ +export interface AiSampleRespVO { + /** + * 创建时间 + */ + createTime?: string; + /** + * 主键 + */ + id?: number; + /** + * 样本注释 + */ + remark?: string; + /** + * 样本文件id + */ + sampleFileId?: number; + /** + * 样本格式 + */ + sampleMineType?: string; + /** + * 样本名称 + */ + sampleName?: string; + /** + * 样本大小 + */ + sampleSize?: string; + /** + * 样本时长 + */ + sampleTime?: string; +} + +export interface SampleReqVo extends PageParam { + name?: string; + status?: number; +} + +// 获得样本库分页 +export const getSamplePage = async (params: SampleReqVo) => { + return request("/ai/sample/get", { + method: "GET", + params, + }); +}; +export const getSample = async (id: number) => { + return request("/ai/sample/get", { + method: "GET", + params: { id }, + }); +}; + +// 查询部门列表 +// export const getDeptPage = async (params: DeptReq): Promise => { +// return await request.get({ url: "/system/dept/list", params }); +// }; + +export const getDeptPage = (params: SampleReqVo) => { + return request("/system/dept/list", { + method: "GET", + params, + }); +}; + +// 查询部门详情 +// export const getDept = async (id: number) => { +// return await request.get({ url: "/system/dept/get?id=" + id }); +// }; + +export const getDept = (id: number) => { + return request("/system/dept/get", { + method: "GET", + params: { id }, + }); +}; + +// 创建样本库 +export const createSample = (formData: FormData) => { + return request("/ai/sample/create", { + method: "POST", + headers: { + "Content-Type": "multipart/form-data", + }, + data: formData, + }); +}; + +export const updateSample = (params: SampleReqVo) => { + return request("/ai/sample/update", { + method: "PUT", + data: params, + }); +}; + +// 删除部门 +// export const deleteDept = async (id: number) => { +// return await request.delete({ url: "/system/dept/delete?id=" + id }); +// }; + +export const deleteSample = (id: number) => { + return request("/ai/sample/delete", { + method: "DELETE", + params: { id }, + }); +};