Merge branch 'wuxichen' of http://gitea.tashowz.com/tashow/tashow-manager into qianpw
Some checks failed
coverage CI / build (push) Has been cancelled

This commit is contained in:
2025-09-26 16:38:32 +08:00
12 changed files with 687 additions and 165 deletions

View File

@@ -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

14
mock/ap.ts Normal file
View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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<boolean>(false);
// 添加表单实例引用
const formRef = React.useRef<any>(null);
const formRef = React.useRef<FormInstance>(null);
useImperativeHandle(ref, () => ({
open: (data) => {
if (data) {

View File

@@ -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<T extends BaseRecord, U extends ParamsType = any>(
// onExport,
// customToolbarRender,
customActionRender,
rowKey,
rowKey = 'id',
...restProps
} = props;
@@ -42,45 +42,20 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
// 行选择配置
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 (
<Space size={24}>
<span>
{selectedRowKeys.length}
<a style={{ marginLeft: 8 }} onClick={onCleanSelected}>
</a>
</span>
</Space>
);
},
[showSelection],
);
const toolBarRender = useCallback(() =>
// action: ActionType | undefined,
// rows: {
// selectedRowKeys?: (string | number)[] | undefined;
// selectedRows?: T[] | undefined;
// }
{
const toolBarRender = useCallback(() => {
const toolbarElements =
toolbarActions?.map((action) => {
return (
@@ -96,16 +71,6 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
</Button>
);
}) || [];
// return [
// <Button
// key="button"
// icon={<PlusOutlined />}
// onClick={}
// type="primary"
// >
// 新建
// </Button>,
// ];
return toolbarElements;
}, [toolbarActions]);
return (
@@ -114,12 +79,11 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
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',

View File

@@ -0,0 +1,8 @@
.uploader-card {
background-color: #fff;
:global {
.ant-upload-drag {
background-color: #fff;
}
}
}

View File

@@ -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<AudioUploaderProps> = ({
value,
onChange,
maxSize = 10,
accept = '.mp3,.wav,.flac,.aac,.ogg',
disabled = false,
maxCount = 10,
placeholder = '点击或拖拽音频文件到此区域上传',
}) => {
const [fileList, setFileList] = useState<UploadFile[]>(() => {
if (Array.isArray(value)) {
return value;
} else if (value) {
return [value];
}
return [];
});
const [uploading, setUploading] = useState<boolean>(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: <PlayCircleOutlined />,
removeIcon: <DeleteOutlined />,
},
};
return (
<div className={styles['uploader-card']}>
<Dragger {...uploadProps}>
<p className="">
<FileTextOutlined style={{ fontSize: 36 }} />
</p>
<p className="ant-upload-text">{placeholder}</p>
<p className="ant-upload-hint">
{accept} {maxCount}
</p>
</Dragger>
</div>
);
};
export default AudioUploader;

View File

@@ -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<API.CategoryDO>[] = [
{
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 <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: "模板内容",
// dataIndex: "content",
// valueType: "textarea",
// },

View File

@@ -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<FormInstance>(null);
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>
<ProFormGroup title="基本信息">
<ProFormText
width="md"
name="sample_name"
placeholder="请输入样本名称"
rules={[{ required: true, message: '样本名称不能为空' }]}
/>
<ProFormText width="md" name="remark" placeholder="请输入注释" />
</ProFormGroup>
<ProFormGroup
title="标签"
style={{
gap: '0 32px',
}}
>
<ProForm.Item name="tags">
<TagEditor placeholder="输入标签名称" maxCount={10} />
</ProForm.Item>
</ProFormGroup>
</ProForm>
</>
);
};
export default SampleTagDetail;

View File

@@ -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;
}
}
}

View File

@@ -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<ActionType>(null);
// const [detail, setDetail] = useState<any>(null);
// const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
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 (
<>
<UploadCard />
<div className={styles['tag-content']}>
<EnhancedProTable<API.CategoryDO>
ref={tableRef}
columns={baseTenantColumns}
request={onFetch}
toolbarActions={toolbarActions}
headerTitle="样本列表"
showIndex={false}
showSelection={true}
/>
<div className="detail">
<SampleTagDetail />
</div>
</div>
</>
);
};
export default SampleTag;

View File

@@ -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<Dept[]> => {
// 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 },
});
};