feat(projects): sync add antv g6 example (#11)

This commit is contained in:
一寸灰
2024-12-05 17:33:12 +08:00
committed by GitHub
parent 5e50a05d78
commit bed44e39a0
16 changed files with 1263 additions and 52 deletions

View File

@@ -51,6 +51,7 @@
"dependencies": {
"@antv/data-set": "0.11.8",
"@antv/g2": "5.2.5",
"@antv/g6": "^5.0.26",
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@sa/alova": "workspace:*",
@@ -59,6 +60,7 @@
"@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*",
"@sa/utils": "workspace:*",
"@vueuse/components": "^11.1.0",
"@vueuse/core": "11.1.0",
"clipboard": "2.0.11",
"dayjs": "1.11.13",

735
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,6 @@ function refresh() {
</script>
<template>
<!-- <NSpace :align="itemAlign" wrap justify="end" class="lt-sm:w-200px"> -->
<ElSpace direction="horizontal" wrap justify="end" class="lt-sm:w-200px">
<slot name="prefix"></slot>
<slot name="default">

View File

@@ -204,6 +204,7 @@ const local: App.I18n.Schema = {
plugin_copy: 'Copy',
plugin_charts: 'Charts',
plugin_charts_echarts: 'ECharts',
plugin_charts_antv: 'AntV',
plugin_editor: 'Editor',
plugin_editor_quill: 'Quill',
plugin_editor_markdown: 'Markdown',

View File

@@ -204,6 +204,7 @@ const local: App.I18n.Schema = {
plugin_copy: '剪贴板',
plugin_charts: '图表',
plugin_charts_echarts: 'ECharts',
plugin_charts_antv: 'AntV',
plugin_editor: '编辑器',
plugin_editor_quill: '富文本编辑器',
plugin_editor_markdown: 'MD 编辑器',

View File

@@ -40,6 +40,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"multi-menu_first_child": () => import("@/views/multi-menu/first_child/index.vue"),
"multi-menu_second_child_home": () => import("@/views/multi-menu/second_child_home/index.vue"),
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
plugin_charts_echarts: () => import("@/views/plugin/charts/echarts/index.vue"),
plugin_copy: () => import("@/views/plugin/copy/index.vue"),
plugin_editor_markdown: () => import("@/views/plugin/editor/markdown/index.vue"),

View File

@@ -405,6 +405,15 @@ export const generatedRoutes: GeneratedRoute[] = [
icon: 'mdi:chart-areaspline'
},
children: [
{
name: 'plugin_charts_antv',
path: '/plugin/charts/antv',
component: 'view.plugin_charts_antv',
meta: {
title: 'plugin_charts_antv',
i18nKey: 'route.plugin_charts_antv'
}
},
{
name: 'plugin_charts_echarts',
path: '/plugin/charts/echarts',

View File

@@ -211,6 +211,7 @@ const routeMap: RouteMap = {
"plugin": "/plugin",
"plugin_barcode": "/plugin/barcode",
"plugin_charts": "/plugin/charts",
"plugin_charts_antv": "/plugin/charts/antv",
"plugin_charts_echarts": "/plugin/charts/echarts",
"plugin_copy": "/plugin/copy",
"plugin_editor": "/plugin/editor",

View File

@@ -72,10 +72,15 @@ declare module 'vue' {
IconCarbonPlay: typeof import('~icons/carbon/play')['default']
IconCarbonStop: typeof import('~icons/carbon/stop')['default']
'IconCharm:download': typeof import('~icons/charm/download')['default']
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
'IconF7:flagCircleFill': typeof import('~icons/f7/flag-circle-fill')['default']
'IconFe:question': typeof import('~icons/fe/question')['default']
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
'IconGg:ratio': typeof import('~icons/gg/ratio')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['default']
'IconIconParkOutline:equalRatio': typeof import('~icons/icon-park-outline/equal-ratio')['default']
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
@@ -93,6 +98,8 @@ declare module 'vue' {
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
'IconMingcute:zoomInLine': typeof import('~icons/mingcute/zoom-in-line')['default']
'IconMingcute:zoomOutLine': typeof import('~icons/mingcute/zoom-out-line')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']

View File

@@ -65,6 +65,7 @@ declare module "@elegant-router/types" {
"plugin": "/plugin";
"plugin_barcode": "/plugin/barcode";
"plugin_charts": "/plugin/charts";
"plugin_charts_antv": "/plugin/charts/antv";
"plugin_charts_echarts": "/plugin/charts/echarts";
"plugin_copy": "/plugin/copy";
"plugin_editor": "/plugin/editor";
@@ -181,6 +182,7 @@ declare module "@elegant-router/types" {
| "multi-menu_first_child"
| "multi-menu_second_child_home"
| "plugin_barcode"
| "plugin_charts_antv"
| "plugin_charts_echarts"
| "plugin_copy"
| "plugin_editor_markdown"

View File

@@ -0,0 +1,60 @@
import type { CustomGraphData } from './modules/types';
// 日期可以自己随便设置,就是字符串展示,也可以修改为业务需要的字段
export function getFlowData(): CustomGraphData {
return {
nodes: [
{
id: 'NS',
name: 'Start',
status: 'COMPLETED',
startDate: '2024-10-01',
endDate: '2024-10-07',
actualStartDate: '2024-10-01',
actualEndDate: '2024-10-07'
},
{
id: 'N1',
name: 'Node1',
status: 'COMPLETED_EARLY',
startDate: '2024-10-08',
endDate: '2024-10-10',
actualStartDate: '2024-10-08',
actualEndDate: '2024-10-09',
milestone: true
},
{
id: 'N2',
name: 'Node2',
status: 'COMPLETED_EARLY',
startDate: '2024-10-11',
endDate: '2024-10-13',
actualStartDate: '2024-10-11',
actualEndDate: '2024-10-12'
},
{ id: 'N3', name: 'Node3', status: 'IN_PROGRESS', isDeleted: true },
{ id: 'N4', name: 'Node4', status: 'COMPLETED_LATE' },
{ id: 'N5', name: 'Node5', status: 'DELAYED', isDelayed: true, milestone: true },
{ id: 'N6', name: 'Node6', status: 'PAUSED' },
{ id: 'N7', name: 'Node7', status: 'NOT_STARTED' },
{ id: 'N8', name: 'Node8', status: 'NOT_STARTED' },
{ id: 'N9', name: 'End', status: 'NOT_STARTED' },
{ id: 'NX', name: 'NodeX', status: 'NOT_STARTED', isDeleted: true }
],
edges: [
{ id: 'E1', source: 'NS', target: 'N1' },
{ id: 'E2', source: 'N1', target: 'N2' },
{ id: 'E3', source: 'N1', target: 'N3', isDeleted: true },
{ id: 'E4', source: 'N1', target: 'N4' },
{ id: 'E5', source: 'N2', target: 'N5' },
{ id: 'E6', source: 'N3', target: 'N5', isDeleted: true },
{ id: 'E7', source: 'N4', target: 'N5' },
{ id: 'E8', source: 'N5', target: 'N6' },
{ id: 'E9', source: 'N6', target: 'N7' },
{ id: 'E10', source: 'N6', target: 'N8' },
{ id: 'E11', source: 'N7', target: 'N9' },
{ id: 'EX', source: 'N8', target: 'N9' },
{ id: 'EO', source: 'N5', target: 'NX', isDeleted: true }
]
};
}

View File

@@ -0,0 +1,67 @@
<script setup lang="tsx">
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import type { Ref } from 'vue';
import type { CustomBehaviorOption, IPointerEvent } from '@antv/g6';
import AntvFlow from './modules/antv-flow.vue';
import type { CustomGraphData } from './modules/types';
import { getFlowData } from './data';
defineOptions({ name: 'AntVCharts' });
const antvFlowRef = useTemplateRef('antvFlowRef');
const flowData = ref({
nodes: [],
edges: []
}) as Ref<CustomGraphData>;
const selectedNode = ref<string | undefined>('N2');
const behaviors: CustomBehaviorOption[] = [
{
type: 'click-select',
enable: (event: IPointerEvent) => event.targetType === 'node',
onClick: (event: IPointerEvent) => {
const node = event.target as unknown as HTMLElement;
const nodeData = flowData.value.nodes.find(item => item.id === node.id);
selectedNode.value = nodeData?.id;
window.$message?.success(`选中节点:[${node.id}]${nodeData?.name}`);
}
}
];
const hasNodeN = computed(() => flowData.value.nodes.some(node => node.id === 'NN'));
function addNode() {
const { nodes, edges } = flowData.value;
nodes.push({ id: 'NN', name: 'New node', status: 'NOT_STARTED' });
edges.push({ id: 'EN', source: 'N5', target: 'NN' });
flowData.value = { nodes, edges };
}
function removeNode(id: string) {
const { nodes, edges } = flowData.value;
// 删除node的同时也需要删除包含NX的edge
flowData.value = {
nodes: nodes.filter(node => node.id !== id),
edges: edges.filter(edge => edge.source !== id && edge.target !== id)
};
}
onMounted(() => {
flowData.value = getFlowData();
});
</script>
<template>
<div class="h-full">
<ElCard header="AntV G6 Next" shadow="never" class="h-full card-wrapper">
<AntvFlow ref="antvFlowRef" :data="flowData" :selected="selectedNode" :behaviors="behaviors" />
<ElDivider />
<ElButton @click="selectedNode = 'N5'">选中节点N5(需要自行处理选中事件不会触发元素点击)</ElButton>
<ElButton v-if="!hasNodeN" @click="addNode">添加节点并与Node5连线</ElButton>
<ElButton v-else @click="() => removeNode('NN')">删除新添加的节点</ElButton>
<ElButton @click="() => removeNode('NX')">删除NodeX</ElButton>
</ElCard>
</div>
</template>

View File

@@ -0,0 +1,135 @@
<script setup lang="tsx">
import { shallowRef, useTemplateRef, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { vResizeObserver } from '@vueuse/components';
import type { CustomBehaviorOption, Graph } from '@antv/g6';
import { useAntFlow } from './antv-g6-flow';
import { nodeStatus } from './status';
import type { CustomGraphData } from './types';
defineOptions({ name: 'AntvFLow' });
interface Props {
behaviors?: CustomBehaviorOption[];
data: CustomGraphData;
selected?: string;
height?: string;
autoFit?: 'view' | 'center';
}
const props = defineProps<Props>();
const containerRef = useTemplateRef('containerRef');
const graphRef = shallowRef<Graph | null>(null);
// 监听容器尺寸变化,调整画布大小为图容器大小
const onContainerResize = useDebounceFn(() => {
if (graphRef.value) {
graphRef.value.resize();
}
}, 5);
async function draw() {
if (graphRef.value) {
graphRef.value.destroy();
}
const { graph } = useAntFlow({
container: 'antv-flow',
data: props.data,
behaviors: props.behaviors,
autoFit: props.autoFit
});
graphRef.value = graph;
await selectNode();
}
async function selectNode() {
if (props.selected && graphRef.value) {
try {
await graphRef.value.setElementState(props.selected, 'selected');
} catch {}
}
}
function zoomOut() {
graphRef.value?.zoomBy(0.9);
}
function zoomIn() {
graphRef.value?.zoomBy(1.1);
}
function resetZoom() {
graphRef.value?.zoomTo(1);
graphRef.value?.fitCenter();
}
function fitZoom() {
graphRef.value?.fitView();
graphRef.value?.fitCenter();
}
watch(
[() => props.data, () => props.selected],
() => {
draw();
},
{ deep: true }
);
defineExpose({ selectNode, graph: graphRef });
</script>
<template>
<div class="relative">
<!-- canvas toolbar -->
<div class="absolute left-0 right-0 z-1 flex items-center items-stretch justify-between">
<ElButtonGroup size="small" class="bg-white!">
<ElButton @click="zoomOut">
<icon-mingcute:zoom-out-line />
</ElButton>
<ElButton @click="zoomIn">
<icon-mingcute:zoom-in-line />
</ElButton>
<ElButton @click="resetZoom">
<icon-icon-park-outline:equal-ratio />
</ElButton>
<ElButton @click="fitZoom">
<icon-gg:ratio />
</ElButton>
</ElButtonGroup>
<div class="flex-center gap-12px">
<ElPopover placement="bottom-end" :width="200" :animated="false">
<template #reference>
<ElButton size="small" class="bg-white!">
<icon-fe:question />
</ElButton>
</template>
<div class="flex-col gap-8px">
<div span="2" class="text-12px font-bold">节点图例</div>
<ElRow>
<ElCol v-for="(config, status) in nodeStatus" :key="status" :span="12" class="mb-8px flex-center">
<ElTag size="small" round :bordered="false">
<template #default>
<icon-f7:flag-circle-fill v-if="status === 'MILESTONE'" :style="{ color: config.color }" />
<icon-f7:circle-fill v-else :style="{ color: config.color }" />
{{ config.type }}
</template>
</ElTag>
</ElCol>
</ElRow>
</div>
</ElPopover>
</div>
</div>
<!-- canvas container -->
<div
id="antv-flow"
ref="containerRef"
v-resize-observer="onContainerResize"
class="w-full"
:style="{ height: props.height || '300px' }"
@contextmenu="event => event.preventDefault()"
></div>
</div>
</template>

View File

@@ -0,0 +1,170 @@
import { Graph } from '@antv/g6';
import type { CustomBehaviorOption, IPointerEvent } from '@antv/g6';
import type { Canvas } from '@antv/g6/lib/runtime/canvas';
import { useThemeStore } from '@/store/modules/theme';
import { getNodeIcon, nodeStatus } from './status';
import type { CustomEdgeData, CustomGraphData, CustomNodeData } from './types';
interface AntFlowConfig {
container: string | HTMLElement | Canvas;
data: CustomGraphData;
behaviors?: CustomBehaviorOption[];
autoFit?: 'view' | 'center';
}
export function useAntFlow(config: AntFlowConfig) {
const themeStore = useThemeStore();
const baseColor = 'rgb(158 163 171)';
const { container, autoFit = 'center', data, behaviors = [] } = config;
const graph = new Graph({
container,
animation: false,
padding: 16,
theme: 'light',
autoFit,
data,
node: {
type: 'rect',
style: (node: CustomNodeData) => {
const iconS = getNodeIcon(node);
let labelFill = '#000000';
if (node.taskState === 'NOT_STARTED') {
labelFill = '#787878';
}
return {
labelText: node.name as string,
size: [120, 26],
radius: 99,
fill: '#FFFFFF',
stroke: node.isDeleted ? themeStore.otherColor.error : baseColor,
lineDash: node.isDeleted ? 4 : 0,
lineWidth: 1,
labelFill,
labelX: 2,
labelY: 2,
labelTextBaseline: 'middle',
labelTextAlign: 'center',
labelLineHeight: 13,
labelWordWrap: true,
labelMaxWidth: 72,
iconSrc: iconS,
iconWidth: 16,
iconHeight: 16,
iconX: -45,
labelFontSize: 12,
labelPlacement: 'center',
badgeLineWidth: 6,
badgeFontSize: 8,
badges: [
{ text: '延期', placement: 'top', offsetY: -11, visibility: node.isDelayed ? 'visible' : 'hidden' },
{ text: '已删除', placement: 'bottom', offsetY: 11, visibility: node.isDeleted ? 'visible' : 'hidden' }
],
badgePalette: [themeStore.otherColor.error, themeStore.otherColor.error],
ports: [{ placement: 'left' }, { placement: 'right' }]
};
},
state: {
selected: {
lineWidth: 2,
stroke: themeStore.themeColor,
labelFill: themeStore.themeColor,
halo: true,
haloStroke: themeStore.themeColor,
haloLineWidth: 6
},
active: (node: CustomNodeData) => ({
halo: true,
haloStroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
haloLineWidth: 6,
zIndex: 2
})
}
},
edge: {
type: 'cubic-horizontal',
style: (node: CustomEdgeData) => ({
curveOffset: 10,
curvePosition: 0.5,
stroke: node.isDeleted ? themeStore.otherColor.error : baseColor,
lineDash: node.isDeleted ? 4 : 0
}),
state: {
active: (node: CustomEdgeData) => ({
lineWidth: 2,
stroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
halo: true,
haloStroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
haloLineWidth: 6,
zIndex: 2
})
}
},
layout: {
type: 'antv-dagre',
rankdir: 'LR',
ranksep: 20,
nodesep: -20,
controlPoints: true
},
behaviors: [
{
key: 'hover-activate',
type: 'hover-activate',
degree: 1,
direction: 'both'
},
'drag-canvas',
...behaviors
],
plugins: [
{
type: 'tooltip',
enable: (event: IPointerEvent) => event.targetType === 'node',
getContent: (_event: IPointerEvent, items?: CustomNodeData[]) => {
let result = '<div style="display: flex; flex-direction: column; gap: 8px;">';
// 弹出提示可以自定义各种内容但是这里很奇怪有的class不跟随unocss的样式
items?.forEach(item => {
result += `
<h3 style="display: flex; align-items: center; gap: 8px;">${item.name}</h3>
<div style="display: flex;">
<b>状态:</b>
<div style="display: flex; gap: 4px;">
<img src="${getNodeIcon(item)}" />
<span style="font-weight: 400 !important;">${nodeStatus[item.status as keyof typeof nodeStatus].type}</span>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 32px; row-gap: 4px;">
<div style="display: flex; flex-direction: column;"><div style="color: rgb(156 163 175);">预计开始</div>
<div style="font-weight: 700;">${item.startDate || '-'}</div>
</div>
<div style="display: flex; flex-direction: column;">
<div style="color: rgb(156 163 175);">预计结束</div>
<div style="font-weight: 700;">${item.endDate || '-'}</div>
</div>
<div style="display: flex; flex-direction: column;">
<div style="color: rgb(156 163 175);">实际开始</div>
<div style="font-weight: 700;">${item.actualStartDate || '-'}</div>
</div>
<div style="display: flex; flex-direction: column;">
<div style="color: rgb(156 163 175);">实际结束</div>
<div style="font-weight: 700;">${item.actualEndDate || '-'}</div>
</div>
`;
});
result += '</div>';
return result;
}
}
]
});
graph.render();
return { graph };
}

View File

@@ -0,0 +1,95 @@
import { h } from 'vue';
import { ElTag } from 'element-plus';
import type { TagProps } from 'element-plus';
import type { CustomNodeData, NodeStatus } from './types';
interface NodeStatusConfig {
type: string;
color: string;
textColor: string;
base64: string;
flag64: string;
}
export const nodeStatus: Record<NodeStatus, NodeStatusConfig> = {
MILESTONE: {
type: '里程碑',
color: '#5b5b5b',
textColor: '',
base64: '',
flag64: ''
},
NOT_STARTED: {
type: '未开始',
color: '#CCCDD0',
textColor: '#5b5b5b',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0NERDAiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0NERDAiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
DELAYED: {
type: '已延期',
color: '#B81111',
textColor: '#dccbcb',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNCODExMTEiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNCODExMTEiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
PAUSED: {
type: '已暂停',
color: '#0E42D2',
textColor: '#dae0f0',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMwRTQyRDIiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMwRTQyRDIiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
IN_PROGRESS: {
type: '进行中',
color: '#E1BE0D',
textColor: '#4f4304',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNFMUJFMEQiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNFMUJFMEQiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
COMPLETED: {
type: '已完成',
color: '#33C73D',
textColor: '#084e0c',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMzM0M3M0QiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMzM0M3M0QiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
COMPLETED_EARLY: {
type: '提前完成',
color: '#CCFF99',
textColor: '#42681d',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0ZGOTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0ZGOTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
COMPLETED_LATE: {
type: '延期完成',
color: '#CC6699',
textColor: '#4b092a',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQzY2OTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQzY2OTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
}
};
export function getNodeIcon(node: CustomNodeData) {
if (!node.status) return '';
const type = node.milestone ? 'flag64' : 'base64';
return nodeStatus[node.status][type];
}
export function getNodeStatusTag(state: NodeStatus, tagProperty?: TagProps) {
const { color, type } = nodeStatus[state] || {};
return h(
ElTag,
{
color,
size: 'small',
...tagProperty
},
{
default: () => type
}
);
}

View File

@@ -0,0 +1,28 @@
import type { EdgeData, GraphData, NodeData } from '@antv/g6';
export type NodeStatus =
| 'MILESTONE'
| 'NOT_STARTED'
| 'DELAYED'
| 'PAUSED'
| 'IN_PROGRESS'
| 'COMPLETED'
| 'COMPLETED_EARLY'
| 'COMPLETED_LATE';
export interface CustomNodeData extends NodeData {
isDelayed?: boolean;
isDeleted?: boolean;
milestone?: boolean;
status?: NodeStatus;
}
export interface CustomEdgeData extends EdgeData {
isDelayed?: boolean;
isDeleted?: boolean;
}
export interface CustomGraphData extends GraphData {
nodes: CustomNodeData[];
edges: CustomEdgeData[];
}