feat: 完成模型演化
This commit is contained in:
parent
d00bc941c1
commit
53dc404113
|
@ -57,6 +57,7 @@
|
|||
"@ant-design/pro-components": "^2.4.4",
|
||||
"@ant-design/use-emotion-css": "1.0.4",
|
||||
"@antv/g6": "^4.8.24",
|
||||
"@antv/hierarchy": "^0.6.12",
|
||||
"@umijs/route-utils": "^4.0.1",
|
||||
"antd": "^5.4.4",
|
||||
"classnames": "^2.3.2",
|
||||
|
|
|
@ -2,13 +2,10 @@
|
|||
height: 100%;
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 110px;
|
||||
margin-bottom: 10px;
|
||||
padding: 25px 30px;
|
||||
padding: 20px 30px 0;
|
||||
background-image: url(/assets/images/dataset-back.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top center;
|
||||
|
@ -17,7 +14,7 @@
|
|||
&__name {
|
||||
margin-bottom: 12px;
|
||||
color: @text-color;
|
||||
font-size: 20;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
|
@ -36,6 +33,22 @@
|
|||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
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 {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
|
||||
import { to } from '@/utils/promise';
|
||||
import { useParams, useSearchParams } from '@umijs/max';
|
||||
import { Flex, Tabs } from 'antd';
|
||||
|
@ -12,14 +13,19 @@ type ResourceIntroProps = {
|
|||
|
||||
const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
|
||||
const [info, setInfo] = useState<ResourceData>({} as ResourceData);
|
||||
const locationParams = useParams(); //新版本获取路由参数接口
|
||||
const locationParams = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [versionList, setVersionList] = useState([]);
|
||||
const [version, setVersion] = useState<string | undefined>(undefined);
|
||||
const isPublic = searchParams.get('isPublic') === 'true';
|
||||
const defaultTab = searchParams.get('tab') || '1';
|
||||
let versionParam = searchParams.get('version');
|
||||
const resourceId = Number(locationParams.id);
|
||||
const name = resourceConfig[resourceType].name;
|
||||
const typeName = resourceConfig[resourceType].name; // 数据集/模型
|
||||
|
||||
useEffect(() => {
|
||||
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 = [
|
||||
{
|
||||
key: '1',
|
||||
label: `${name}简介`,
|
||||
label: `${typeName}简介`,
|
||||
children: (
|
||||
<>
|
||||
<div className={styles['resource-intro__title']}>简介</div>
|
||||
|
@ -44,18 +79,38 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
|
|||
},
|
||||
{
|
||||
key: '2',
|
||||
label: `${name}文件/版本`,
|
||||
label: `${typeName}文件/版本`,
|
||||
children: (
|
||||
<ResourceVersion
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
resourceName={info.name}
|
||||
isPublic={isPublic}
|
||||
versionList={versionList}
|
||||
version={version}
|
||||
getVersionList={getVersionList}
|
||||
onVersionChange={handleVersionChange}
|
||||
></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]
|
||||
.infoTypePropertyName as keyof ResourceData;
|
||||
const infoTagPropertyName = resourceConfig[resourceType]
|
||||
|
@ -64,21 +119,25 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
|
|||
return (
|
||||
<div className={styles['resource-intro']}>
|
||||
<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">
|
||||
<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>
|
||||
{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>
|
||||
</div>
|
||||
<div className={styles['resource-intro__bottom']}>
|
||||
<Tabs defaultActiveKey="1" items={items}></Tabs>
|
||||
<Tabs defaultActiveKey={defaultTab} items={items}></Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,14 +2,13 @@ import CommonTableCell from '@/components/CommonTableCell';
|
|||
import DateTableCell from '@/components/DateTableCell';
|
||||
import KFIcon from '@/components/KFIcon';
|
||||
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 { openAntdModal } from '@/utils/modal';
|
||||
import { to } from '@/utils/promise';
|
||||
import { modalConfirm } from '@/utils/ui';
|
||||
import { App, Button, Flex, Select, Table } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ResourceFileData, resourceConfig } from '../../config';
|
||||
import styles from './index.less';
|
||||
|
||||
type ResourceVersionProps = {
|
||||
|
@ -17,42 +16,32 @@ type ResourceVersionProps = {
|
|||
resourceId: number;
|
||||
resourceName: string;
|
||||
isPublic: boolean;
|
||||
versionList: { label: string; value: string }[];
|
||||
version?: string;
|
||||
getVersionList: () => void;
|
||||
onVersionChange: (version: string) => void;
|
||||
};
|
||||
function ResourceVersion({
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceName,
|
||||
isPublic,
|
||||
versionList,
|
||||
version,
|
||||
getVersionList,
|
||||
onVersionChange,
|
||||
}: ResourceVersionProps) {
|
||||
const [versionList, setVersionList] = useState([]);
|
||||
const [version, setVersion] = useState<string | undefined>(undefined);
|
||||
const [fileList, setFileList] = useState<ResourceFileData[]>([]);
|
||||
const { message } = App.useApp();
|
||||
|
||||
// 获取版本文件列表
|
||||
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 {
|
||||
setVersion(undefined);
|
||||
setFileList([]);
|
||||
}
|
||||
};
|
||||
}, [resourceId, version]);
|
||||
|
||||
// 获取版本下的文件列表
|
||||
const getFileList = async (version: string) => {
|
||||
|
@ -120,16 +109,6 @@ function ResourceVersion({
|
|||
downLoadZip(`${url}/${record.id}`);
|
||||
};
|
||||
|
||||
// 版本变化
|
||||
const handleChange = (value: string) => {
|
||||
if (value) {
|
||||
getFileList(value);
|
||||
setVersion(value);
|
||||
} else {
|
||||
setVersion(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
|
@ -194,8 +173,7 @@ function ResourceVersion({
|
|||
placeholder="请选择版本号"
|
||||
style={{ width: '160px', marginRight: '20px' }}
|
||||
value={version}
|
||||
allowClear
|
||||
onChange={handleChange}
|
||||
onChange={onVersionChange}
|
||||
options={versionList}
|
||||
/>
|
||||
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>
|
||||
|
|
|
@ -16,7 +16,7 @@ function DatasetAnnotation() {
|
|||
};
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
flex: none;
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
|
||||
|
@ -35,14 +36,15 @@
|
|||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
color: #575757;
|
||||
color: @text-color-secondary;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
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 {
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -75,6 +75,7 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => {
|
|||
return (
|
||||
<div className={Styles.collapse}>
|
||||
<div className={Styles.modelMenusTitle}>组件库</div>
|
||||
{/* 这样 defaultActiveKey 才能生效 */}
|
||||
{modelMenusList.length > 0 ? (
|
||||
<Collapse
|
||||
collapsible="header"
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
background-color: #fff;
|
||||
|
||||
&__workflow {
|
||||
flex: 1;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
|
||||
&__top {
|
||||
|
@ -12,15 +13,15 @@
|
|||
align-items: center;
|
||||
justify-content: end;
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
padding: 0 30px;
|
||||
height: 52px;
|
||||
padding: 0 20px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
|
||||
}
|
||||
|
||||
&__graph {
|
||||
width: 100%;
|
||||
height: calc(100% - 45px);
|
||||
height: calc(100% - 52px);
|
||||
background-color: @background-color;
|
||||
background-image: url(/assets/images/pipeline-canvas-back.png);
|
||||
background-size: 100% 100%;
|
||||
|
|
|
@ -148,14 +148,14 @@ function QuickStart() {
|
|||
x={left + 2 * (192 + space) + 56}
|
||||
y={139}
|
||||
width={taskLeftArrowWidth}
|
||||
height={125}
|
||||
height={120}
|
||||
arrowLeft={taskLeftArrowWidth}
|
||||
arrorwTop={-4}
|
||||
borderLeft={1}
|
||||
borderTop={1}
|
||||
/>
|
||||
<WorkArrow
|
||||
x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 6}
|
||||
x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4}
|
||||
y={127}
|
||||
width={taskRightArrowWidth}
|
||||
height={156}
|
||||
|
|
|
@ -42,6 +42,7 @@ export const requestConfig: RequestConfig = {
|
|||
message.error('请重新登录');
|
||||
return Promise.reject(response);
|
||||
} else {
|
||||
console.log(message, data);
|
||||
message.error(data?.msg ?? '请求失败');
|
||||
return Promise.reject(response);
|
||||
}
|
||||
|
|
|
@ -130,3 +130,11 @@ export function deleteDataset(id) {
|
|||
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
|
||||
};
|
||||
|
||||
// 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<
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* @Description: 工具类
|
||||
*/
|
||||
|
||||
import G6 from '@antv/g6';
|
||||
|
||||
// 生成 8 位随机数
|
||||
export function s8() {
|
||||
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>) {
|
||||
if (!isPlainObject(obj)) {
|
||||
return obj;
|
||||
}
|
||||
const newObj: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
|
@ -38,7 +54,9 @@ export function underscoreToCamelCase(obj: Record<string, any>) {
|
|||
return $1.toUpperCase().replace('[-_]', '').replace('_', '');
|
||||
});
|
||||
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);
|
||||
}
|
||||
newObj[newKey] = value;
|
||||
|
@ -47,14 +65,19 @@ export function underscoreToCamelCase(obj: Record<string, any>) {
|
|||
return newObj;
|
||||
}
|
||||
|
||||
// camelCase-to-underscore
|
||||
// camelCase to underscore
|
||||
export function camelCaseToUnderscore(obj: Record<string, any>) {
|
||||
if (!isPlainObject(obj)) {
|
||||
return obj;
|
||||
}
|
||||
const newObj: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||
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);
|
||||
}
|
||||
newObj[newKey] = value;
|
||||
|
@ -63,15 +86,20 @@ export function camelCaseToUnderscore(obj: Record<string, any>) {
|
|||
return newObj;
|
||||
}
|
||||
|
||||
// null 转 undefined
|
||||
// null to undefined
|
||||
export function nullToUndefined(obj: Record<string, any>) {
|
||||
if (!isPlainObject(obj)) {
|
||||
return obj;
|
||||
}
|
||||
const newObj: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const value = obj[key];
|
||||
if (value === null) {
|
||||
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);
|
||||
} else {
|
||||
newObj[key] = value;
|
||||
|
@ -80,3 +108,62 @@ export function nullToUndefined(obj: Record<string, any>) {
|
|||
}
|
||||
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 公共方法
|
||||
*/
|
||||
import { PageEnum } from '@/enums/pagesEnums';
|
||||
import { removeAllPageCacheState } from '@/hooks/pageCacheState';
|
||||
import themes from '@/styles/theme.less';
|
||||
import { history } from '@umijs/max';
|
||||
import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd';
|
||||
|
@ -60,6 +61,7 @@ export const gotoLoginPage = (toHome: boolean = true) => {
|
|||
console.log('search', search);
|
||||
if (window.location.pathname !== PageEnum.LOGIN) {
|
||||
closeAllModals();
|
||||
removeAllPageCacheState();
|
||||
history.replace({
|
||||
pathname: PageEnum.LOGIN,
|
||||
search: newSearch,
|
||||
|
|
Loading…
Reference in New Issue