| @@ -57,6 +57,7 @@ | |||||
| "@ant-design/pro-components": "^2.4.4", | "@ant-design/pro-components": "^2.4.4", | ||||
| "@ant-design/use-emotion-css": "1.0.4", | "@ant-design/use-emotion-css": "1.0.4", | ||||
| "@antv/g6": "^4.8.24", | "@antv/g6": "^4.8.24", | ||||
| "@antv/hierarchy": "^0.6.12", | |||||
| "@umijs/route-utils": "^4.0.1", | "@umijs/route-utils": "^4.0.1", | ||||
| "antd": "^5.4.4", | "antd": "^5.4.4", | ||||
| "classnames": "^2.3.2", | "classnames": "^2.3.2", | ||||
| @@ -2,13 +2,10 @@ | |||||
| height: 100%; | height: 100%; | ||||
| &__top { | &__top { | ||||
| display: flex; | |||||
| flex-direction: column; | |||||
| justify-content: space-between; | |||||
| width: 100%; | width: 100%; | ||||
| height: 110px; | height: 110px; | ||||
| margin-bottom: 10px; | margin-bottom: 10px; | ||||
| padding: 25px 30px; | |||||
| padding: 20px 30px 0; | |||||
| background-image: url(/assets/images/dataset-back.png); | background-image: url(/assets/images/dataset-back.png); | ||||
| background-repeat: no-repeat; | background-repeat: no-repeat; | ||||
| background-position: top center; | background-position: top center; | ||||
| @@ -17,7 +14,7 @@ | |||||
| &__name { | &__name { | ||||
| margin-bottom: 12px; | margin-bottom: 12px; | ||||
| color: @text-color; | color: @text-color; | ||||
| font-size: 20; | |||||
| font-size: 20px; | |||||
| } | } | ||||
| &__tag { | &__tag { | ||||
| @@ -36,6 +33,22 @@ | |||||
| background: #ffffff; | background: #ffffff; | ||||
| border-radius: 10px; | border-radius: 10px; | ||||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | ||||
| :global { | |||||
| .ant-tabs { | |||||
| height: 100%; | |||||
| .ant-tabs-content-holder { | |||||
| height: 100%; | |||||
| .ant-tabs-content { | |||||
| height: 100%; | |||||
| .ant-tabs-tabpane { | |||||
| height: 100%; | |||||
| overflow-y: auto; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||
| &__title { | &__title { | ||||
| @@ -1,3 +1,4 @@ | |||||
| import ModelEvolution from '@/pages/Model/components/ModelEvolution'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useParams, useSearchParams } from '@umijs/max'; | import { useParams, useSearchParams } from '@umijs/max'; | ||||
| import { Flex, Tabs } from 'antd'; | import { Flex, Tabs } from 'antd'; | ||||
| @@ -12,14 +13,19 @@ type ResourceIntroProps = { | |||||
| const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | ||||
| const [info, setInfo] = useState<ResourceData>({} as ResourceData); | const [info, setInfo] = useState<ResourceData>({} as ResourceData); | ||||
| const locationParams = useParams(); //新版本获取路由参数接口 | |||||
| const locationParams = useParams(); | |||||
| const [searchParams] = useSearchParams(); | const [searchParams] = useSearchParams(); | ||||
| const [versionList, setVersionList] = useState([]); | |||||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||||
| const isPublic = searchParams.get('isPublic') === 'true'; | const isPublic = searchParams.get('isPublic') === 'true'; | ||||
| const defaultTab = searchParams.get('tab') || '1'; | |||||
| let versionParam = searchParams.get('version'); | |||||
| const resourceId = Number(locationParams.id); | const resourceId = Number(locationParams.id); | ||||
| const name = resourceConfig[resourceType].name; | |||||
| const typeName = resourceConfig[resourceType].name; // 数据集/模型 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| getModelByDetail(); | getModelByDetail(); | ||||
| getVersionList(); | |||||
| }, []); | }, []); | ||||
| // 获取详情 | // 获取详情 | ||||
| @@ -31,10 +37,39 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||||
| } | } | ||||
| }; | }; | ||||
| // 获取版本列表 | |||||
| const getVersionList = async () => { | |||||
| const request = resourceConfig[resourceType].getVersions; | |||||
| const [res] = await to(request(resourceId)); | |||||
| if (res && res.data && res.data.length > 0) { | |||||
| setVersionList( | |||||
| res.data.map((item: string) => { | |||||
| return { | |||||
| label: item, | |||||
| value: item, | |||||
| }; | |||||
| }), | |||||
| ); | |||||
| if (versionParam) { | |||||
| setVersion(versionParam); | |||||
| versionParam = null; | |||||
| } else { | |||||
| setVersion(res.data[0]); | |||||
| } | |||||
| } else { | |||||
| setVersion(undefined); | |||||
| } | |||||
| }; | |||||
| // 版本变化 | |||||
| const handleVersionChange = (value: string) => { | |||||
| setVersion(value); | |||||
| }; | |||||
| const items = [ | const items = [ | ||||
| { | { | ||||
| key: '1', | key: '1', | ||||
| label: `${name}简介`, | |||||
| label: `${typeName}简介`, | |||||
| children: ( | children: ( | ||||
| <> | <> | ||||
| <div className={styles['resource-intro__title']}>简介</div> | <div className={styles['resource-intro__title']}>简介</div> | ||||
| @@ -44,18 +79,38 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||||
| }, | }, | ||||
| { | { | ||||
| key: '2', | key: '2', | ||||
| label: `${name}文件/版本`, | |||||
| label: `${typeName}文件/版本`, | |||||
| children: ( | children: ( | ||||
| <ResourceVersion | <ResourceVersion | ||||
| resourceType={resourceType} | resourceType={resourceType} | ||||
| resourceId={resourceId} | resourceId={resourceId} | ||||
| resourceName={info.name} | resourceName={info.name} | ||||
| isPublic={isPublic} | isPublic={isPublic} | ||||
| versionList={versionList} | |||||
| version={version} | |||||
| getVersionList={getVersionList} | |||||
| onVersionChange={handleVersionChange} | |||||
| ></ResourceVersion> | ></ResourceVersion> | ||||
| ), | ), | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| if (resourceType === ResourceType.Model) { | |||||
| items.push({ | |||||
| key: '3', | |||||
| label: `模型演化`, | |||||
| children: ( | |||||
| <ModelEvolution | |||||
| resourceId={resourceId} | |||||
| resourceName={info.name} | |||||
| versionList={versionList} | |||||
| version={version} | |||||
| onVersionChange={handleVersionChange} | |||||
| ></ModelEvolution> | |||||
| ), | |||||
| }); | |||||
| } | |||||
| const infoTypePropertyName = resourceConfig[resourceType] | const infoTypePropertyName = resourceConfig[resourceType] | ||||
| .infoTypePropertyName as keyof ResourceData; | .infoTypePropertyName as keyof ResourceData; | ||||
| const infoTagPropertyName = resourceConfig[resourceType] | const infoTagPropertyName = resourceConfig[resourceType] | ||||
| @@ -64,21 +119,25 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||||
| return ( | return ( | ||||
| <div className={styles['resource-intro']}> | <div className={styles['resource-intro']}> | ||||
| <div className={styles['resource-intro__top']}> | <div className={styles['resource-intro__top']}> | ||||
| <span className={styles['resource-intro__top__name']}>{info.name}</span> | |||||
| <div className={styles['resource-intro__top__name']}>{info.name}</div> | |||||
| <Flex align="center"> | <Flex align="center"> | ||||
| <div className={styles['resource-intro__top__tag']}> | <div className={styles['resource-intro__top__tag']}> | ||||
| {name} id:{info.id} | |||||
| </div> | |||||
| <div className={styles['resource-intro__top__tag']}> | |||||
| {info[infoTypePropertyName] || '--'} | |||||
| </div> | |||||
| <div className={styles['resource-intro__top__tag']}> | |||||
| {info[infoTagPropertyName] || '--'} | |||||
| {typeName} id:{info.id} | |||||
| </div> | </div> | ||||
| {info[infoTypePropertyName] && ( | |||||
| <div className={styles['resource-intro__top__tag']}> | |||||
| {info[infoTypePropertyName] || '--'} | |||||
| </div> | |||||
| )} | |||||
| {info[infoTagPropertyName] && ( | |||||
| <div className={styles['resource-intro__top__tag']}> | |||||
| {info[infoTagPropertyName] || '--'} | |||||
| </div> | |||||
| )} | |||||
| </Flex> | </Flex> | ||||
| </div> | </div> | ||||
| <div className={styles['resource-intro__bottom']}> | <div className={styles['resource-intro__bottom']}> | ||||
| <Tabs defaultActiveKey="1" items={items}></Tabs> | |||||
| <Tabs defaultActiveKey={defaultTab} items={items}></Tabs> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -2,14 +2,13 @@ import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import DateTableCell from '@/components/DateTableCell'; | import DateTableCell from '@/components/DateTableCell'; | ||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import AddVersionModal from '@/pages/Dataset/components/AddVersionModal'; | import AddVersionModal from '@/pages/Dataset/components/AddVersionModal'; | ||||
| import { ResourceType } from '@/pages/Dataset/config'; | |||||
| import { ResourceFileData, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||||
| import { downLoadZip } from '@/utils/downloadfile'; | import { downLoadZip } from '@/utils/downloadfile'; | ||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { App, Button, Flex, Select, Table } from 'antd'; | import { App, Button, Flex, Select, Table } from 'antd'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import { ResourceFileData, resourceConfig } from '../../config'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ResourceVersionProps = { | type ResourceVersionProps = { | ||||
| @@ -17,42 +16,32 @@ type ResourceVersionProps = { | |||||
| resourceId: number; | resourceId: number; | ||||
| resourceName: string; | resourceName: string; | ||||
| isPublic: boolean; | isPublic: boolean; | ||||
| versionList: { label: string; value: string }[]; | |||||
| version?: string; | |||||
| getVersionList: () => void; | |||||
| onVersionChange: (version: string) => void; | |||||
| }; | }; | ||||
| function ResourceVersion({ | function ResourceVersion({ | ||||
| resourceType, | resourceType, | ||||
| resourceId, | resourceId, | ||||
| resourceName, | resourceName, | ||||
| isPublic, | isPublic, | ||||
| versionList, | |||||
| version, | |||||
| getVersionList, | |||||
| onVersionChange, | |||||
| }: ResourceVersionProps) { | }: ResourceVersionProps) { | ||||
| const [versionList, setVersionList] = useState([]); | |||||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||||
| const [fileList, setFileList] = useState<ResourceFileData[]>([]); | const [fileList, setFileList] = useState<ResourceFileData[]>([]); | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| // 获取版本文件列表 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| getVersionList(); | |||||
| }, []); | |||||
| // 获取版本列表 | |||||
| const getVersionList = async () => { | |||||
| const request = resourceConfig[resourceType].getVersions; | |||||
| const [res] = await to(request(resourceId)); | |||||
| if (res && res.data && res.data.length > 0) { | |||||
| setVersionList( | |||||
| res.data.map((item: string) => { | |||||
| return { | |||||
| label: item, | |||||
| value: item, | |||||
| }; | |||||
| }), | |||||
| ); | |||||
| setVersion(res.data[0]); | |||||
| getFileList(res.data[0]); | |||||
| if (version) { | |||||
| getFileList(version); | |||||
| } else { | } else { | ||||
| setVersion(undefined); | |||||
| setFileList([]); | setFileList([]); | ||||
| } | } | ||||
| }; | |||||
| }, [resourceId, version]); | |||||
| // 获取版本下的文件列表 | // 获取版本下的文件列表 | ||||
| const getFileList = async (version: string) => { | const getFileList = async (version: string) => { | ||||
| @@ -120,16 +109,6 @@ function ResourceVersion({ | |||||
| downLoadZip(`${url}/${record.id}`); | downLoadZip(`${url}/${record.id}`); | ||||
| }; | }; | ||||
| // 版本变化 | |||||
| const handleChange = (value: string) => { | |||||
| if (value) { | |||||
| getFileList(value); | |||||
| setVersion(value); | |||||
| } else { | |||||
| setVersion(undefined); | |||||
| } | |||||
| }; | |||||
| const columns = [ | const columns = [ | ||||
| { | { | ||||
| title: '序号', | title: '序号', | ||||
| @@ -194,8 +173,7 @@ function ResourceVersion({ | |||||
| placeholder="请选择版本号" | placeholder="请选择版本号" | ||||
| style={{ width: '160px', marginRight: '20px' }} | style={{ width: '160px', marginRight: '20px' }} | ||||
| value={version} | value={version} | ||||
| allowClear | |||||
| onChange={handleChange} | |||||
| onChange={onVersionChange} | |||||
| options={versionList} | options={versionList} | ||||
| /> | /> | ||||
| <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | ||||
| @@ -16,7 +16,7 @@ function DatasetAnnotation() { | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <div className={styles.container}> | <div className={styles.container}> | ||||
| <iframe src="http://172.20.32.181:31213/label-studio" className={styles.frame}></iframe> | |||||
| <iframe src={iframeUrl} className={styles.frame}></iframe> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -0,0 +1,17 @@ | |||||
| .graph-legend { | |||||
| &__item { | |||||
| margin-right: 20px; | |||||
| color: @text-color; | |||||
| font-size: @font-size-content; | |||||
| &:last-child { | |||||
| margin-right: 0; | |||||
| } | |||||
| &__name { | |||||
| margin-left: 10px; | |||||
| color: @text-color-secondary; | |||||
| font-size: @font-size-content; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,55 @@ | |||||
| import { Flex } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| type GraphLegandData = { | |||||
| name: string; | |||||
| color: string; | |||||
| radius: number; | |||||
| fill: boolean; | |||||
| }; | |||||
| type GraphLegandProps = { | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| function GraphLegand({ style }: GraphLegandProps) { | |||||
| const legends: GraphLegandData[] = [ | |||||
| { | |||||
| name: '父模型', | |||||
| color: '#76b1ff', | |||||
| radius: 2, | |||||
| fill: true, | |||||
| }, | |||||
| { | |||||
| name: '当前模型', | |||||
| color: '#1664ff', | |||||
| radius: 2, | |||||
| fill: true, | |||||
| }, | |||||
| { | |||||
| name: '衍生模型', | |||||
| color: '#b7cfff', | |||||
| radius: 2, | |||||
| fill: true, | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <Flex align="center" className={styles['graph-legend']} style={style}> | |||||
| {legends.map((item) => ( | |||||
| <Flex align="center" key={item.name} className={styles['graph-legend__item']}> | |||||
| <div | |||||
| style={{ | |||||
| width: '16px', | |||||
| height: '12px', | |||||
| borderRadius: item.radius, | |||||
| backgroundColor: item.color, | |||||
| }} | |||||
| ></div> | |||||
| <div className={styles['graph-legend__item__name']}>{item.name}</div> | |||||
| </Flex> | |||||
| ))} | |||||
| </Flex> | |||||
| ); | |||||
| } | |||||
| export default GraphLegand; | |||||
| @@ -0,0 +1,18 @@ | |||||
| .model-evolution { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| background-color: white; | |||||
| &__top { | |||||
| padding: 30px 0; | |||||
| color: @text-color; | |||||
| font-size: @font-size-content; | |||||
| } | |||||
| &__graph { | |||||
| height: calc(100% - 92px); | |||||
| background-color: @background-color; | |||||
| background-image: url(/assets/images/pipeline-canvas-back.png); | |||||
| background-size: 100% 100%; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,484 @@ | |||||
| import { getModelAtlasReq } from '@/services/dataset/index.js'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import { changePropertyName, fittingString } from '@/utils'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import G6, { | |||||
| EdgeConfig, | |||||
| G6GraphEvent, | |||||
| Graph, | |||||
| GraphData, | |||||
| LayoutConfig, | |||||
| NodeConfig, | |||||
| TreeGraphData, | |||||
| Util, | |||||
| } from '@antv/g6'; | |||||
| // @ts-ignore | |||||
| import Hierarchy from '@antv/hierarchy'; | |||||
| import { Flex, Select } from 'antd'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import GraphLegand from '../GraphLegand'; | |||||
| import NodeTooltips from '../NodeTooltips'; | |||||
| import styles from './index.less'; | |||||
| const nodeWidth = 98; | |||||
| const nodeHeight = 58; | |||||
| const vGap = 30; | |||||
| const hGap = 30; | |||||
| enum NodeType { | |||||
| current = 'current', | |||||
| parent = 'parent', | |||||
| children = 'children', | |||||
| project = 'project', | |||||
| trainDataset = 'trainDataset', | |||||
| testDataset = 'testDataset', | |||||
| } | |||||
| type TrainTask = { | |||||
| ins_id: number; | |||||
| name: string; | |||||
| task_id: string; | |||||
| }; | |||||
| interface TrainDataset extends NodeConfig { | |||||
| dataset_id: number; | |||||
| dataset_name: string; | |||||
| dataset_version: string; | |||||
| model_type: NodeType; | |||||
| } | |||||
| interface ProjectDependency extends NodeConfig { | |||||
| url: string; | |||||
| name: string; | |||||
| branch: string; | |||||
| model_type: NodeType; | |||||
| } | |||||
| export interface ModelDepsAPIData { | |||||
| current_model_id: number; | |||||
| version: string; | |||||
| exp_ins_id: number; | |||||
| model_type: NodeType; | |||||
| current_model_name: string; | |||||
| project_dependency: ProjectDependency; | |||||
| test_dataset: TrainDataset[]; | |||||
| train_dataset: TrainDataset[]; | |||||
| train_task: TrainTask; | |||||
| children_models: ModelDepsAPIData[]; | |||||
| parent_models: ModelDepsAPIData[]; | |||||
| } | |||||
| interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData { | |||||
| children: ModelDepsData[]; | |||||
| } | |||||
| // 规范化子数据 | |||||
| function normalizeChildren(data: ModelDepsData[]) { | |||||
| if (Array.isArray(data)) { | |||||
| data.forEach((item) => { | |||||
| item.id = `$M_${item.current_model_id}_${item.version}`; | |||||
| item.label = getLabel(item); | |||||
| item.style = getStyle(NodeType.children); | |||||
| item.model_type = NodeType.children; | |||||
| normalizeChildren(item.children); | |||||
| }); | |||||
| } | |||||
| } | |||||
| // 获取 label | |||||
| function getLabel(node: { current_model_name: string; version: string }) { | |||||
| return ( | |||||
| fittingString(`${node.current_model_name}`, 87, 8) + | |||||
| '\n' + | |||||
| fittingString(`${node.version}`, 87, 8) | |||||
| ); | |||||
| } | |||||
| // 获取 style | |||||
| function getStyle(model_type: NodeType) { | |||||
| let fill = ''; | |||||
| switch (model_type) { | |||||
| case NodeType.current: | |||||
| fill = '#1664ff'; | |||||
| break; | |||||
| case NodeType.parent: | |||||
| fill = '#76b1ff'; | |||||
| break; | |||||
| case NodeType.children: | |||||
| fill = '#b7cfff'; | |||||
| break; | |||||
| case NodeType.project: | |||||
| fill = '#0000ff'; | |||||
| break; | |||||
| case NodeType.trainDataset: | |||||
| fill = '#ff0000'; | |||||
| break; | |||||
| case NodeType.testDataset: | |||||
| fill = '#ff00ff'; | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| return { | |||||
| fill, | |||||
| }; | |||||
| } | |||||
| // 将后台返回的数据转换成树形数据 | |||||
| function normalizeTreeData(apiData: ModelDepsAPIData, currentNodeName: string): ModelDepsData { | |||||
| // 将 children_models 转换成 children | |||||
| let normalizedData = changePropertyName(apiData, { | |||||
| children_models: 'children', | |||||
| }) as ModelDepsData; | |||||
| // 设置当前模型的数据 | |||||
| normalizedData.label = getLabel(normalizedData); | |||||
| normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`; | |||||
| normalizedData.style = getStyle(NodeType.current); | |||||
| normalizedData.model_type = NodeType.current; | |||||
| normalizedData.current_model_name = currentNodeName; | |||||
| normalizeChildren(normalizedData.children as ModelDepsData[]); | |||||
| // 将 parent_models 转换成树形结构 | |||||
| let parent_models = normalizedData.parent_models || []; | |||||
| while (parent_models.length > 0) { | |||||
| const parent = parent_models[0]; | |||||
| normalizedData = { | |||||
| ...parent, | |||||
| id: `$M_${parent.current_model_id}_${parent.version}`, | |||||
| model_type: NodeType.parent, | |||||
| label: getLabel(parent), | |||||
| style: getStyle(NodeType.parent), | |||||
| children: [ | |||||
| { | |||||
| ...normalizedData, | |||||
| parent_models: [], | |||||
| }, | |||||
| ], | |||||
| }; | |||||
| parent_models = normalizedData.parent_models || []; | |||||
| } | |||||
| return normalizedData; | |||||
| } | |||||
| // 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据 | |||||
| function getGraphData(data: ModelDepsData): GraphData { | |||||
| const config = { | |||||
| direction: 'LR', | |||||
| getHeight: () => nodeHeight, | |||||
| getWidth: () => nodeWidth, | |||||
| getVGap: () => vGap, | |||||
| getHGap: () => hGap, | |||||
| }; | |||||
| // 树形布局计算出坐标 | |||||
| const treeLayoutData: LayoutConfig = Hierarchy['compactBox'](data, config); | |||||
| const nodes: NodeConfig[] = []; | |||||
| const edges: EdgeConfig[] = []; | |||||
| Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => { | |||||
| const data = node.data as ModelDepsData; | |||||
| nodes.push({ | |||||
| ...data, | |||||
| x: node.x, | |||||
| y: node.y, | |||||
| }); | |||||
| if (parent) { | |||||
| edges.push({ | |||||
| source: parent.id, | |||||
| target: node.id, | |||||
| }); | |||||
| } | |||||
| // 当前模型显示数据集和项目 | |||||
| if (data.model_type === NodeType.current) { | |||||
| const { project_dependency, train_dataset, test_dataset } = data; | |||||
| train_dataset.forEach((item) => { | |||||
| item.id = `$DTrain_${item.dataset_id}`; | |||||
| item.model_type = NodeType.trainDataset; | |||||
| item.type = 'ellipse'; | |||||
| item.label = fittingString(`${item.dataset_name}`, 87, 8); | |||||
| item.style = getStyle(NodeType.trainDataset); | |||||
| }); | |||||
| test_dataset.forEach((item) => { | |||||
| item.id = `$DTest_${item.dataset_id}`; | |||||
| item.model_type = NodeType.testDataset; | |||||
| item.type = 'ellipse'; | |||||
| item.label = fittingString(item.dataset_name, 87, 8); | |||||
| item.style = getStyle(NodeType.testDataset); | |||||
| }); | |||||
| const len = train_dataset.length + test_dataset.length; | |||||
| [...train_dataset, ...test_dataset].forEach((item, index) => { | |||||
| const half = len / 2 - 0.5; | |||||
| item.x = node.x! - (half - index) * (nodeWidth + 30); | |||||
| item.y = node.y! - nodeHeight - 30; | |||||
| nodes.push(item); | |||||
| edges.push({ | |||||
| source: node.id, | |||||
| target: item.id, | |||||
| sourceAnchor: 2, | |||||
| targetAnchor: 3, | |||||
| type: 'cubic-vertical', | |||||
| }); | |||||
| }); | |||||
| if (project_dependency.url) { | |||||
| project_dependency.id = `$P_${project_dependency.url}`; | |||||
| project_dependency.model_type = NodeType.project; | |||||
| project_dependency.type = 'rect'; | |||||
| project_dependency.size = [nodeHeight, nodeHeight]; | |||||
| project_dependency.label = fittingString(project_dependency.name, 48, 8); | |||||
| project_dependency.style = getStyle(NodeType.project); | |||||
| project_dependency.x = node.x; | |||||
| project_dependency.y = node.y! + nodeHeight + 30; | |||||
| nodes.push(project_dependency); | |||||
| edges.push({ | |||||
| source: node.id, | |||||
| target: project_dependency.id, | |||||
| sourceAnchor: 3, | |||||
| targetAnchor: 2, | |||||
| type: 'cubic-vertical', | |||||
| }); | |||||
| } | |||||
| } | |||||
| }); | |||||
| return { nodes, edges }; | |||||
| } | |||||
| type modeModelEvolutionProps = { | |||||
| resourceId: number; | |||||
| resourceName: string; | |||||
| versionList: { label: string; value: string }[]; | |||||
| version?: string; | |||||
| onVersionChange: (version: string) => void; | |||||
| }; | |||||
| let graph: Graph; | |||||
| function ModelEvolution({ | |||||
| resourceId, | |||||
| resourceName, | |||||
| versionList, | |||||
| version, | |||||
| onVersionChange, | |||||
| }: modeModelEvolutionProps) { | |||||
| const graphRef = useRef<HTMLDivElement>(null); | |||||
| const [showNodeTooltip, setShowNodeTooltip] = useState(false); | |||||
| const [enterTooltip, setEnterTooltip] = useState(false); | |||||
| const [nodeTooltipX, setNodeToolTipX] = useState(0); | |||||
| const [nodeTooltipY, setNodeToolTipY] = useState(0); | |||||
| const [hoverNodeData, setHoverNodeData] = useState<ModelDepsData | undefined>(undefined); | |||||
| useEffect(() => { | |||||
| initGraph(); | |||||
| const changSize = () => { | |||||
| if (!graph || graph.get('destroyed')) return; | |||||
| if (!graphRef.current) return; | |||||
| graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); | |||||
| graph.fitView(); | |||||
| }; | |||||
| window.addEventListener('resize', changSize); | |||||
| return () => { | |||||
| window.removeEventListener('resize', changSize); | |||||
| }; | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| if (version) { | |||||
| getModelAtlas(); | |||||
| } else { | |||||
| graph.data({ | |||||
| nodes: [], | |||||
| edges: [], | |||||
| }); | |||||
| graph.render(); | |||||
| graph.fitView(); | |||||
| } | |||||
| }, [resourceId, version]); | |||||
| // 初始化图 | |||||
| const initGraph = () => { | |||||
| graph = new G6.Graph({ | |||||
| container: graphRef.current!, | |||||
| width: graphRef.current!.clientWidth, | |||||
| height: graphRef.current!.clientHeight, | |||||
| // animate: false, | |||||
| fitView: true, | |||||
| fitViewPadding: [50, 100, 50, 100], | |||||
| minZoom: 0.5, | |||||
| maxZoom: 5, | |||||
| defaultNode: { | |||||
| type: 'rect', | |||||
| size: [nodeWidth, nodeHeight], | |||||
| anchorPoints: [ | |||||
| [0, 0.5], | |||||
| [1, 0.5], | |||||
| [0.5, 0], | |||||
| [0.5, 1], | |||||
| ], | |||||
| style: { | |||||
| fill: themes['primaryColor'], | |||||
| lineWidth: 0, | |||||
| radius: 6, | |||||
| cursor: 'pointer', | |||||
| }, | |||||
| labelCfg: { | |||||
| position: 'center', | |||||
| style: { | |||||
| fill: '#ffffff', | |||||
| fontSize: 8, | |||||
| textAlign: 'center', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| defaultEdge: { | |||||
| type: 'cubic-horizontal', | |||||
| labelCfg: { | |||||
| autoRotate: true, | |||||
| }, | |||||
| style: { | |||||
| stroke: '#a2c1ff', | |||||
| lineWidth: 1, | |||||
| }, | |||||
| }, | |||||
| modes: { | |||||
| default: [ | |||||
| 'drag-canvas', | |||||
| 'zoom-canvas', | |||||
| // { | |||||
| // type: 'collapse-expand', | |||||
| // onChange(item?: Item, collapsed?: boolean) { | |||||
| // const data = item!.getModel(); | |||||
| // data.collapsed = collapsed; | |||||
| // return true; | |||||
| // }, | |||||
| // }, | |||||
| ], | |||||
| }, | |||||
| }); | |||||
| bindEvents(); | |||||
| }; | |||||
| // 绑定事件 | |||||
| const bindEvents = () => { | |||||
| graph.on('node:mouseenter', (e: G6GraphEvent) => { | |||||
| const nodeItem = e.item; | |||||
| graph.setItemState(nodeItem, 'hover', true); | |||||
| const model = nodeItem.getModel() as ModelDepsData; | |||||
| const { x, y, model_type } = model; | |||||
| if ( | |||||
| model_type === NodeType.project || | |||||
| model_type === NodeType.trainDataset || | |||||
| model_type === NodeType.testDataset | |||||
| ) { | |||||
| return; | |||||
| } | |||||
| const point = graph.getCanvasByPoint(x!, y!); | |||||
| const canvasWidth = graphRef.current!.clientWidth; | |||||
| if (point.x + 300 > canvasWidth) { | |||||
| point.x = canvasWidth - 300; | |||||
| } | |||||
| setHoverNodeData(model as ModelDepsData); | |||||
| setNodeToolTipX(point.x); | |||||
| setNodeToolTipY(point.y - 240); | |||||
| setShowNodeTooltip(true); | |||||
| }); | |||||
| graph.on('node:mouseleave', (e: G6GraphEvent) => { | |||||
| const nodeItem = e.item; | |||||
| graph.setItemState(nodeItem, 'hover', false); | |||||
| setShowNodeTooltip(false); | |||||
| }); | |||||
| graph.on('node:click', (e: G6GraphEvent) => { | |||||
| const nodeItem = e.item; | |||||
| const model = nodeItem.getModel() as ModelDepsChildren; | |||||
| const { model_type } = model; | |||||
| const { origin } = location; | |||||
| let url: string = ''; | |||||
| switch (model_type) { | |||||
| case NodeType.children: | |||||
| case NodeType.current: | |||||
| case NodeType.parent: { | |||||
| const { current_model_id, version } = model as ModelDepsData; | |||||
| url = `${origin}/dataset/model/${current_model_id}?isPublic=true&tab=3&version=${version}`; | |||||
| break; | |||||
| } | |||||
| case NodeType.project: { | |||||
| const { url: projectUrl } = model as ProjectDependency; | |||||
| url = projectUrl; | |||||
| break; | |||||
| } | |||||
| case NodeType.trainDataset: | |||||
| case NodeType.testDataset: { | |||||
| const { dataset_id, dataset_version } = model as TrainDataset; | |||||
| url = `${origin}/dataset/dataset/${dataset_id}?isPublic=true&tab=2&version=${dataset_version}`; | |||||
| break; | |||||
| } | |||||
| default: | |||||
| break; | |||||
| } | |||||
| if (url) { | |||||
| window.open(url, '_blank'); | |||||
| } | |||||
| }); | |||||
| }; | |||||
| const handleTooltipsMouseEnter = () => { | |||||
| setEnterTooltip(true); | |||||
| }; | |||||
| const handleTooltipsMouseLeave = () => { | |||||
| setEnterTooltip(false); | |||||
| }; | |||||
| // 获取模型依赖 | |||||
| const getModelAtlas = async () => { | |||||
| const params = { | |||||
| model_id: resourceId, | |||||
| version, | |||||
| }; | |||||
| const [res] = await to(getModelAtlasReq(params)); | |||||
| if (res && res.data) { | |||||
| const data = normalizeTreeData(res.data, resourceName); | |||||
| const graphData = getGraphData(data); | |||||
| graph.data(graphData); | |||||
| graph.render(); | |||||
| graph.fitView(); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <div className={styles['model-evolution']}> | |||||
| <Flex align="center" className={styles['model-evolution__top']}> | |||||
| <span style={{ marginRight: '10px' }}>版本号:</span> | |||||
| <Select | |||||
| placeholder="请选择版本号" | |||||
| style={{ width: '160px', marginRight: '20px' }} | |||||
| value={version} | |||||
| allowClear | |||||
| onChange={onVersionChange} | |||||
| options={versionList} | |||||
| /> | |||||
| <GraphLegand style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegand> | |||||
| </Flex> | |||||
| <div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div> | |||||
| {(showNodeTooltip || enterTooltip) && ( | |||||
| <NodeTooltips | |||||
| x={nodeTooltipX} | |||||
| y={nodeTooltipY} | |||||
| data={hoverNodeData!} | |||||
| onMouseEnter={handleTooltipsMouseEnter} | |||||
| onMouseLeave={handleTooltipsMouseLeave} | |||||
| /> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ModelEvolution; | |||||
| @@ -0,0 +1,56 @@ | |||||
| .node-tooltips { | |||||
| position: absolute; | |||||
| top: -100px; | |||||
| left: -300px; | |||||
| width: 300px; | |||||
| padding: 10px; | |||||
| background: white; | |||||
| border: 1px solid #eaeaea; | |||||
| border-radius: 4px; | |||||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||||
| &__title { | |||||
| margin: 10px 0; | |||||
| color: @text-color; | |||||
| font-weight: 500; | |||||
| font-size: @font-size-content; | |||||
| } | |||||
| &__row { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| margin: 4px 0; | |||||
| color: @text-color; | |||||
| font-size: 14px; | |||||
| &:first-child { | |||||
| margin-top: 0; | |||||
| } | |||||
| &:last-child { | |||||
| margin-bottom: 10px; | |||||
| } | |||||
| &__title { | |||||
| display: inline-block; | |||||
| width: 100px; | |||||
| color: @text-color-secondary; | |||||
| text-align: right; | |||||
| } | |||||
| &__value { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| color: @text-color; | |||||
| font-weight: 500; | |||||
| .singleLine(); | |||||
| } | |||||
| &__link { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| font-weight: 500; | |||||
| .singleLine(); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,73 @@ | |||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { useEffect } from 'react'; | |||||
| import { ModelDepsData } from '../ModelEvolution'; | |||||
| import styles from './index.less'; | |||||
| type NodeTooltipsProps = { | |||||
| data: ModelDepsData; | |||||
| x: number; | |||||
| y: number; | |||||
| onMouseEnter?: () => void; | |||||
| onMouseLeave?: () => void; | |||||
| }; | |||||
| function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) { | |||||
| const navigate = useNavigate(); | |||||
| useEffect(() => {}, []); | |||||
| const gotoExperimentPage = () => { | |||||
| if (data.train_task?.ins_id) { | |||||
| navigate(`/pipeline/experiment/144/${data.train_task.ins_id}`); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <div | |||||
| className={styles['node-tooltips']} | |||||
| style={{ left: `${x}px`, top: `${y}px` }} | |||||
| onMouseEnter={onMouseEnter} | |||||
| onMouseLeave={onMouseLeave} | |||||
| > | |||||
| <div className={styles['node-tooltips__title']}>模型信息</div> | |||||
| <div> | |||||
| <div className={styles['node-tooltips__row']}> | |||||
| <span className={styles['node-tooltips__row__title']}>模型名称:</span> | |||||
| <span className={styles['node-tooltips__row__value']}>{data.current_model_name}</span> | |||||
| </div> | |||||
| <div className={styles['node-tooltips__row']}> | |||||
| <span className={styles['node-tooltips__row__title']}>版本:</span> | |||||
| <span className={styles['node-tooltips__row__value']}>{data.version}</span> | |||||
| </div> | |||||
| <div className={styles['node-tooltips__row']}> | |||||
| <span className={styles['node-tooltips__row__title']}>模型框架:</span> | |||||
| <span className={styles['node-tooltips__row__value']}>{data.version}</span> | |||||
| </div> | |||||
| <div className={styles['node-tooltips__row']}> | |||||
| <span className={styles['node-tooltips__row__title']}>模型大小:</span> | |||||
| <span className={styles['node-tooltips__row__value']}>{data.version}</span> | |||||
| </div> | |||||
| <div className={styles['node-tooltips__row']}> | |||||
| <span className={styles['node-tooltips__row__title']}>创建时间:</span> | |||||
| <span className={styles['node-tooltips__row__value']}>{data.version}</span> | |||||
| </div> | |||||
| <div className={styles['node-tooltips__row']}> | |||||
| <span className={styles['node-tooltips__row__title']}>模型权限:</span> | |||||
| <span className={styles['node-tooltips__row__value']}>{data.version}</span> | |||||
| </div> | |||||
| </div> | |||||
| <div className={styles['node-tooltips__title']}>训练相关信息</div> | |||||
| <div> | |||||
| <div className={styles['node-tooltips__row']}> | |||||
| <span className={styles['node-tooltips__row__title']}>训练任务:</span> | |||||
| {data.train_task?.name ? ( | |||||
| <a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}> | |||||
| {data.train_task?.name} | |||||
| </a> | |||||
| ) : null} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default NodeTooltips; | |||||
| @@ -1,4 +1,5 @@ | |||||
| .collapse { | .collapse { | ||||
| flex: none; | |||||
| width: 250px; | width: 250px; | ||||
| height: 100%; | height: 100%; | ||||
| @@ -35,14 +36,15 @@ | |||||
| align-items: center; | align-items: center; | ||||
| height: 40px; | height: 40px; | ||||
| padding: 0 16px; | padding: 0 16px; | ||||
| color: #575757; | |||||
| color: @text-color-secondary; | |||||
| font-size: 14px; | font-size: 14px; | ||||
| border-radius: 4px; | border-radius: 4px; | ||||
| cursor: pointer; | cursor: pointer; | ||||
| } | |||||
| .collapseItem:hover { | |||||
| color: #1664ff; | |||||
| background: rgba(22, 100, 255, 0.08); | |||||
| &:hover { | |||||
| color: @primary-color; | |||||
| background: rgba(22, 100, 255, 0.08); | |||||
| } | |||||
| } | } | ||||
| .modelMenusTitle { | .modelMenusTitle { | ||||
| margin-bottom: 10px; | margin-bottom: 10px; | ||||
| @@ -75,6 +75,7 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { | |||||
| return ( | return ( | ||||
| <div className={Styles.collapse}> | <div className={Styles.collapse}> | ||||
| <div className={Styles.modelMenusTitle}>组件库</div> | <div className={Styles.modelMenusTitle}>组件库</div> | ||||
| {/* 这样 defaultActiveKey 才能生效 */} | |||||
| {modelMenusList.length > 0 ? ( | {modelMenusList.length > 0 ? ( | ||||
| <Collapse | <Collapse | ||||
| collapsible="header" | collapsible="header" | ||||
| @@ -4,7 +4,8 @@ | |||||
| background-color: #fff; | background-color: #fff; | ||||
| &__workflow { | &__workflow { | ||||
| flex: 1; | |||||
| flex: 1 1 0; | |||||
| min-width: 0; | |||||
| height: 100%; | height: 100%; | ||||
| &__top { | &__top { | ||||
| @@ -12,15 +13,15 @@ | |||||
| align-items: center; | align-items: center; | ||||
| justify-content: end; | justify-content: end; | ||||
| width: 100%; | width: 100%; | ||||
| height: 45px; | |||||
| padding: 0 30px; | |||||
| height: 52px; | |||||
| padding: 0 20px; | |||||
| background: #ffffff; | background: #ffffff; | ||||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | ||||
| } | } | ||||
| &__graph { | &__graph { | ||||
| width: 100%; | width: 100%; | ||||
| height: calc(100% - 45px); | |||||
| height: calc(100% - 52px); | |||||
| background-color: @background-color; | background-color: @background-color; | ||||
| background-image: url(/assets/images/pipeline-canvas-back.png); | background-image: url(/assets/images/pipeline-canvas-back.png); | ||||
| background-size: 100% 100%; | background-size: 100% 100%; | ||||
| @@ -148,14 +148,14 @@ function QuickStart() { | |||||
| x={left + 2 * (192 + space) + 56} | x={left + 2 * (192 + space) + 56} | ||||
| y={139} | y={139} | ||||
| width={taskLeftArrowWidth} | width={taskLeftArrowWidth} | ||||
| height={125} | |||||
| height={120} | |||||
| arrowLeft={taskLeftArrowWidth} | arrowLeft={taskLeftArrowWidth} | ||||
| arrorwTop={-4} | arrorwTop={-4} | ||||
| borderLeft={1} | borderLeft={1} | ||||
| borderTop={1} | borderTop={1} | ||||
| /> | /> | ||||
| <WorkArrow | <WorkArrow | ||||
| x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 6} | |||||
| x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4} | |||||
| y={127} | y={127} | ||||
| width={taskRightArrowWidth} | width={taskRightArrowWidth} | ||||
| height={156} | height={156} | ||||
| @@ -42,6 +42,7 @@ export const requestConfig: RequestConfig = { | |||||
| message.error('请重新登录'); | message.error('请重新登录'); | ||||
| return Promise.reject(response); | return Promise.reject(response); | ||||
| } else { | } else { | ||||
| console.log(message, data); | |||||
| message.error(data?.msg ?? '请求失败'); | message.error(data?.msg ?? '请求失败'); | ||||
| return Promise.reject(response); | return Promise.reject(response); | ||||
| } | } | ||||
| @@ -130,3 +130,11 @@ export function deleteDataset(id) { | |||||
| method: 'DELETE', | method: 'DELETE', | ||||
| }); | }); | ||||
| } | } | ||||
| // 获取模型依赖 | |||||
| export function getModelAtlasReq(data) { | |||||
| return request(`/api/mmp/modelDependency/queryModelAtlas`, { | |||||
| method: 'POST', | |||||
| data | |||||
| }); | |||||
| } | |||||
| @@ -62,7 +62,12 @@ export type PipelineNodeModelParameter = { | |||||
| checkedKeys?: string[]; // ResourceSelectorModal checkedKeys | checkedKeys?: string[]; // ResourceSelectorModal checkedKeys | ||||
| }; | }; | ||||
| // type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType } | |||||
| // 修改属性类型 | |||||
| export type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType }; | |||||
| // export type PascalCaseType<T> = { | |||||
| // [K in keyof T as `${Capitalize<string & K>}`]: T[K]; | |||||
| // } | |||||
| // 序列化后的流水线节点 | // 序列化后的流水线节点 | ||||
| export type PipelineNodeModelSerialize = Omit< | export type PipelineNodeModelSerialize = Omit< | ||||
| @@ -4,6 +4,8 @@ | |||||
| * @Description: 工具类 | * @Description: 工具类 | ||||
| */ | */ | ||||
| import G6 from '@antv/g6'; | |||||
| // 生成 8 位随机数 | // 生成 8 位随机数 | ||||
| export function s8() { | export function s8() { | ||||
| return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); | return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); | ||||
| @@ -29,8 +31,22 @@ export function parseJsonText(text?: string | null): any | null { | |||||
| } | } | ||||
| } | } | ||||
| // underscore-to-camelCase | |||||
| // 判断是否为对象 | |||||
| function isPlainObject(value: any) { | |||||
| if (value === null || typeof value !== 'object') return false; | |||||
| let proto = Object.getPrototypeOf(value); | |||||
| while (proto !== null) { | |||||
| if (proto.constructor && proto.constructor !== Object) return false; | |||||
| proto = Object.getPrototypeOf(proto); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| // underscore to camelCase | |||||
| export function underscoreToCamelCase(obj: Record<string, any>) { | export function underscoreToCamelCase(obj: Record<string, any>) { | ||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | const newObj: Record<string, any> = {}; | ||||
| for (const key in obj) { | for (const key in obj) { | ||||
| if (obj.hasOwnProperty(key)) { | if (obj.hasOwnProperty(key)) { | ||||
| @@ -38,7 +54,9 @@ export function underscoreToCamelCase(obj: Record<string, any>) { | |||||
| return $1.toUpperCase().replace('[-_]', '').replace('_', ''); | return $1.toUpperCase().replace('[-_]', '').replace('_', ''); | ||||
| }); | }); | ||||
| let value = obj[key]; | let value = obj[key]; | ||||
| if (typeof value === 'object' && value !== null) { | |||||
| if (Array.isArray(value)) { | |||||
| value = value.map((item) => underscoreToCamelCase(item)); | |||||
| } else if (isPlainObject(value)) { | |||||
| value = underscoreToCamelCase(value); | value = underscoreToCamelCase(value); | ||||
| } | } | ||||
| newObj[newKey] = value; | newObj[newKey] = value; | ||||
| @@ -47,14 +65,19 @@ export function underscoreToCamelCase(obj: Record<string, any>) { | |||||
| return newObj; | return newObj; | ||||
| } | } | ||||
| // camelCase-to-underscore | |||||
| // camelCase to underscore | |||||
| export function camelCaseToUnderscore(obj: Record<string, any>) { | export function camelCaseToUnderscore(obj: Record<string, any>) { | ||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | const newObj: Record<string, any> = {}; | ||||
| for (const key in obj) { | for (const key in obj) { | ||||
| if (obj.hasOwnProperty(key)) { | if (obj.hasOwnProperty(key)) { | ||||
| const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); | const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); | ||||
| let value = obj[key]; | let value = obj[key]; | ||||
| if (typeof value === 'object' && value !== null) { | |||||
| if (Array.isArray(value)) { | |||||
| value = value.map((item) => camelCaseToUnderscore(item)); | |||||
| } else if (isPlainObject(value)) { | |||||
| value = camelCaseToUnderscore(value); | value = camelCaseToUnderscore(value); | ||||
| } | } | ||||
| newObj[newKey] = value; | newObj[newKey] = value; | ||||
| @@ -63,15 +86,20 @@ export function camelCaseToUnderscore(obj: Record<string, any>) { | |||||
| return newObj; | return newObj; | ||||
| } | } | ||||
| // null 转 undefined | |||||
| // null to undefined | |||||
| export function nullToUndefined(obj: Record<string, any>) { | export function nullToUndefined(obj: Record<string, any>) { | ||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | const newObj: Record<string, any> = {}; | ||||
| for (const key in obj) { | for (const key in obj) { | ||||
| if (obj.hasOwnProperty(key)) { | if (obj.hasOwnProperty(key)) { | ||||
| const value = obj[key]; | const value = obj[key]; | ||||
| if (value === null) { | if (value === null) { | ||||
| newObj[key] = undefined; | newObj[key] = undefined; | ||||
| } else if (typeof value === 'object' && value !== null) { | |||||
| } else if (Array.isArray(value)) { | |||||
| newObj[key] = value.map((item) => nullToUndefined(item)); | |||||
| } else if (isPlainObject(value)) { | |||||
| newObj[key] = nullToUndefined(value); | newObj[key] = nullToUndefined(value); | ||||
| } else { | } else { | ||||
| newObj[key] = value; | newObj[key] = value; | ||||
| @@ -80,3 +108,62 @@ export function nullToUndefined(obj: Record<string, any>) { | |||||
| } | } | ||||
| return newObj; | return newObj; | ||||
| } | } | ||||
| /** | |||||
| * Changes the property names of an object based on a mapping provided. | |||||
| * | |||||
| * @param obj - The object whose property names need to be changed. | |||||
| * @param mapping - The mapping of old property names to new property names. | |||||
| * @return The object with the changed property names. | |||||
| */ | |||||
| export function changePropertyName(obj: Record<string, any>, mapping: Record<string, string>) { | |||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | |||||
| for (const key in obj) { | |||||
| if (obj.hasOwnProperty(key)) { | |||||
| let value = obj[key]; | |||||
| const newKey = mapping.hasOwnProperty(key) ? mapping[key] : key; | |||||
| if (Array.isArray(value)) { | |||||
| value = value.map((item) => changePropertyName(item, mapping)); | |||||
| } else if (isPlainObject(value)) { | |||||
| value = changePropertyName(value, mapping); | |||||
| } | |||||
| newObj[newKey] = value; | |||||
| } | |||||
| } | |||||
| return newObj; | |||||
| } | |||||
| /** | |||||
| * 计算显示的字符串 | |||||
| * @param tr 要裁剪的字符串 | |||||
| * @param maxWidth 最大宽度 | |||||
| * @param fontSize 字体大小 | |||||
| * @return 处理后的字符串 | |||||
| */ | |||||
| export const fittingString = (str: string, maxWidth: number, fontSize: number) => { | |||||
| if (!str) { | |||||
| return ''; | |||||
| } | |||||
| const ellipsis = '...'; | |||||
| const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0]; | |||||
| let currentWidth = 0; | |||||
| let res = str; | |||||
| const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters | |||||
| str.split('').forEach((letter, i) => { | |||||
| if (currentWidth > maxWidth - ellipsisLength) return; | |||||
| if (pattern.test(letter)) { | |||||
| // Chinese charactors | |||||
| currentWidth += fontSize; | |||||
| } else { | |||||
| // get the width of single letter according to the fontSize | |||||
| currentWidth += G6.Util.getLetterWidth(letter, fontSize); | |||||
| } | |||||
| if (currentWidth > maxWidth - ellipsisLength) { | |||||
| res = `${str.substring(0, i)}${ellipsis}`; | |||||
| } | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| @@ -4,6 +4,7 @@ | |||||
| * @Description: UI 公共方法 | * @Description: UI 公共方法 | ||||
| */ | */ | ||||
| import { PageEnum } from '@/enums/pagesEnums'; | import { PageEnum } from '@/enums/pagesEnums'; | ||||
| import { removeAllPageCacheState } from '@/hooks/pageCacheState'; | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { history } from '@umijs/max'; | import { history } from '@umijs/max'; | ||||
| import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd'; | import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd'; | ||||
| @@ -60,6 +61,7 @@ export const gotoLoginPage = (toHome: boolean = true) => { | |||||
| console.log('search', search); | console.log('search', search); | ||||
| if (window.location.pathname !== PageEnum.LOGIN) { | if (window.location.pathname !== PageEnum.LOGIN) { | ||||
| closeAllModals(); | closeAllModals(); | ||||
| removeAllPageCacheState(); | |||||
| history.replace({ | history.replace({ | ||||
| pathname: PageEnum.LOGIN, | pathname: PageEnum.LOGIN, | ||||
| search: newSearch, | search: newSearch, | ||||