diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index bf51980e..bf012a65 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -143,6 +143,11 @@ export default [ path: 'compare', component: './Experiment/Comparison/index', }, + { + name: '实验可视化对比', + path: 'compare-visual', + component: './Experiment/Aim/index', + }, ], }, { diff --git a/react-ui/package.json b/react-ui/package.json index e4d95504..2c7b13a1 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -16,7 +16,7 @@ "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up", "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro", "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro", - "docs": "typedoc --entryPointStrategy expand --entryPoints 'src/utils' --skipErrorChecking --out docs", + "docs": "typedoc", "gh-pages": "gh-pages -d dist", "i18n-remove": "pro i18n-remove --locale=zh-CN --write", "postinstall": "max setup", diff --git a/react-ui/src/assets/img/confirm-icon.png b/react-ui/src/assets/img/confirm-icon.png new file mode 100644 index 00000000..6865b719 Binary files /dev/null and b/react-ui/src/assets/img/confirm-icon.png differ diff --git a/react-ui/src/assets/img/comfirm-icon.png b/react-ui/src/assets/img/copy-icon.png similarity index 100% rename from react-ui/src/assets/img/comfirm-icon.png rename to react-ui/src/assets/img/copy-icon.png diff --git a/react-ui/src/components/IFramePage/index.tsx b/react-ui/src/components/IFramePage/index.tsx index 6a1ddbdc..b57869d3 100644 --- a/react-ui/src/components/IFramePage/index.tsx +++ b/react-ui/src/components/IFramePage/index.tsx @@ -3,6 +3,7 @@ import KFSpin from '@/components/KFSpin'; import { getLabelStudioUrl } from '@/services/developmentEnvironment'; import { to } from '@/utils/promise'; import SessionStorage from '@/utils/sessionStorage'; +import { FloatButton } from 'antd'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -13,6 +14,7 @@ export enum IframePageType { AppDevelopment = 'AppDevelopment', // 应用开发 DevEnv = 'DevEnv', // 开发环境 GitLink = 'GitLink', // git link + Aim = 'Aim', // 实验对比 } const getRequestAPI = (type: IframePageType): (() => Promise) => { @@ -29,12 +31,20 @@ const getRequestAPI = (type: IframePageType): (() => Promise) => { }); case IframePageType.GitLink: // git link return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); + case IframePageType.Aim: // Aim + return () => + Promise.resolve({ + code: 200, + data: SessionStorage.getItem(SessionStorage.aimUrlKey) || '', + }); } }; type IframePageProps = { /** 子系统 */ type: IframePageType; + /** 是否可以在页签上打开 */ + openInTab: boolean; /** 自定义样式类名 */ className?: string; /** 自定义样式 */ @@ -42,7 +52,7 @@ type IframePageProps = { }; /** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ -function IframePage({ type, className, style }: IframePageProps) { +function IframePage({ type, openInTab = false, className, style }: IframePageProps) { const [iframeUrl, setIframeUrl] = useState(''); const [loading, setLoading] = useState(false); @@ -65,6 +75,7 @@ function IframePage({ type, className, style }: IframePageProps) {
{loading && createPortal(, document.body)} + {openInTab && window.open(iframeUrl, '_blank')} />}
); } diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less index fff69eb1..ff0a21f7 100644 --- a/react-ui/src/components/ParameterInput/index.less +++ b/react-ui/src/components/ParameterInput/index.less @@ -22,7 +22,7 @@ border-radius: 4px; &__value { - .singleLine(); + //.singleLine(); margin-right: 8px; font-size: @font-size-input; line-height: 1.5714285714285714; diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 32672b98..08cf8649 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -6,7 +6,7 @@ import { CommonTabKeys } from '@/enums'; import { CloseOutlined } from '@ant-design/icons'; -import { ConfigProvider, Form, Input } from 'antd'; +import { ConfigProvider, Form, Input, Typography } from 'antd'; import { RuleObject } from 'antd/es/form'; import classNames from 'classnames'; import './index.less'; @@ -120,7 +120,12 @@ function ParameterInput({ > {valueObj?.showValue ? (
- {valueObj?.showValue} + + {valueObj.showValue} + { } }; -// 移除所有页面 state 缓存 +/** + * 移除所有页面 state 缓存 + */ export const removeAllPageCacheState = () => { pageKeys.forEach((key) => { sessionStorage.removeItem(key); }); }; +/** + * 缓存页面数据 + */ export const useCacheState = () => { const { pathname } = window.location; const key = 'pagecache:' + pathname; diff --git a/react-ui/src/hooks/useCheck.ts b/react-ui/src/hooks/useCheck.ts index 1b965549..3c9c985b 100644 --- a/react-ui/src/hooks/useCheck.ts +++ b/react-ui/src/hooks/useCheck.ts @@ -1,15 +1,9 @@ import { useCallback, useMemo, useState } from 'react'; /** - * @description 选择、全选操作 - * @param list 需要进行选择的列表 - * @returns selected 选中的项 - * setSelected 设置 selected 的方法 - * checked 是否全选 - * indeterminate 是否部分选中 - * checkAll 全选 - * isSingleChecked 是否单个选中 - * checkSingle 单个选中 + * 选择、全选操作 + * @param list - 需要进行选择的列表 + * @return [选中的项, 设置选中的方法, 是否全选, 是否部分选中, 全选方法,是否单个选中,选中单个方法] */ export const useCheck = (list: T[]) => { const [selected, setSelected] = useState([]); diff --git a/react-ui/src/hooks/useComputingResource.ts b/react-ui/src/hooks/useComputingResource.ts index e1e27506..b2239247 100644 --- a/react-ui/src/hooks/useComputingResource.ts +++ b/react-ui/src/hooks/useComputingResource.ts @@ -12,7 +12,7 @@ import { useCallback, useEffect, useState } from 'react'; const computingResource: ComputingResource[] = []; -// 过滤资源规格 +/** 过滤资源规格 */ export const filterResourceStandard: SelectProps['filterOption'] = ( input: string, option?: ComputingResource, @@ -22,13 +22,13 @@ export const filterResourceStandard: SelectProps['fil ); }; -// 资源规格字段 +/** 资源规格字段 */ export const resourceFieldNames = { label: 'description', value: 'id', }; -// 获取资源规格 +/** 获取资源规格 */ export function useComputingResource() { const [resourceStandardList, setResourceStandardList] = useState([]); diff --git a/react-ui/src/hooks/useDraggable.ts b/react-ui/src/hooks/useDraggable.ts index b093ea6f..121aaafa 100644 --- a/react-ui/src/hooks/useDraggable.ts +++ b/react-ui/src/hooks/useDraggable.ts @@ -1,6 +1,8 @@ -// 处理 react-draggable 组件拖动结束时,响应了点击事件的 import { useState } from 'react'; +/** + * 处理 react-draggable 组件拖动结束时,响应了点击事件的 + */ export const useDraggable = (onClick: () => void) => { const [isDragging, setIsDragging] = useState(false); diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx index 756b11c7..9b27862d 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx @@ -58,7 +58,8 @@ function ExperimentInstanceComponent({ // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { modalConfirm({ - title: '确定删除该条实例吗?', + title: '删除后,该实验实例将不可恢复', + content: '是否确认删除?', onOk: () => { deleteExperimentInstance(instance.id); }, @@ -96,6 +97,18 @@ function ExperimentInstanceComponent({ } }; + // 终止实验实例 + const handleTerminate = (instance: ExperimentInstance) => { + modalConfirm({ + title: '终止后,该次实验运行将不可恢复', + content: '是否确认终止?', + isDelete: false, + onOk: () => { + terminateExperimentInstance(instance); + }, + }); + }; + // 终止实验实例 const terminateExperimentInstance = async (instance: ExperimentInstance) => { const request = config.stopInsReq; @@ -188,7 +201,7 @@ function ExperimentInstanceComponent({ item.status === ExperimentStatus.Terminated } icon={} - onClick={() => terminateExperimentInstance(item)} + onClick={() => handleTerminate(item)} > 终止 diff --git a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx index 883e20c7..624ef833 100644 --- a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx @@ -4,7 +4,12 @@ import KFModal from '@/components/KFModal'; import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; import { addDataset } from '@/services/dataset/index.js'; import { to } from '@/utils/promise'; -import { getFileListFromEvent, limitUploadFileType, validateUploadFiles } from '@/utils/ui'; +import { + getFileListFromEvent, + limitUploadFileType, + removeUploadedFile, + validateUploadFiles, +} from '@/utils/ui'; import { Button, Form, @@ -29,11 +34,6 @@ interface AddDatasetModalProps extends Omit { function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { const [uuid] = useState(Date.now()); - // const [clusterOptions, setClusterOptions] = useState([]); - - // useEffect(() => { - // getClusterOptions(); - // }, []); // 上传组件参数 const uploadProps: UploadProps = { @@ -44,16 +44,9 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr defaultFileList: [], accept: '.zip,.tgz', beforeUpload: limitUploadFileType('zip,tgz'), + onRemove: removeUploadedFile, }; - // 获取集群版本数据 - // const getClusterOptions = async () => { - // const [res] = await to(getDictSelectOption('available_cluster')); - // if (res) { - // setClusterOptions(res); - // } - // }; - // 上传请求 const createDataset = async (params: any) => { const [res] = await to(addDataset(params)); @@ -113,7 +106,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr }, ]} > - + - + ; +} +export default AimPage; diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index 4b2d6d5b..200b4ba1 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -12,8 +12,9 @@ import { } from '@/services/experiment'; import { tableSorter } from '@/utils'; import { to } from '@/utils/promise'; +import SessionStorage from '@/utils/sessionStorage'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { useSearchParams } from '@umijs/max'; +import { useNavigate, useSearchParams } from '@umijs/max'; import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; @@ -46,6 +47,7 @@ function ExperimentComparison() { }); const { message } = App.useApp(); + const navigate = useNavigate(); const config = comparisonConfig[comparisonType]; useEffect(() => { @@ -73,7 +75,9 @@ function ExperimentComparison() { const [res] = await to(getExpMetricsReq(selectedRowKeys)); if (res && res.data) { const url = res.data; - window.open(url, '_blank'); + // window.open(url, '_blank'); + SessionStorage.setItem(SessionStorage.aimUrlKey, url); + navigate('../compare-visual'); } }; diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx index 9812b893..362cf995 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx @@ -62,7 +62,8 @@ function ExperimentInstanceComponent({ // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { modalConfirm({ - title: '确定删除该条实例吗?', + title: '删除后,该实验实例将不可恢复', + content: '是否确认删除?', onOk: () => { deleteExperimentInstance(instance.id); }, @@ -101,7 +102,8 @@ function ExperimentInstanceComponent({ // 终止实验实例 const handleTerminate = (instance: ExperimentInstance) => { modalConfirm({ - title: '确定要终止此次实验运行吗?', + title: '终止后,该次实验运行将不可恢复', + content: '是否确认终止?', isDelete: false, onOk: () => { terminateExperimentInstance(instance); diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index d283d863..b0006ce0 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -286,8 +286,6 @@ function Experiment() { message.success('运行成功'); refreshExperimentList(); refreshExperimentIns(id); - } else { - message.error('运行失败'); } }; diff --git a/react-ui/src/pages/Points/index.tsx b/react-ui/src/pages/Points/index.tsx index 0fa3a1d8..19baa315 100644 --- a/react-ui/src/pages/Points/index.tsx +++ b/react-ui/src/pages/Points/index.tsx @@ -25,19 +25,19 @@ enum TaskType { const taskTypeOptions = [ { - value: 'dev_environment', + value: TaskType.DevEnvironment, label: '开发环境', }, { - value: 'workflow', + value: TaskType.Workflow, label: '实验', }, { - value: 'ray', + value: TaskType.Ray, label: '超参数自动寻优', }, { - value: 'service', + value: TaskType.Service, label: '服务', }, ]; diff --git a/react-ui/src/pages/System/User/components/ResetPwd.tsx b/react-ui/src/pages/System/User/components/ResetPwd.tsx index 07bd8c03..33581242 100644 --- a/react-ui/src/pages/System/User/components/ResetPwd.tsx +++ b/react-ui/src/pages/System/User/components/ResetPwd.tsx @@ -17,6 +17,7 @@ const UpdateForm: React.FC = (props) => { const [form] = Form.useForm(); const loginPassword = Form.useWatch('password', form); const userId = props.values.userId; + const originPassword = props.values.originPassword; const intl = useIntl(); const handleOk = () => { @@ -26,7 +27,7 @@ const UpdateForm: React.FC = (props) => { props.onCancel(); }; const handleFinish = async (values: Record) => { - props.onSubmit({ ...values, userId } as FormValueType); + props.onSubmit({ password: values.password, userId, originPassword } as FormValueType); }; const checkPassword = (rule: any, value: string) => { diff --git a/react-ui/src/pages/System/User/edit.tsx b/react-ui/src/pages/System/User/edit.tsx index 3c8f045b..e030eefb 100644 --- a/react-ui/src/pages/System/User/edit.tsx +++ b/react-ui/src/pages/System/User/edit.tsx @@ -63,8 +63,8 @@ const UserForm: React.FC = (props) => { loginIp: props.values.loginIp, loginDate: props.values.loginDate, remark: props.values.remark, - gitLinkUsername: props.values.gitLinkUsername, - gitLinkPassword: props.values.gitLinkPassword, + // gitLinkUsername: props.values.gitLinkUsername, + // gitLinkPassword: props.values.gitLinkPassword, credit: props.values.credit, }); }, [form, props, statusOptions]); @@ -80,6 +80,7 @@ const UserForm: React.FC = (props) => { const params = { ...values, userId: props.values.userId, + originPassword: props.values.originPassword, }; props.onSubmit(params as UserFormData); }; @@ -150,7 +151,7 @@ const UserForm: React.FC = (props) => { colProps={{ md: 12, xl: 12 }} rules={[ { - required: false, + required: true, message: , }, { @@ -174,7 +175,7 @@ const UserForm: React.FC = (props) => { colProps={{ md: 12, xl: 12 }} rules={[ { - required: false, + required: true, message: , }, { @@ -194,7 +195,7 @@ const UserForm: React.FC = (props) => { id: 'system.user.user_name', defaultMessage: '用户账号', })} - hidden={userId} + disabled={!!props.values.userId} placeholder="请输入用户账号" colProps={{ md: 12, xl: 12 }} rules={[ @@ -202,9 +203,9 @@ const UserForm: React.FC = (props) => { required: true, }, { - pattern: /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/, + pattern: /^[a-zA-Z](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/, message: - '只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以数字或字母开头与结尾', + '只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以字母开头,数字或字母结尾', }, ]} /> @@ -214,14 +215,23 @@ const UserForm: React.FC = (props) => { id: 'system.user.password', defaultMessage: '密码', })} - hidden={userId} placeholder="请输入密码" colProps={{ md: 12, xl: 12 }} fieldProps={{ autoComplete: 'new-password', }} allowClear - rules={props.values.userId ? [] : [{ required: true, message: '请输入密码!' }]} + rules={ + props.values.userId + ? [] + : [ + { required: true, message: '请输入密码!' }, + { + pattern: /^[A-Za-z0-9!"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]{8,16}$/, + message: '密码长度为8 ~ 16位,只支持字母数字和符号', + }, + ] + } /> = (props) => { colProps={{ md: 12, xl: 12 }} rules={[{ required: true, message: '请选择角色!' }]} /> - = (props) => { autoComplete: 'new-password', }} rules={props.values.userId ? [] : [{ required: true, message: '请输入 Git 密码!' }]} - /> + /> */} { /** * 导出数据 - * - * */ -const handleExport = async () => { +const handleExport = async (deptId: string) => { const hide = message.loading('正在导出'); try { - await exportUser(); + await downloadXlsx('/api/system/user/export', 'POST', { data: { deptId: deptId } }); hide(); - message.success('导出成功'); return true; } catch (error) { hide(); @@ -470,7 +467,7 @@ const UserTableList: React.FC = () => { key="export" hidden={!access.hasPerms('system:user:export')} onClick={async () => { - handleExport(); + handleExport(selectDept.id); }} > {' '} @@ -563,7 +560,7 @@ const UserTableList: React.FC = () => { /> { - const success = await resetUserPwd(values.userId, values.password); + const success = await resetUserPwd(values); if (success) { setResetPwdModalVisible(false); setSelectedRows([]); @@ -581,7 +578,7 @@ const UserTableList: React.FC = () => { /> { - const success = await updateAuthRole(values); + const success = await updateAuthRole(currentRow!.userId, values.roleIds); if (success) { setAuthRoleModalVisible(false); setSelectedRows([]); diff --git a/react-ui/src/services/dataset/index.js b/react-ui/src/services/dataset/index.js index 9ddeba2e..25cc8b16 100644 --- a/react-ui/src/services/dataset/index.js +++ b/react-ui/src/services/dataset/index.js @@ -181,4 +181,13 @@ export function compareModelVersion(data) { method: 'POST', data, }); -} \ No newline at end of file +} + + +// 删除上传的文件 +export function deleteUploadFileReq(params) { + return request(`/api/mmp/newdataset/deleteFile`, { + method: 'DELETE', + params, + }); +} diff --git a/react-ui/src/services/system/user.ts b/react-ui/src/services/system/user.ts index eb3cfeb8..fbd34f32 100644 --- a/react-ui/src/services/system/user.ts +++ b/react-ui/src/services/system/user.ts @@ -60,8 +60,9 @@ export async function removeUser(ids: string, options?: { [key: string]: any }) // 导出用户信息 export function exportUser(params?: API.System.UserListParams, options?: { [key: string]: any }) { return request(`/api/system/user/export`, { - method: 'GET', - params, + method: 'POST', + data: params, + skipValidating: true, ...(options || {}), }); } @@ -93,11 +94,7 @@ export function updateUserProfile(data: API.CurrentUser) { } // 用户密码重置 -export function resetUserPwd(userId: number, password: string) { - const data = { - userId, - password, - }; +export function resetUserPwd(data: any) { return request('/api/system/user/resetPwd', { method: 'put', data: data, @@ -126,16 +123,19 @@ export function uploadAvatar(data: any) { // 查询授权角色 export function getAuthRole(userId: number) { - return request('/system/user/authRole/' + userId, { + return request('/api/system/user/authRole/' + userId, { method: 'get', }); } // 保存授权角色 -export function updateAuthRole(data: Record) { - return request('/system/user/authRole', { - method: 'put', - params: data, +export function updateAuthRole(userId: number, data: Record) { + return request(`/api/system/user/authRole/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + data: data, }); } diff --git a/react-ui/src/stories/ParameterInput.stories.tsx b/react-ui/src/stories/ParameterInput.stories.tsx index 3ca116c8..ba4407d0 100644 --- a/react-ui/src/stories/ParameterInput.stories.tsx +++ b/react-ui/src/stories/ParameterInput.stories.tsx @@ -76,6 +76,45 @@ export const Select: Story = { }, }; +export const Ellipse: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + canInput: true, + size: 'large', + }, + render: function Render(args) { + const [value, setValue] = useState(''); + + const onClick = () => { + const value = { + value: 'storybook', + showValue: + 'storybookstorybookstorybookstorybookstorybookstorybookstorybookstorybookstorybookstorybook', + fromSelect: true, + otherValue: 'others', + }; + setValue(value); + action('onChange')(value); + }; + return ( + <> + { + setValue(value); + action('onChange')(value); + }} + > + + + ); + }, +}; + export const Disabled: Story = { args: { placeholder: '请输入工作目录', diff --git a/react-ui/src/types.ts b/react-ui/src/types.ts index e3c219ee..c3906e29 100644 --- a/react-ui/src/types.ts +++ b/react-ui/src/types.ts @@ -136,3 +136,17 @@ export type NodeStatus = { startedAt: string; finishedAt: string; }; + +// 应用响应 +export type AppResponse = { + code: number; + msg: string; + data: T; +}; + +// 上传文件的响应 +export type UploadFileRes = { + fileName: string; + fileSize: number; + url: string; +}; diff --git a/react-ui/src/types/system/user.d.ts b/react-ui/src/types/system/user.d.ts index 0eca095a..c129130c 100644 --- a/react-ui/src/types/system/user.d.ts +++ b/react-ui/src/types/system/user.d.ts @@ -22,6 +22,7 @@ declare namespace API.System { gitLinkUsername?: string; gitLinkPassword?: string; credit?: number; + originPassword?: string; } export interface UserListParams { diff --git a/react-ui/src/utils/constant.ts b/react-ui/src/utils/constant.ts index 4fe1ea9b..0ca0c423 100644 --- a/react-ui/src/utils/constant.ts +++ b/react-ui/src/utils/constant.ts @@ -1,3 +1,13 @@ -export const xlCols = { span: 12 }; -export const xllCols = { span: 10 }; +/* + * @Author: 赵伟 + * @Date: 2025-02-21 09:52:50 + * @Description: 通用表单项输入控件宽度 + */ + +const xlCols = { span: 12 }; +const xllCols = { span: 10 }; + +/** + * 输入控件宽度,xl: 12, xll: 10 + */ export const formCols = { xl: xlCols, xxl: xllCols }; diff --git a/react-ui/src/utils/date.ts b/react-ui/src/utils/date.ts index 33c8dda8..02734886 100644 --- a/react-ui/src/utils/date.ts +++ b/react-ui/src/utils/date.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs'; /** - * Calculates the elapsed time between two dates and returns a formatted string representing the duration. + * 计算两个日期之间经过的时间,如 "3分12秒" * * @param {string | null | undefined} begin - The starting date. * @param {string | null | undefined} end - The ending date. diff --git a/react-ui/src/utils/downloadfile.ts b/react-ui/src/utils/downloadfile.ts index ee56962e..7d4e46cd 100644 --- a/react-ui/src/utils/downloadfile.ts +++ b/react-ui/src/utils/downloadfile.ts @@ -1,19 +1,21 @@ import { request } from '@umijs/max'; -const mimeMap = { +/** MimeType */ +export const mimeMap = { xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', zip: 'application/zip', }; /** * 解析blob响应内容并下载 - * @param {*} res blob响应内容 - * @param {String} mimeType MIME类型 + * @param res - blob响应内容 + * @param mimeType - MIME类型 */ export function resolveBlob(res: any, mimeType: string) { const aLink = document.createElement('a'); const blob = new Blob([res.data], { type: mimeType }); - // //从response的headers中获取filename, 后端response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名; + // 从response的headers中获取filename, + // 后端response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名; const patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*'); // console.log(res); const contentDisposition = decodeURI(res.headers['content-disposition']); @@ -29,6 +31,11 @@ export function resolveBlob(res: any, mimeType: string) { document.body.removeChild(aLink); } +/** + * 下载 Zip 文件 + * @param url - url 地址 + * @param options - 请求参数 + */ export function downLoadZip(url: string, params?: any) { request(url, { method: 'GET', @@ -40,24 +47,30 @@ export function downLoadZip(url: string, params?: any) { }); } -export async function downLoadXlsx(url: string, params: any, fileName: string) { +/** + * 下载 XLSX 文件 + * @param url - url 地址 + * @param method - 请求方式 + * @param options - 请求选项 + */ +export async function downloadXlsx( + url: string, + method: string = 'GET', + options?: Record, +) { return request(url, { - ...params, - method: 'POST', + method: method, + ...options, responseType: 'blob', - }).then((data) => { - const aLink = document.createElement('a'); - const blob = data as any; // new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); - aLink.style.display = 'none'; - aLink.href = URL.createObjectURL(blob); - aLink.setAttribute('download', fileName); // 设置下载文件名称 - document.body.appendChild(aLink); - aLink.click(); - URL.revokeObjectURL(aLink.href); // 清除引用 - document.body.removeChild(aLink); + getResponse: true, + }).then((res) => { + resolveBlob(res, mimeMap.xlsx); }); } +/** + * @deprecated 无效 + */ export function download(fileName: string) { window.location.href = `/api/common/download?fileName=${encodeURI(fileName)}&delete=${true}`; } diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts index 9c1d327d..32e7d7d4 100644 --- a/react-ui/src/utils/format.ts +++ b/react-ui/src/utils/format.ts @@ -1,3 +1,4 @@ +import { BasicInfoLink } from '@/components/BasicInfo/types'; import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal'; import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; import { @@ -18,8 +19,13 @@ type SelectedCodeConfig = { show_value?: string; // 后端使用的 }; -// 格式化数据集数组 -export const formatDatasets = (datasets?: DatasetData[]) => { +/** + * 格式化数据集数组 + * + * @param datasets - 数据集数组 + * @return 基本信息链接对象数组 + */ +export const formatDatasets = (datasets?: DatasetData[]): BasicInfoLink[] | undefined => { if (!datasets || datasets.length === 0) { return undefined; } @@ -29,8 +35,13 @@ export const formatDatasets = (datasets?: DatasetData[]) => { })); }; -// 格式化数据集 -export const formatDataset = (dataset?: DatasetData) => { +/** + * 格式化数据集 + * + * @param dataset - 数据集 + * @return 基本信息链接对象 + */ +export const formatDataset = (dataset?: DatasetData): BasicInfoLink | undefined => { if (!dataset) { return undefined; } @@ -40,8 +51,13 @@ export const formatDataset = (dataset?: DatasetData) => { }; }; -// 格式化模型 -export const formatModel = (model: ModelData) => { +/** + * 格式化模型 + * + * @param model - 模型 + * @return 基本信息链接对象 + */ +export const formatModel = (model: ModelData): BasicInfoLink | undefined => { if (!model) { return undefined; } @@ -51,16 +67,28 @@ export const formatModel = (model: ModelData) => { }; }; -// 格式化镜像 -export const formatMirror = (mirror: ResourceSelectorResponse) => { +/** + * 格式化镜像 + * + * @param mirror - 选择的镜像 + * @return 镜像地址 + */ +export const formatMirror = (mirror: ResourceSelectorResponse): string | undefined => { if (!mirror) { return undefined; } return mirror.path; }; -// 格式化代码配置 -export const formatCodeConfig = (project?: ProjectDependency | SelectedCodeConfig) => { +/** + * 格式化代码配置 + * + * @param project - 代码配置或者选择的代码配置 + * @return 基本信息链接对象 + */ +export const formatCodeConfig = ( + project?: ProjectDependency | SelectedCodeConfig, +): BasicInfoLink | undefined => { if (!project) { return undefined; } @@ -81,7 +109,12 @@ export const formatCodeConfig = (project?: ProjectDependency | SelectedCodeConfi } }; -// 格式化训练任务(实验实例) +/** + * 格式化训练任务(实验实例) + * + * @param task - 训练任务 + * @return 基本信息链接对象 + */ export const formatTrainTask = (task?: TrainTask) => { if (!task) { return undefined; @@ -92,8 +125,13 @@ export const formatTrainTask = (task?: TrainTask) => { }; }; -// 格式化数据来源 -export const formatSource = (source?: string) => { +/** + * 格式化数据来源 + * + * @param source - 数据来源枚举值 + * @return 数据来源中文名称 + */ +export const formatSource = (source?: string): string | undefined => { if (source === DataSource.Create) { return '用户上传'; } else if (source === DataSource.HandExport) { @@ -106,7 +144,12 @@ export const formatSource = (source?: string) => { return source; }; -// 格式化字符串数组,以逗号分隔 +/** + * 格式化字符串数组,以逗号分隔 + * + * @param value - 字符串数组 + * @return 字符串,以逗号分隔 + */ export const formatList = (value: string[] | null | undefined): string => { if ( value === undefined || @@ -119,14 +162,24 @@ export const formatList = (value: string[] | null | undefined): string => { return value.join(','); }; -// 格式化布尔值 +/** + * 格式化布尔值 + * + * @param value - 布尔值 + * @return true 为 "是",false 为 "否" + */ export const formatBoolean = (value: boolean): string => { return value ? '是' : '否'; }; type FormatEnumFunc = (value: string | number) => React.ReactNode; -// 格式化枚举 +/** + * 格式化枚举 + * + * @param options - 枚举选项数组 + * @return 一个函数,参数是枚举值,从选项数组中找到对应的项,然后返回该项的 label + */ export const formatEnum = ( options: { value?: string | number | null; label?: React.ReactNode }[], ): FormatEnumFunc => { diff --git a/react-ui/src/utils/localStorage.ts b/react-ui/src/utils/localStorage.ts index 4d224465..bb583ba7 100644 --- a/react-ui/src/utils/localStorage.ts +++ b/react-ui/src/utils/localStorage.ts @@ -6,6 +6,11 @@ export default class LocalStorage { // 记住密码 static readonly rememberPasswordKey = 'login-remember-password'; + /** + * 获取 LocalStorage 值 + * @param key - LocalStorage key + * @param isObject - 是不是对象 + */ static getItem(key: string, isObject: boolean = false) { const jsonStr = localStorage.getItem(key); if (!isObject) { @@ -17,12 +22,22 @@ export default class LocalStorage { return null; } + /** + * 设置 LocalStorage 值 + * @param key - LocalStorage key + * @param state - SessionStorage state + * @param isObject - 是不是对象 + */ static setItem(key: string, state?: any, isObject: boolean = false) { if (state) { localStorage.setItem(key, isObject ? JSON.stringify(state) : state); } } + /** + * 移除 LocalStorage 值 + * @param key - LocalStorage key + */ static removeItem(key: string) { localStorage.removeItem(key); } diff --git a/react-ui/src/utils/promise.ts b/react-ui/src/utils/promise.ts index 1919ecd1..7340e3bf 100644 --- a/react-ui/src/utils/promise.ts +++ b/react-ui/src/utils/promise.ts @@ -1,6 +1,7 @@ /** - * @param { Promise } promise - * @return { Promise } + * 封装 Promise,不会抛异常,resolve 的时候返回 [data, null], reject 的时候返回 [null, error] + * @param promise + * @return resolve 的时候返回 [data, null], reject 的时候返回 [null, error] */ export async function to(promise: Promise): Promise<[T, null] | [null, U]> { try { diff --git a/react-ui/src/utils/sessionStorage.ts b/react-ui/src/utils/sessionStorage.ts index 4a41e214..a230197c 100644 --- a/react-ui/src/utils/sessionStorage.ts +++ b/react-ui/src/utils/sessionStorage.ts @@ -1,15 +1,22 @@ import { parseJsonText } from './index'; export default class SessionStorage { - // 用于新建镜像 + /** 用于新建镜像 */ static readonly mirrorNameKey = 'mirror-name'; - // 模型部署服务版本 + /** 模型部署服务版本 */ static readonly serviceVersionInfoKey = 'service-version-info'; - // 编辑器 url + /** 编辑器 url */ static readonly editorUrlKey = 'editor-url'; - // 客户端信息 + /** 客户端信息 */ static readonly clientInfoKey = 'client-info'; + /** aim url */ + static readonly aimUrlKey = 'aim-url'; + /** + * 获取 SessionStorage 值 + * @param key - SessionStorage key + * @param isObject - 是不是对象 + */ static getItem(key: string, isObject: boolean = false) { const jsonStr = sessionStorage.getItem(key); if (!isObject) { @@ -21,12 +28,22 @@ export default class SessionStorage { return null; } + /** + * 设置 SessionStorage 值 + * @param key - SessionStorage key + * @param state - SessionStorage state + * @param isObject - 是不是对象 + */ static setItem(key: string, state?: any, isObject: boolean = false) { if (state) { sessionStorage.setItem(key, isObject ? JSON.stringify(state) : state); } } + /** + * 移除 SessionStorage 值 + * @param key - SessionStorage key + */ static removeItem(key: string) { sessionStorage.removeItem(key); } diff --git a/react-ui/src/utils/statusTableCell.tsx b/react-ui/src/utils/statusTableCell.tsx index 8efa5803..265aa9c4 100644 --- a/react-ui/src/utils/statusTableCell.tsx +++ b/react-ui/src/utils/statusTableCell.tsx @@ -10,6 +10,10 @@ export type StatusInfo = { color?: string; }; +/** + * 通用的 Table 状态单元格 + * @param infos - 选项数组 + */ function statusTableCell(infos: StatusInfo[]) { return function (status?: string | number | null) { const info = infos.find((item) => item.value === status); diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index a26b719f..ac66967f 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -10,27 +10,44 @@ import { Tooltip, TooltipProps, Typography } from 'antd'; import dayjs from 'dayjs'; export enum TableCellValueType { + /** 序号 */ Index = 'Index', + /** 文本 */ Text = 'Text', + /** 日期 */ Date = 'Date', + /** 数组 */ Array = 'Array', + /** 链接 */ Link = 'Link', + /** 自定义 */ Custom = 'Custom', } export type TableCellValueOptions = { - page?: number; // 类型为 Index 时有效 - pageSize?: number; // 类型为 Index 时有效 - property?: string; // 类型为 Array 时有效 - dateFormat?: string; // 类型为 Date 时有效 - onClick?: (record: T, e: React.MouseEvent) => void; // 类型为 Link 时有效 - format?: (value: any | undefined | null, record: T, index: number) => string | undefined | null; // 类型为 Custom 时有效 - copyable?: boolean; // 省略时是否可以复制 + /** 页数,类型为 Index 时有效 */ + page?: number; + /** 分页大小,类型为 Index 时有效 */ + pageSize?: number; + /** 取数组对象的哪个属性值,类型为 Array 时有效 */ + property?: string; + /** 日期格式,类型为 Date 时有效*/ + dateFormat?: string; + /** 链接点击回调,类型为 Link 时有效 */ + onClick?: (record: T, e: React.MouseEvent) => void; + /** 自定义函数,类型为 Custom 时有效*/ + format?: (value: any | undefined | null, record: T, index: number) => string | undefined | null; + /** 省略时是否可以复制 */ + copyable?: boolean; }; type TableCellFormatter = (value: any | undefined | null) => string | undefined | null; -// 日期转换函数 +/** + * 日期转换函数 + * @param {string | undefined} dateFormat - 日期格式 + * @returns {TableCellFormatter} Table cell 渲染函数 + */ function formatDateText(dateFormat?: string): TableCellFormatter { return (value: any | undefined | null): ReturnType => { if (value === undefined || value === null || value === '') { @@ -45,7 +62,7 @@ function formatDateText(dateFormat?: string): TableCellFormatter { /** * 数组转换函数,将数组元素转换为字符串,用逗号分隔 - * @param {string} property 如果数组元素是对象,那么取数组元素的某个属性 + * @param {string} property - 如果数组元素是对象,那么取数组元素的某个属性 * @returns {TableCellFormatter} Table cell 渲染函数 */ function formatArray(property?: string): TableCellFormatter { @@ -65,6 +82,13 @@ function formatArray(property?: string): TableCellFormatter { }; } +/** + * Table cell render 函数 + * @param ellipsis - 是否省略 + * @param type - 类型 + * @param options - 选项 + * @returns React 节点 + */ function tableCellRender( ellipsis: boolean | TooltipProps | 'auto' = false, type: TableCellValueType = TableCellValueType.Text, diff --git a/react-ui/src/utils/ui.tsx b/react-ui/src/utils/ui.tsx index df042bc9..5cd39e43 100644 --- a/react-ui/src/utils/ui.tsx +++ b/react-ui/src/utils/ui.tsx @@ -5,8 +5,10 @@ */ import { PageEnum } from '@/enums/pagesEnums'; import { removeAllPageCacheState } from '@/hooks/useCacheState'; +import { deleteUploadFileReq } from '@/services/dataset/index.js'; import themes from '@/styles/theme.less'; -import { type ClientInfo } from '@/types'; +import { type AppResponse, type ClientInfo, type UploadFileRes } from '@/types'; +import { to } from '@/utils/promise'; import { history } from '@umijs/max'; import { Modal, @@ -25,7 +27,9 @@ type ModalConfirmProps = ModalFuncProps & { isDelete?: boolean; }; -// 自定义删除 Confirm 弹框 +/** + * 自定义 Confirm 弹框 + */ export function modalConfirm({ title, content, @@ -45,7 +49,7 @@ export function modalConfirm({ src={ isDelete ? require('@/assets/img/delete-icon.png') - : require('@/assets/img/comfirm-icon.png') + : require('@/assets/img/confirm-icon.png') } style={{ width: '120px', marginBottom: '24px' }} draggable={false} @@ -63,7 +67,7 @@ export function modalConfirm({ /** * 跳转到登录页 - * @param toHome 是否跳转到首页 + * @param toHome - 是否跳转到首页 */ export const gotoLoginPage = (toHome: boolean = true) => { const { pathname, search } = location; @@ -80,6 +84,9 @@ export const gotoLoginPage = (toHome: boolean = true) => { } }; +/** + * 跳转到 OAuth2 登录页 + */ export const gotoOAuth2 = () => { const clientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true) as ClientInfo; if (clientInfo) { @@ -89,7 +96,10 @@ export const gotoOAuth2 = () => { } }; -// 从事件中获取上传文件列表,用于 Upload + Form 中 +/** + * 从事件中获取上传文件列表,用于 Upload + Form 中 + * @param e - 事件,包含文件列表 fileList + */ export const getFileListFromEvent = (e: any) => { const fileList: UploadFile[] = (Array.isArray(e) ? e : e?.fileList) || []; return fileList.map((item) => { @@ -137,7 +147,10 @@ export const validateUploadFiles = (files: UploadFile[], required: boolean = tru return !hasError; }; -// 限制上传文件类型 +/** + * 限制上传文件类型 + * @param type - 只允许上次的的文件类型 + */ export const limitUploadFileType = (type: string) => { return (file: UploadFile): boolean | string => { const acceptTypes = type.split(',').map((item) => item.trim()); @@ -151,14 +164,40 @@ export const limitUploadFileType = (type: string) => { }; }; +/** + * 删除已上传的文件 + * @param file - 已上传的文件 + */ +export const removeUploadedFile = async (file: UploadFile>) => { + const { status, response } = file; + const { code, data } = response ?? {}; + if (status === 'done' && code === 200 && Array.isArray(data) && data.length > 0) { + const uploadRes = data[0]; + const { fileName, url } = uploadRes; + const [res] = await to( + deleteUploadFileReq({ + fileName, + url, + }), + ); + if (res) { + return true; + } else { + return false; + } + } + + return true; +}; + /** * 删除 FormList 表单项,如果表单项没有值,则直接删除,否则弹出确认框 - * @param form From实例 - * @param listName FormList 的 name - * @param name FormList 的其中一项 - * @param remove FormList 的删除方法 - * @param fieldNames FormList 的子项名称数组 - * @param confirmTitle 弹出确认框的标题 + * @param form - From实例 + * @param listName - FormList 的 name + * @param name - FormList 的其中一项 + * @param remove - FormList 的删除方法 + * @param fieldNames - FormList 的子项名称数组 + * @param confirmTitle - 弹出确认框的标题 */ export const removeFormListItem = ( form: FormInstance, diff --git a/react-ui/typedoc.json b/react-ui/typedoc.json index 33384478..f9868f57 100644 --- a/react-ui/typedoc.json +++ b/react-ui/typedoc.json @@ -1,5 +1,5 @@ { - "entryPoints": ["./src/utils"], + "entryPoints": ["./src/utils", "./src/hooks"], "entryPointStrategy": "expand", "out": "docs", "excludePrivate": true, @@ -7,5 +7,14 @@ "excludeExternals": true, "includeVersion": true, "categorizeByGroup": true, + "skipErrorChecking": true, + "exclude": [ + "src/utils/formRules.ts", + "src/utils/loading.tsx", + "src/utils/menuRender.tsx", + "src/utils/IconUtil.ts", + "src/utils/permission.ts", + "src/utils/tree.ts" + ], "name": "工具类文档" }