Commit 94a40ca1b24bdebe78e74e456e7bf7e9e145721f

Authored by ww
1 parent db7f19b3

feat: 规则链设计器节点目录

1 1 import type { VueFlowStore, Getters, Elements } from '@vue-flow/core';
2   -import { ComputedRef, ref, unref } from 'vue';
  2 +import { ComputedRef, computed, ref, unref } from 'vue';
3 3 import { BasicNodeBindData, EdgeData, NodeData } from '../types/node';
4 4 import { EntryCategoryComponentEnum } from '../enum/category';
5 5 import { useBasicDataTransform } from './useBasicDataTransform';
... ... @@ -7,6 +7,7 @@ import { getRuleChainData, saveRuleChainData } from '/@/api/ruleDesigner';
7 7 import { ConnectionItemType, RuleChainType } from '../types/ruleNode';
8 8 import { useInputNode } from './useInputNode';
9 9 import { buildUUID } from '/@/utils/uuid';
  10 +import { useRoute } from 'vue-router';
10 11
11 12 const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT];
12 13
... ... @@ -17,6 +18,10 @@ export function useSaveAndRedo() {
17 18
18 19 const redoDataRef = ref<Elements>([]);
19 20
  21 + const route = useRoute();
  22 +
  23 + const getRuleChainId = computed(() => (route.params as Record<'id', string>).id);
  24 +
20 25 const { mergeData, deconstructionData } = useBasicDataTransform();
21 26
22 27 const triggerChange = () => {
... ... @@ -124,7 +129,7 @@ export function useSaveAndRedo() {
124 129 firstNodeIndex,
125 130 ruleChainId: {
126 131 entityType: 'RULE_CHAIN',
127   - id: '992ddda0-3d97-11ee-8b46-0dfb0900ab17',
  132 + id: unref(getRuleChainId),
128 133 },
129 134 });
130 135
... ... @@ -140,9 +145,7 @@ export function useSaveAndRedo() {
140 145 try {
141 146 loading.value = true;
142 147
143   - const id = '992ddda0-3d97-11ee-8b46-0dfb0900ab17';
144   -
145   - const data = await getRuleChainData(id);
  148 + const data = await getRuleChainData(unref(getRuleChainId));
146 149
147 150 const elements = parseRuleChain(data);
148 151
... ...
... ... @@ -5,7 +5,12 @@
5 5 import { Panel, PanelPosition, VueFlow } from '@vue-flow/core';
6 6 import { computed, onMounted, Ref, ref, unref } from 'vue';
7 7 import './style';
8   - import { BasicConnectionArrow, BasicConnectionLine, BasicEdge, Sidebar } from './src/components';
  8 + import {
  9 + BasicConnectionArrow,
  10 + BasicConnectionLine,
  11 + BasicEdge,
  12 + MenuSidebar,
  13 + } from './src/components';
9 14 import type { FlowElRef } from './types/flow';
10 15 import { useDragCreate } from './hook/useDragCreate';
11 16 import { createFlowContext } from './hook/useFlowContext';
... ... @@ -88,7 +93,8 @@
88 93
89 94 <template>
90 95 <main ref="rootElRef" class="w-full h-full flex relative" @drop="handleOnDrop">
91   - <Sidebar />
  96 + <!-- <Sidebar /> -->
  97 + <MenuSidebar />
92 98
93 99 <Spin :spinning="loading" wrapperClassName="w-full h-ful rule-chain-designer-loading-container">
94 100 <VueFlow
... ...
1 1 import { BasicConnectionModalEnum } from '../enum';
2 2 import { FetchNodeComFlagTypeENum } from '../enum/node';
3   -import type { CategoryConfigType, NodeItemConfigType } from '../types/node';
  3 +import type { NodeItemConfigType } from '../types/node';
4 4 import { ActionCategoryConfig, ActionComponents } from './Action';
5 5 import { EnrichmentCategoryConfig, EnrichmentComponents } from './Enrichment';
6 6 import { ExternalCategoryConfig, ExternalComponents } from './External';
7 7 import { FilterCategoryConfig, FilterComponents } from './Filter';
8 8 import { FlowCategoryConfig, FlowComponents } from './Flow';
9 9 import { TransformationCategoryConfig, TransformationComponents } from './Transformation';
10   -import { RuleNodeTypeEnum } from './index.type';
  10 +import { CategoryItemType, RuleNodeTypeEnum } from './index.type';
11 11
12 12 const createModules = import.meta.glob('../packages/**/create.vue');
13 13 const connectionModules = import.meta.glob('../packages/**/connection.vue');
... ... @@ -63,7 +63,7 @@ export const fetchNodeExtraContent = async (config: NodeItemConfigType) => {
63 63
64 64 export const allComponents: Record<
65 65 Exclude<RuleNodeTypeEnum, RuleNodeTypeEnum.ENTRY>,
66   - { category: CategoryConfigType; components: NodeItemConfigType[] }
  66 + CategoryItemType
67 67 > = {
68 68 [RuleNodeTypeEnum.FILTER]: {
69 69 category: FilterCategoryConfig,
... ...
  1 +import { CategoryConfigType, NodeItemConfigType } from '../types/node';
  2 +
1 3 export enum RuleNodeTypeEnum {
2 4 ENTRY = 'ENTRY',
3 5 FLOW = 'FLOW',
... ... @@ -7,3 +9,8 @@ export enum RuleNodeTypeEnum {
7 9 FILTER = 'FILTER',
8 10 TRANSFORMATION = 'TRANSFORMATION',
9 11 }
  12 +
  13 +export interface CategoryItemType {
  14 + category: CategoryConfigType;
  15 + components: NodeItemConfigType[];
  16 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { CategoryItemType, RuleNodeTypeEnum } from '../../../packages/index.type';
  3 + import Icon from '/@/components/Icon';
  4 + import { Tooltip } from 'ant-design-vue';
  5 +
  6 + const emit = defineEmits<{
  7 + (eventName: 'update:activeKey', activeKey?: RuleNodeTypeEnum);
  8 + }>();
  9 +
  10 + defineProps<{
  11 + activeKey?: RuleNodeTypeEnum;
  12 + category?: CategoryItemType;
  13 + }>();
  14 +
  15 + const handleClick = (category?: RuleNodeTypeEnum) => {
  16 + emit('update:activeKey', category);
  17 + };
  18 +</script>
  19 +
  20 +<template>
  21 + <Tooltip color="#fff" placement="right">
  22 + <template #title>
  23 + <p class="text-dark-900 font-bold text-xs m-0 py-1">
  24 + {{ category?.category.title }}
  25 + </p>
  26 + <p class="text-gray-500 text-xs m-0 py-1">
  27 + {{ category?.category.description }}
  28 + </p>
  29 + </template>
  30 + <div
  31 + class="h-16 mt-2 flex justify-center items-center flex-col cursor-pointer rounded"
  32 + :class="activeKey === category?.category.category ? 'bg-blue-100 text-blue-400' : ''"
  33 + @click="handleClick(category?.category.category)"
  34 + >
  35 + <Icon :icon="category?.category.icon" class="svg:text-3xl" />
  36 + <div class="select-none">{{ category?.category.title }}</div>
  37 + </div>
  38 + </Tooltip>
  39 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { cloneDeep } from 'lodash';
  3 + import { computed, ref, unref } from 'vue';
  4 + import { allComponents } from '../../../packages';
  5 + import { CategoryItemType, RuleNodeTypeEnum } from '../../../packages/index.type';
  6 + import CategoryItem from './CategoryItem.vue';
  7 + import NodeItem from './NodeItem.vue';
  8 +
  9 + const props = defineProps<{
  10 + searchText?: string;
  11 + }>();
  12 +
  13 + const activeKey = ref<RuleNodeTypeEnum | undefined>(RuleNodeTypeEnum.FILTER);
  14 +
  15 + const getCurrentCategoryNode = computed<CategoryItemType>(() => {
  16 + const category = cloneDeep(allComponents[unref(activeKey)!]) as CategoryItemType;
  17 + const { searchText } = props;
  18 + if (searchText) {
  19 + category.components = category.components.filter((item) =>
  20 + item.name.toUpperCase().includes(searchText.toUpperCase())
  21 + );
  22 + }
  23 + return category;
  24 + });
  25 +</script>
  26 +
  27 +<template>
  28 + <section class="absolute top-11 w-full flex flex-auto h-[calc(100%-2.75rem)] bg-neutral-100">
  29 + <nav class="w-20 min-w-20 p-2 border-t border-light-50">
  30 + <CategoryItem
  31 + v-for="category in allComponents"
  32 + :key="category.category.category"
  33 + v-model:activeKey="activeKey"
  34 + :category="category"
  35 + />
  36 + </nav>
  37 + <body class="p-4 w-full flex flex-col gap-2 items-center overflow-x-hidden overflow-y-auto">
  38 + <NodeItem
  39 + v-for="config in getCurrentCategoryNode.components"
  40 + :key="config.clazz"
  41 + :config="config"
  42 + :category="getCurrentCategoryNode.category"
  43 + />
  44 + </body>
  45 + </section>
  46 +</template>
... ...
  1 +<script setup lang="ts">
  2 + import { Input, Tooltip } from 'ant-design-vue';
  3 + import { Icon } from '/@/components/Icon';
  4 +
  5 + const emit = defineEmits(['fold', 'search']);
  6 +
  7 + const handleOnFold = () => emit('fold');
  8 +
  9 + const handleInputChange = (event: Event) => {
  10 + const value = (event.target as HTMLInputElement).value;
  11 + emit('search', value);
  12 + };
  13 +</script>
  14 +
  15 +<template>
  16 + <head class="h-11 w-full flex justify-between items-center bg-neutral-100">
  17 + <Input
  18 + placeholder="搜索节点"
  19 + class="!mx-2 flex-auto"
  20 + @change="handleInputChange"
  21 + :allowClear="true"
  22 + >
  23 + <template #suffix>
  24 + <Icon icon="material-symbols:search" />
  25 + </template>
  26 + </Input>
  27 + <div class="w-14 min-w-14 flex justify-center">
  28 + <Tooltip title="折叠">
  29 + <Icon
  30 + icon="ant-design:menu-fold-outlined"
  31 + class="cursor-pointer svg:text-xl"
  32 + @click="handleOnFold"
  33 + />
  34 + </Tooltip>
  35 + </div>
  36 + </head>
  37 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { Tooltip } from 'ant-design-vue';
  3 + import { computed, ref, toRaw, unref } from 'vue';
  4 + import { EFFECT_SYMBOL, TRANSFER_DATA_KEY } from '../../../hook/useDragCreate';
  5 + import { CategoryConfigType, NodeItemConfigType } from '../../../types/node';
  6 +
  7 + const props = defineProps<{
  8 + category?: CategoryConfigType;
  9 + config?: NodeItemConfigType;
  10 + }>();
  11 +
  12 + const visible = ref(false);
  13 +
  14 + const getNodeDefinition = computed(() => {
  15 + const { config } = props;
  16 + const { configurationDescriptor } = config || {};
  17 + const { nodeDefinition } = configurationDescriptor || {};
  18 + return nodeDefinition || {};
  19 + });
  20 +
  21 + const getIcon = computed(() => {
  22 + const { icon } = unref(getNodeDefinition);
  23 + const { icon: categoryIcon } = props.category || {};
  24 + return icon || categoryIcon || 'tabler:circuit-ground';
  25 + });
  26 +
  27 + const getIconUrl = computed(() => {
  28 + const { iconUrl } = unref(getNodeDefinition);
  29 + return iconUrl;
  30 + });
  31 +
  32 + const getBackgroundColor = computed(() => {
  33 + const { config, category } = props;
  34 + const { backgroundColor } = config || {};
  35 + const { backgroundColor: categoryBackgroundColor } = category || {};
  36 + return backgroundColor || categoryBackgroundColor;
  37 + });
  38 +
  39 + const handleOnDragStart = (event: DragEvent, options: object) => {
  40 + if (event.dataTransfer) {
  41 + event.dataTransfer.setData(
  42 + TRANSFER_DATA_KEY,
  43 + JSON.stringify({ offsetX: event.offsetX, offsetY: event.offsetY, options })
  44 + );
  45 + event.dataTransfer.effectAllowed = EFFECT_SYMBOL;
  46 + }
  47 + };
  48 +
  49 + const handleDragStart = (event: DragEvent) => {
  50 + visible.value = false;
  51 + handleOnDragStart(event, toRaw(unref(props)));
  52 + };
  53 +</script>
  54 +
  55 +<template>
  56 + <Tooltip
  57 + v-model:visible="visible"
  58 + placement="right"
  59 + color="#fff"
  60 + trigger="hover"
  61 + :mouse-enter-delay="0.5"
  62 + overlay-class-name="!max-w-90"
  63 + >
  64 + <template #title>
  65 + <section class="text-dark-900">
  66 + <h1 class="font-bold dark:text-dark-50">
  67 + {{ config?.name }}
  68 + </h1>
  69 + <p class="italic text-gray-600">
  70 + {{ getNodeDefinition.description }}
  71 + </p>
  72 + <p v-html="getNodeDefinition.details"></p>
  73 + </section>
  74 + </template>
  75 + <main
  76 + class="node flex items-center cursor-pointer w-44 h-12 min-h-12 rounded border relative px-4 py-2 border-gray-700 z-10"
  77 + :style="{ backgroundColor: getBackgroundColor }"
  78 + :draggable="true"
  79 + @dragstart="handleDragStart"
  80 + >
  81 + <div>
  82 + <img v-if="getIconUrl" class="w-4 h-4" :src="getIconUrl" alt="" />
  83 + <Icon v-if="!getIconUrl" class="text-2xl dark:text-light-50" :icon="getIcon" />
  84 + </div>
  85 + <div class="flex flex-1 text-xs flex-col ml-2 text-left truncate">
  86 + <span class="text-gray-700 w-full truncate text-xs dark:text-light-50">
  87 + {{ config?.name }}
  88 + </span>
  89 + </div>
  90 + <div class="w-4 h-4 bg-dark-50 rounded-1 border absolute -left-2 border-light-50"></div>
  91 + <div class="w-4 h-4 bg-dark-50 rounded-1 border absolute -right-2 border-light-50"></div>
  92 + </main>
  93 + </Tooltip>
  94 +</template>
  95 +
  96 +<style lang="css" scoped>
  97 + .node::before {
  98 + content: '';
  99 + position: absolute;
  100 + width: 100%;
  101 + height: 100%;
  102 + left: 0;
  103 + display: none;
  104 + background-color: #00000027;
  105 + }
  106 +
  107 + .node:hover::before {
  108 + display: block;
  109 + }
  110 +</style>
... ...
  1 +export { default as MenuSidebar } from './index.vue';
... ...
  1 +<script setup lang="ts">
  2 + import NodeFilter from './NodeFilter.vue';
  3 + import CategoryPanel from './CategoryPanel.vue';
  4 + import { computed, CSSProperties, ref, unref } from 'vue';
  5 + import { Icon } from '/@/components/Icon';
  6 + import { Tooltip } from 'ant-design-vue';
  7 +
  8 + const expand = ref(true);
  9 +
  10 + const searchText = ref();
  11 +
  12 + const handleOnFold = () => {
  13 + expand.value = false;
  14 + };
  15 +
  16 + const getSidebarStyle = computed<CSSProperties>(() => {
  17 + return {
  18 + flex: `0 0 ${unref(expand) ? '320px' : 0}`,
  19 + width: unref(expand) ? '320px' : 0,
  20 + minWidth: unref(expand) ? '320px' : 0,
  21 + };
  22 + });
  23 +
  24 + const handleSearch = (value: string) => {
  25 + searchText.value = value;
  26 + };
  27 +</script>
  28 +
  29 +<template>
  30 + <section
  31 + :style="getSidebarStyle"
  32 + class="h-full shadow shadow-lg relative z-50 shadow-dark-600 overflow-hidden transition-all"
  33 + >
  34 + <NodeFilter @fold="handleOnFold" @search="handleSearch" />
  35 + <CategoryPanel :searchText="searchText" />
  36 + </section>
  37 + <Tooltip title="展开">
  38 + <Icon
  39 + icon="ant-design:menu-unfold-outlined"
  40 + class="cursor-pointer svg:text-2xl absolute top-5 left-5 z-40 !flex justify-center items-center w-10 h-10 rounded-1 bg-blue-200 svg:text-light-50 shadow-lg"
  41 + @click="expand = true"
  42 + />
  43 + </Tooltip>
  44 +</template>
... ...
  1 +import { h } from 'vue';
1 2 import { BasicColumn } from '/@/components/Table';
  3 +import { dateUtil } from '/@/utils/dateUtil';
  4 +import Icon from '/@/components/Icon';
  5 +import { Modal } from 'ant-design-vue';
  6 +import { JsonPreview } from '/@/components/CodeEditor';
  7 +
  8 +const handleOpenJsonPreviewModal = (title: string, content: string) => {
  9 + Modal.info({
  10 + title,
  11 + width: 600,
  12 + content: h(JsonPreview, { data: JSON.parse(content) }),
  13 + });
  14 +};
2 15
3 16 export const columns: BasicColumn[] = [
4 17 {
5 18 title: '事件时间',
6 19 dataIndex: 'createdTime',
  20 + width: 200,
  21 + format(text) {
  22 + return dateUtil(text).format('YYYY-MM-DD HH:mm:ss');
  23 + },
7 24 },
8 25 {
9 26 title: '服务器',
10 27 dataIndex: 'body.server',
  28 + ellipsis: true,
11 29 },
12 30 {
13 31 title: '类型',
14   - dataIndex: 'type',
  32 + dataIndex: 'body.type',
  33 + ellipsis: true,
15 34 },
16 35 {
17 36 title: '实体类型',
18   - dataIndex: 'entityId.entityType',
  37 + dataIndex: 'body.entityName',
  38 + ellipsis: true,
19 39 },
20 40 {
21 41 title: '消息ID',
22   - dataIndex: 'id.id',
  42 + dataIndex: 'body.entityId',
  43 + ellipsis: true,
23 44 },
24 45 {
25 46 title: '消息类型',
26   - dataIndex: 'msgtype',
  47 + dataIndex: 'body.msgType',
  48 + ellipsis: true,
27 49 },
28 50 {
29 51 title: '关联类型',
30   - dataIndex: 'relatedType',
  52 + dataIndex: 'body.relationType',
  53 + ellipsis: true,
31 54 },
32 55 {
33 56 title: '数据',
34   - dataIndex: 'data',
  57 + dataIndex: 'body.data',
  58 + ellipsis: true,
  59 + width: 80,
  60 + customRender({ text }) {
  61 + return (
  62 + text &&
  63 + h(Icon, {
  64 + icon: 'material-symbols:more-horiz',
  65 + class: 'cursor-pointer svg:text-2xl',
  66 + onClick: () => handleOpenJsonPreviewModal('数据', text),
  67 + })
  68 + );
  69 + },
35 70 },
36 71 {
37 72 title: '元数据',
38   - dataIndex: 'metadaeta',
  73 + dataIndex: 'body.metadata',
  74 + ellipsis: true,
  75 + width: 80,
  76 + customRender({ text }) {
  77 + return (
  78 + text &&
  79 + h(Icon, {
  80 + icon: 'material-symbols:more-horiz',
  81 + class: 'cursor-pointer svg:text-2xl',
  82 + onClick: () => handleOpenJsonPreviewModal('元数据', text),
  83 + })
  84 + );
  85 + },
39 86 },
40 87 {
41 88 title: '错误',
42 89 dataIndex: 'error',
  90 + width: 80,
  91 + slots: {
  92 + customRender: 'error',
  93 + },
43 94 },
44 95 ];
... ...
... ... @@ -46,6 +46,8 @@
46 46 const result = await getRuleNodeEventList(
47 47 unref(getNodeId)!,
48 48 {
  49 + sortProperty: 'createdTime',
  50 + sortOrder: 'DESC',
49 51 page: params.page - 1,
50 52 pageSize: params.pageSize,
51 53 tenantId: unref(getTenantId),
... ... @@ -130,6 +132,7 @@
130 132 </template>
131 133 <template #error="{ text }">
132 134 <Icon
  135 + v-if="text"
133 136 icon="material-symbols:more-horiz"
134 137 class="cursor-pointer svg:text-2xl"
135 138 @click="handleOpenDetailModal(text)"
... ...
... ... @@ -18,12 +18,14 @@ export const columns: BasicColumn[] = [
18 18 },
19 19 {
20 20 title: '消息处理',
21   - dataIndex: 'body.event',
  21 + dataIndex: 'body.messagesProcessed',
22 22 ellipsis: true,
  23 + width: 100,
23 24 },
24 25 {
25 26 title: '错误发生',
26   - dataIndex: 'body.event',
  27 + dataIndex: 'body.errorsOccurred',
27 28 ellipsis: true,
  29 + width: 100,
28 30 },
29 31 ];
... ...
1 1 export * from './Basic';
2 2 export * from './Sidebar';
  3 +export { MenuSidebar } from './MenuSidebar';
... ...