Showing
57 changed files
with
4573 additions
and
15 deletions
Too many changes to show.
To preserve performance only 57 of 63 files are displayed.
1 | -const { defineConfig } = require('vite') | 1 | +const { defineConfig } = require('vite'); |
2 | 2 | ||
3 | -module.exports = defineConfig({ | 3 | +const process = require('process'); |
4 | +const path = require('path'); | ||
5 | + | ||
6 | +const cwd = process.cwd(); | ||
4 | 7 | ||
5 | -}) | 8 | +module.exports = defineConfig({ |
9 | + resolve: { | ||
10 | + extensions: ['.json', '.jsx', '.js', '.ts', '.tsx'], | ||
11 | + alias: { | ||
12 | + '@': path.resolve(cwd, './src/'), | ||
13 | + '@/src': path.resolve(cwd, './src/'), | ||
14 | + }, | ||
15 | + }, | ||
16 | + css: { | ||
17 | + preprocessorOptions: { | ||
18 | + less: { | ||
19 | + javascriptEnabled: true, | ||
20 | + additionalData: `@import '@qx/ui/dist/src/style/variable.less';`, | ||
21 | + }, | ||
22 | + }, | ||
23 | + }, | ||
24 | +}); |
@@ -52,6 +52,7 @@ module.exports = { | @@ -52,6 +52,7 @@ module.exports = { | ||
52 | extensions: ['.json', '.jsx', '.js', '.ts', '.tsx'], | 52 | extensions: ['.json', '.jsx', '.js', '.ts', '.tsx'], |
53 | alias: { | 53 | alias: { |
54 | '@': path.resolve(cwd, './src/'), | 54 | '@': path.resolve(cwd, './src/'), |
55 | + '@/src': path.resolve(cwd, './src/'), | ||
55 | }, | 56 | }, |
56 | }, | 57 | }, |
57 | plugins: [ | 58 | plugins: [ |
@@ -23,19 +23,36 @@ | @@ -23,19 +23,36 @@ | ||
23 | "author": "", | 23 | "author": "", |
24 | "license": "ISC", | 24 | "license": "ISC", |
25 | "dependencies": { | 25 | "dependencies": { |
26 | + "@ant-design/icons": "^5.2.5", | ||
27 | + "@qx/flow": "1.0.0-alpha.10", | ||
28 | + "@qx/icon-btn": "^0.0.13", | ||
29 | + "@qx/ui": "0.0.3-beta.1", | ||
30 | + "@qx/utils": "0.0.56-alpha.4", | ||
31 | + "ahooks": "^3.7.8", | ||
32 | + "antd": "^5.8.4", | ||
33 | + "classnames": "^2.3.2", | ||
26 | "hox": "^2.1.1", | 34 | "hox": "^2.1.1", |
35 | + "lodash-es": "^4.17.21", | ||
36 | + "normalize.css": "^8.0.1", | ||
27 | "react": "^18.2.0", | 37 | "react": "^18.2.0", |
38 | + "react-cookies": "^0.1.1", | ||
28 | "react-dom": "^18.2.0", | 39 | "react-dom": "^18.2.0", |
29 | "react-router": "^6.15.0", | 40 | "react-router": "^6.15.0", |
30 | - "react-router-dom": "^6.15.0" | 41 | + "react-router-dom": "^6.15.0", |
42 | + "react-sortablejs": "^6.1.4", | ||
43 | + "sortablejs": "^1.15.0", | ||
44 | + "uuid": "^9.0.0" | ||
31 | }, | 45 | }, |
32 | "devDependencies": { | 46 | "devDependencies": { |
33 | "@babel/core": "^7.22.9", | 47 | "@babel/core": "^7.22.9", |
34 | "@babel/preset-env": "^7.22.9", | 48 | "@babel/preset-env": "^7.22.9", |
35 | "@babel/preset-react": "^7.22.5", | 49 | "@babel/preset-react": "^7.22.5", |
50 | + "@qx/types": "^0.0.2", | ||
36 | "@svgr/webpack": "^8.1.0", | 51 | "@svgr/webpack": "^8.1.0", |
52 | + "@types/lodash-es": "^4.17.8", | ||
37 | "@types/react": "^18.2.21", | 53 | "@types/react": "^18.2.21", |
38 | "@types/react-dom": "^18.2.7", | 54 | "@types/react-dom": "^18.2.7", |
55 | + "@types/uuid": "^9.0.2", | ||
39 | "babel-loader": "^9.1.3", | 56 | "babel-loader": "^9.1.3", |
40 | "copy-webpack-plugin": "^11.0.0", | 57 | "copy-webpack-plugin": "^11.0.0", |
41 | "css-loader": "^6.8.1", | 58 | "css-loader": "^6.8.1", |
src/components/config-drawer/index.less
0 → 100644
1 | +//抽屉 蒙层样式全局覆盖 | ||
2 | +.ant-drawer .ant-drawer-mask { | ||
3 | + background-color: transparent; | ||
4 | + transition: none; | ||
5 | + animation: none; | ||
6 | +} | ||
7 | + | ||
8 | +//开始节点 配置 抽屉页面 样式覆盖 | ||
9 | +.qx-df-start-setting-drawer { | ||
10 | + .ant-drawer-body { | ||
11 | + padding: 0; | ||
12 | + } | ||
13 | + | ||
14 | + .ant-form.ant-form-vertical { | ||
15 | + height: calc(100% - 57px); | ||
16 | + padding: 24px 18px 24px 24px; | ||
17 | + font-size: 14px; | ||
18 | + background-color: #fff; | ||
19 | + overflow-y: hidden; | ||
20 | + scrollbar-gutter: stable; | ||
21 | + | ||
22 | + &:hover { | ||
23 | + overflow-y: auto; | ||
24 | + } | ||
25 | + | ||
26 | + .qx-data-flow-tooltip-opt { | ||
27 | + margin-left: 4px; | ||
28 | + color: @N6; | ||
29 | + } | ||
30 | + | ||
31 | + .qx-flow-form__label-title { | ||
32 | + margin-bottom: 16px; | ||
33 | + line-height: 22px; | ||
34 | + font-size: 14px; | ||
35 | + font-weight: 600; | ||
36 | + color: @N9; | ||
37 | + } | ||
38 | + | ||
39 | + .qx-flow-form__label-tips { | ||
40 | + margin-bottom: 8px; | ||
41 | + line-height: 22px; | ||
42 | + font-size: 14px; | ||
43 | + font-weight: 400; | ||
44 | + color: @N8; | ||
45 | + } | ||
46 | + | ||
47 | + .ant-row.ant-form-item:not(:first-child) { | ||
48 | + margin-bottom: 16px; | ||
49 | + } | ||
50 | + | ||
51 | + .ant-col.ant-form-item-control { | ||
52 | + color: @N9; | ||
53 | + | ||
54 | + .ant-checkbox-wrapper, | ||
55 | + .ant-radio-wrapper { | ||
56 | + color: @N9; | ||
57 | + } | ||
58 | + } | ||
59 | + | ||
60 | + .ant-col ant-form-item-label { | ||
61 | + color: @N8; | ||
62 | + } | ||
63 | + } | ||
64 | +} |
src/components/config-drawer/index.tsx
0 → 100644
1 | +import React, { memo, useRef, useContext, useState } from 'react'; | ||
2 | +import { Drawer, Button } from 'antd'; | ||
3 | +import { | ||
4 | + IDrawerComponent, | ||
5 | + BuilderContext, | ||
6 | + useHistory, | ||
7 | + useDrawer, | ||
8 | +} from '@qx/flow'; | ||
9 | +import { QxIcon } from '@/components'; | ||
10 | +import './index.less'; | ||
11 | +import cls from 'classnames'; | ||
12 | +import commonStyles from '@/components/header/index.module.less'; | ||
13 | +// 头部相关 | ||
14 | +import FlowHeader from '../header'; | ||
15 | +import FlowNameEditor from '../name-editor'; | ||
16 | +import { merge } from 'lodash-es'; | ||
17 | + | ||
18 | +const ConfigDrawerContainer: React.FC<ConfigDrawerContainerProps> = (props) => { | ||
19 | + const configRef = useRef<any>(); | ||
20 | + | ||
21 | + const { selectedNode, setSelectedNode, onChange, nodes } = | ||
22 | + useContext(BuilderContext); | ||
23 | + | ||
24 | + const [showEdit, setShowEdit] = useState<boolean>(false); | ||
25 | + | ||
26 | + const { pushHistory } = useHistory(); | ||
27 | + | ||
28 | + const { saveDrawer } = useDrawer(); | ||
29 | + | ||
30 | + const handleChange = async (values: any, extra: Record<string, any>) => { | ||
31 | + if (selectedNode) { | ||
32 | + selectedNode.data = values; | ||
33 | + try { | ||
34 | + await configRef.current?.validateFields?.(); | ||
35 | + selectedNode.validateStatusError = false; | ||
36 | + } catch (error) { | ||
37 | + selectedNode.validateStatusError = true; | ||
38 | + } | ||
39 | + | ||
40 | + merge(setSelectedNode, extra); | ||
41 | + onChange([...nodes], 'config-change'); | ||
42 | + } | ||
43 | + }; | ||
44 | + | ||
45 | + const handleClose = () => { | ||
46 | + if (selectedNode) { | ||
47 | + selectedNode.configuring = false; | ||
48 | + } | ||
49 | + setSelectedNode(undefined); | ||
50 | + pushHistory(); | ||
51 | + onChange([...nodes], 'close-drawer'); | ||
52 | + }; | ||
53 | + // console.log('数据展示 nodes ===', nodes); | ||
54 | + // console.log('数据展示 selectedNode ===', selectedNode); | ||
55 | + | ||
56 | + return ( | ||
57 | + <Drawer | ||
58 | + {...props} | ||
59 | + title={null} | ||
60 | + closable={false} | ||
61 | + maskClosable={true} | ||
62 | + drawerStyle={{ | ||
63 | + boxShadow: '-4px 0 16px 0 rgba(36, 40, 53, 0.1)', | ||
64 | + }} | ||
65 | + width={480} | ||
66 | + destroyOnClose={true} | ||
67 | + className={'qx-df-start-setting-drawer'} | ||
68 | + onClose={handleClose} | ||
69 | + > | ||
70 | + <FlowHeader | ||
71 | + extraStyles={{ padding: '0 24px' }} | ||
72 | + renderLeft={ | ||
73 | + <FlowNameEditor | ||
74 | + nodeName={selectedNode?.name || ''} | ||
75 | + nodeIcon={<QxIcon type={'icon-two-dimensional-code'} />} | ||
76 | + editTitle={showEdit} | ||
77 | + setEditTitle={setShowEdit} | ||
78 | + onChange={(val) => { | ||
79 | + if (selectedNode) { | ||
80 | + selectedNode.name = val; | ||
81 | + onChange([...nodes], 'config-change'); | ||
82 | + } | ||
83 | + }} | ||
84 | + /> | ||
85 | + } | ||
86 | + renderRight={ | ||
87 | + <Button | ||
88 | + className={cls(commonStyles['qx-data-flow__modal-close'])} | ||
89 | + type="text" | ||
90 | + onClick={handleClose} | ||
91 | + icon={<QxIcon type={'icon-close'} />} | ||
92 | + /> | ||
93 | + } | ||
94 | + /> | ||
95 | + {props.children && | ||
96 | + React.cloneElement(props.children as React.ReactElement, { | ||
97 | + ref: configRef, | ||
98 | + onChange: handleChange, | ||
99 | + })} | ||
100 | + </Drawer> | ||
101 | + ); | ||
102 | +}; | ||
103 | + | ||
104 | +interface ConfigDrawerContainerProps extends IDrawerComponent {} | ||
105 | + | ||
106 | +export default memo(ConfigDrawerContainer); |
src/components/dynamic-component/index.tsx
0 → 100644
1 | +@prefix-cls: ~'qx-flow-version'; | ||
2 | + | ||
3 | +.@{prefix-cls} { | ||
4 | + width: auto; | ||
5 | + height: 24px; | ||
6 | + display: inline-flex; | ||
7 | + align-items: center; | ||
8 | + justify-content: center; | ||
9 | + padding: 0 8px; | ||
10 | + font-size: 14px; | ||
11 | + font-weight: 400; | ||
12 | + color: @N9; | ||
13 | + background-color: @N3; | ||
14 | + border-radius: 4px; | ||
15 | + border: 1px solid @N3; | ||
16 | + | ||
17 | + &.operational { | ||
18 | + cursor: pointer; | ||
19 | + | ||
20 | + &:hover { | ||
21 | + color: @B8; | ||
22 | + border-color: @B8; | ||
23 | + } | ||
24 | + } | ||
25 | + | ||
26 | + &.published { | ||
27 | + color: #00b42a; | ||
28 | + background-color: #e8ffea; | ||
29 | + border-color: #e8ffea; | ||
30 | + | ||
31 | + &.operational:hover { | ||
32 | + color: #00b42a; | ||
33 | + background-color: #aff0b5; | ||
34 | + border-color: #e8ffea; | ||
35 | + } | ||
36 | + } | ||
37 | + | ||
38 | + &.edit { | ||
39 | + color: #ff7d00; | ||
40 | + background-color: #fff7e8; | ||
41 | + border-color: #e8ffea; | ||
42 | + | ||
43 | + &.operational:hover { | ||
44 | + color: #ff7d00; | ||
45 | + background-color: #ffe4ba; | ||
46 | + border-color: #ffe4ba; | ||
47 | + } | ||
48 | + } | ||
49 | +} |
src/components/flow-version/index.tsx
0 → 100644
1 | +// 数据流名称编辑通用组件 | ||
2 | +import React from 'react'; | ||
3 | + | ||
4 | +import { | ||
5 | + FlowStatusEnums, | ||
6 | + FlowStatusProps, | ||
7 | + FlowVersionStatusProps, | ||
8 | +} from '@/interface'; | ||
9 | + | ||
10 | +// 样式 | ||
11 | +import cls from 'classnames'; | ||
12 | +import styles from './index.module.less'; | ||
13 | + | ||
14 | +const prefix = 'qx-flow-version'; | ||
15 | + | ||
16 | +interface FlowVersionProps { | ||
17 | + versionText?: string | number; | ||
18 | + status?: FlowVersionStatusProps | FlowStatusProps; | ||
19 | + statusEnums?: Record<string, string>; // 状态枚举 | ||
20 | + handleClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void; // 调用方法 | ||
21 | +} | ||
22 | + | ||
23 | +const FlowVersion: React.FC<FlowVersionProps> = (props) => { | ||
24 | + const { handleClick, status, versionText, statusEnums } = props; | ||
25 | + | ||
26 | + return ( | ||
27 | + <> | ||
28 | + {versionText || | ||
29 | + (statusEnums || FlowStatusEnums)[status || 'UNPUBLISHED'] ? ( | ||
30 | + <div | ||
31 | + className={cls( | ||
32 | + styles[`${prefix}`], | ||
33 | + styles[`${(status || 'DRAFT').toLowerCase()}`], | ||
34 | + !!handleClick && versionText && Number(versionText) > 1 | ||
35 | + ? styles['operational'] | ||
36 | + : ' ', | ||
37 | + !!handleClick && versionText && Number(versionText) > 1 | ||
38 | + ? ' qx-flow-version__default-operational' | ||
39 | + : ' qx-flow-version__default', | ||
40 | + )} | ||
41 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => { | ||
42 | + if (handleClick && versionText && Number(versionText) > 1) | ||
43 | + handleClick(e); | ||
44 | + }} | ||
45 | + style={{ | ||
46 | + minWidth: | ||
47 | + (!!versionText ? (versionText + '').length + 1 : 0) * 14 + | ||
48 | + (!!status ? 3 : 0) * 14 + | ||
49 | + 'px', | ||
50 | + }} | ||
51 | + > | ||
52 | + {!!versionText ? <span>V {versionText}</span> : null} | ||
53 | + {!!status ? ( | ||
54 | + <span>{(statusEnums || FlowStatusEnums)[status]}</span> | ||
55 | + ) : null} | ||
56 | + </div> | ||
57 | + ) : null} | ||
58 | + </> | ||
59 | + ); | ||
60 | +}; | ||
61 | + | ||
62 | +export default FlowVersion; |
src/components/header/index.module.less
0 → 100644
1 | +@prefix-cls: ~'qx-data-flow__header'; | ||
2 | + | ||
3 | +.qx-data-flow__container { | ||
4 | + height: 100%; | ||
5 | + | ||
6 | + :global { | ||
7 | + .qx-flow-wrap { | ||
8 | + height: calc(100% - 57px); | ||
9 | + | ||
10 | + &.is-full { | ||
11 | + height: 100%; | ||
12 | + } | ||
13 | + } | ||
14 | + } | ||
15 | +} | ||
16 | + | ||
17 | +.@{prefix-cls} { | ||
18 | + display: flex; | ||
19 | + align-items: center; | ||
20 | + justify-content: space-between; | ||
21 | + padding: 0 16px; | ||
22 | + width: 100%; | ||
23 | + height: 57px; | ||
24 | + background-color: #fff; | ||
25 | + border-bottom: 1px solid @N4; | ||
26 | + box-sizing: border-box; | ||
27 | +} | ||
28 | + | ||
29 | +.@{prefix-cls}__left { | ||
30 | + height: 100%; | ||
31 | + display: flex; | ||
32 | + align-items: center; | ||
33 | + justify-content: flex-start; | ||
34 | + font-size: 16px; | ||
35 | + color: @N9; | ||
36 | + cursor: pointer; | ||
37 | + | ||
38 | + :global { | ||
39 | + .anticon { | ||
40 | + font-size: 16px; | ||
41 | + } | ||
42 | + } | ||
43 | +} | ||
44 | + | ||
45 | +.@{prefix-cls}__left-return { | ||
46 | + width: 24px; | ||
47 | + height: 24px; | ||
48 | + display: flex; | ||
49 | + align-items: center; | ||
50 | + justify-content: center; | ||
51 | + font-size: 18px; | ||
52 | + cursor: pointer; | ||
53 | + transform: rotate(90deg); | ||
54 | + color: @N9; | ||
55 | + cursor: pointer; | ||
56 | +} | ||
57 | + | ||
58 | +.@{prefix-cls}__left-title { | ||
59 | + margin: 0 8px; | ||
60 | + font-size: 16px; | ||
61 | + color: @N9; | ||
62 | + font-weight: 600; | ||
63 | +} | ||
64 | + | ||
65 | +.@{prefix-cls}__left-custom { | ||
66 | + height: 100%; | ||
67 | + display: flex; | ||
68 | + align-items: center; | ||
69 | + justify-content: flex-start; | ||
70 | + font-size: 16px; | ||
71 | +} | ||
72 | + | ||
73 | +.@{prefix-cls}__left-custom__opt { | ||
74 | + margin-right: 16px; | ||
75 | + | ||
76 | + :global { | ||
77 | + .ant-btn-icon-only { | ||
78 | + width: 24px; | ||
79 | + height: 24px; | ||
80 | + padding: 0; | ||
81 | + display: inline-flex; | ||
82 | + align-items: center; | ||
83 | + justify-content: center; | ||
84 | + } | ||
85 | + | ||
86 | + .anticon { | ||
87 | + font-size: 16px; | ||
88 | + color: @N7; | ||
89 | + cursor: pointer; | ||
90 | + | ||
91 | + &:hover { | ||
92 | + color: @B8; | ||
93 | + } | ||
94 | + } | ||
95 | + } | ||
96 | +} | ||
97 | + | ||
98 | +.@{prefix-cls}__right { | ||
99 | + :global { | ||
100 | + .ant-btn-text { | ||
101 | + &:focus, | ||
102 | + &:hover { | ||
103 | + background-color: transparent; | ||
104 | + } | ||
105 | + } | ||
106 | + | ||
107 | + .ant-btn-default { | ||
108 | + color: @N8; | ||
109 | + border-color: @N4; | ||
110 | + | ||
111 | + &:focus, | ||
112 | + &:hover { | ||
113 | + color: @N8; | ||
114 | + } | ||
115 | + | ||
116 | + &:hover { | ||
117 | + color: @B8; | ||
118 | + border-color: @B8; | ||
119 | + } | ||
120 | + } | ||
121 | + | ||
122 | + .ant-btn-primary { | ||
123 | + &:focus, | ||
124 | + &:hover { | ||
125 | + background-color: transparent; | ||
126 | + } | ||
127 | + | ||
128 | + &:hover { | ||
129 | + background-color: @B7; | ||
130 | + } | ||
131 | + } | ||
132 | + } | ||
133 | +} | ||
134 | + | ||
135 | +.qx-data-flow__modal-close { | ||
136 | + width: 24px; | ||
137 | + height: 24px; | ||
138 | + display: flex; | ||
139 | + align-items: center; | ||
140 | + justify-content: center; | ||
141 | + | ||
142 | + :global { | ||
143 | + .anticon { | ||
144 | + font-size: 12px !important; | ||
145 | + color: @N7; | ||
146 | + } | ||
147 | + } | ||
148 | + | ||
149 | + &:hover { | ||
150 | + :global { | ||
151 | + .anticon { | ||
152 | + color: @N9; | ||
153 | + } | ||
154 | + } | ||
155 | + } | ||
156 | +} |
src/components/header/index.tsx
0 → 100644
1 | +// 数据流 头部组件 | ||
2 | +import React, { useEffect, useState } from 'react'; | ||
3 | +import { Button, Space } from 'antd'; | ||
4 | +// import { useLocation } from 'umi'; | ||
5 | +import { useLocation } from 'react-router-dom'; | ||
6 | +import { QxIcon } from '@/components'; | ||
7 | + | ||
8 | +// 样式 | ||
9 | +import cls from 'classnames'; | ||
10 | +import styles from './index.module.less'; | ||
11 | + | ||
12 | +const prefix = 'qx-data-flow__header'; | ||
13 | + | ||
14 | +interface FlowHeaderProps { | ||
15 | + handleClick?: (type: string) => void; | ||
16 | + type?: 'FLOW' | 'VERSION'; // 数据流 | 数据流详情页 | 流程版本 | ||
17 | + renderLeft?: React.ReactNode; | ||
18 | + title?: string; | ||
19 | + renderRight?: React.ReactNode; | ||
20 | + extraStyles?: React.CSSProperties; | ||
21 | +} | ||
22 | + | ||
23 | +const FlowHeader: React.FC<FlowHeaderProps> = (props) => { | ||
24 | + const { | ||
25 | + handleClick, | ||
26 | + type = 'FLOW', | ||
27 | + renderLeft, | ||
28 | + title, | ||
29 | + renderRight, | ||
30 | + extraStyles, | ||
31 | + } = props; | ||
32 | + | ||
33 | + // @ts-ignore | ||
34 | + const { query } = useLocation(); | ||
35 | + | ||
36 | + return ( | ||
37 | + <header className={cls(styles[`${prefix}`])} style={extraStyles}> | ||
38 | + {!!renderLeft ? ( | ||
39 | + renderLeft | ||
40 | + ) : ( | ||
41 | + <div | ||
42 | + className={cls(styles[`${prefix}__left`])} | ||
43 | + onClick={() => { | ||
44 | + handleClick && handleClick('RETURN'); | ||
45 | + }} | ||
46 | + > | ||
47 | + <QxIcon | ||
48 | + className={cls(styles[`${prefix}__left-return`])} | ||
49 | + type={'icon-common-arrow'} | ||
50 | + /> | ||
51 | + <div className={cls(styles[`${prefix}__left-title`])}> | ||
52 | + {title || '数据流'} | ||
53 | + </div> | ||
54 | + </div> | ||
55 | + )} | ||
56 | + <div className={cls(styles[`${prefix}__right`])}> | ||
57 | + {!!renderRight ? ( | ||
58 | + renderRight | ||
59 | + ) : ( | ||
60 | + <> | ||
61 | + {type === 'FLOW' ? ( | ||
62 | + <Button | ||
63 | + type="primary" | ||
64 | + // loading={false} | ||
65 | + onClick={() => { | ||
66 | + handleClick && handleClick('ADD'); | ||
67 | + }} | ||
68 | + > | ||
69 | + 新建数据流 | ||
70 | + </Button> | ||
71 | + ) : null} | ||
72 | + </> | ||
73 | + )} | ||
74 | + </div> | ||
75 | + </header> | ||
76 | + ); | ||
77 | +}; | ||
78 | + | ||
79 | +export default FlowHeader; |
1 | +export { default as FlowHeader } from './header'; | ||
2 | +export { default as FlowVersion } from './flow-version'; | ||
3 | +export { default as NameDescriptionSetting } from './name-description-setting'; | ||
4 | +export { default as ConfigDrawer } from './config-drawer'; | ||
5 | +export { default as NodesPanel } from './nodes-panel'; | ||
6 | +export { default as NameEditor } from './name-editor'; | ||
7 | +export { default as QxIcon } from './qx-icon'; |
1 | +.qx-name-description-modal { | ||
2 | + .qx-data-flow-input--clear { | ||
3 | + .fr-item-wrapper .fr-item-status-error input[class~='ant-input'], | ||
4 | + .fr-item-wrapper .fr-item-status-error textarea[class~='ant-input'] { | ||
5 | + &:hover, | ||
6 | + &:focus { | ||
7 | + border: none !important; // 由于该样式使用了!important,所以覆盖还是使用!important | ||
8 | + box-shadow: none !important; | ||
9 | + } | ||
10 | + } | ||
11 | + | ||
12 | + .fr-item-wrapper textarea[class~='ant-input'] + span.ant-input-clear-icon { | ||
13 | + top: unset; | ||
14 | + bottom: 8px; | ||
15 | + } | ||
16 | + } | ||
17 | + | ||
18 | + .ant-modal-body { | ||
19 | + .fr-container .field-block, | ||
20 | + .fr-container .error-message { | ||
21 | + min-height: 24px !important; | ||
22 | + } | ||
23 | + } | ||
24 | +} |
1 | +// 数据流名称编辑通用组件 | ||
2 | +import React, { useImperativeHandle, useState } from 'react'; | ||
3 | +import { Button, Modal, Space } from 'antd'; | ||
4 | +// import { useForm } from '@qx/form-render'; | ||
5 | +import { QxIcon } from '@/components'; | ||
6 | + | ||
7 | +import _ from 'lodash-es'; | ||
8 | +import '@/styles/modal-reset.less'; | ||
9 | +import './index.less'; | ||
10 | +import { Form } from 'antd'; | ||
11 | +// import QxFormRender from '@/packages/qx-form-generator/src/form-render'; | ||
12 | + | ||
13 | +const FORM_SCHEMA: object = { | ||
14 | + type: 'object', | ||
15 | + properties: { | ||
16 | + id: { | ||
17 | + type: 'string', | ||
18 | + hidden: true, | ||
19 | + }, | ||
20 | + name: { | ||
21 | + title: '名称', | ||
22 | + type: 'string', | ||
23 | + max: 20, | ||
24 | + placeholder: '请输入名称', | ||
25 | + required: true, | ||
26 | + props: { | ||
27 | + allowClear: true, | ||
28 | + }, | ||
29 | + className: 'qx-data-flow-input--clear', | ||
30 | + }, | ||
31 | + remark: { | ||
32 | + type: 'string', | ||
33 | + widget: 'textarea', | ||
34 | + title: '说明', | ||
35 | + placeholder: '请输入说明', | ||
36 | + max: 200, | ||
37 | + props: { | ||
38 | + allowClear: true, | ||
39 | + // showCount: true, // 显示字数提示 | ||
40 | + }, | ||
41 | + className: 'qx-data-flow-input--clear', | ||
42 | + }, | ||
43 | + }, | ||
44 | +}; | ||
45 | + | ||
46 | +interface NameDescriptionSettingProps { | ||
47 | + cRef: any; | ||
48 | + handleClick: (type: string, data?: any) => void; // 名称变化调用方法 | ||
49 | +} | ||
50 | + | ||
51 | +const NameDescriptionSetting: React.FC<NameDescriptionSettingProps> = ( | ||
52 | + props, | ||
53 | +) => { | ||
54 | + const { cRef, handleClick } = props; | ||
55 | + const [form] = Form.useForm(); | ||
56 | + const [visible, setVisible] = useState(false); | ||
57 | + | ||
58 | + useImperativeHandle(cRef, () => ({ | ||
59 | + // 暴露给父组件 | ||
60 | + open: (values: any) => { | ||
61 | + setTimeout(() => { | ||
62 | + if (_.isEmpty(values)) { | ||
63 | + form.resetFields(); | ||
64 | + } else { | ||
65 | + form.setValues(values); | ||
66 | + } | ||
67 | + }, 100); | ||
68 | + setVisible(true); | ||
69 | + }, | ||
70 | + })); | ||
71 | + | ||
72 | + const onFinish = async (data: any, error: any) => { | ||
73 | + // console.log('表单数据:',data); | ||
74 | + if (error.length > 0) { | ||
75 | + return; | ||
76 | + } | ||
77 | + // 校验成功 保存名称、说明 | ||
78 | + handleClick('SAVE_NAME', data); | ||
79 | + setVisible(false); | ||
80 | + }; | ||
81 | + | ||
82 | + return ( | ||
83 | + <div> | ||
84 | + <Modal | ||
85 | + width={480} | ||
86 | + title="编辑名称与说明" | ||
87 | + onCancel={() => setVisible(false)} | ||
88 | + keyboard={false} | ||
89 | + maskClosable={false} | ||
90 | + visible={visible} | ||
91 | + footer={ | ||
92 | + <Space size={8}> | ||
93 | + <Button onClick={() => setVisible(false)}>取消</Button> | ||
94 | + <Button | ||
95 | + type={'primary'} | ||
96 | + onClick={() => { | ||
97 | + form.submit(); | ||
98 | + }} | ||
99 | + > | ||
100 | + 保存 | ||
101 | + </Button> | ||
102 | + </Space> | ||
103 | + } | ||
104 | + wrapClassName={'qx-data-flow__modal qx-name-description-modal'} | ||
105 | + closeIcon={ | ||
106 | + <QxIcon | ||
107 | + className={'qx-data-flow__modal__close'} | ||
108 | + type={'icon-close'} | ||
109 | + /> | ||
110 | + } | ||
111 | + destroyOnClose | ||
112 | + > | ||
113 | + <Form | ||
114 | + form={form} | ||
115 | + schema={FORM_SCHEMA} | ||
116 | + onMount={() => {}} | ||
117 | + onFinish={onFinish} | ||
118 | + watch={{}} | ||
119 | + /> | ||
120 | + </Modal> | ||
121 | + </div> | ||
122 | + ); | ||
123 | +}; | ||
124 | + | ||
125 | +export default NameDescriptionSetting; |
src/components/name-editor/index.module.less
0 → 100644
1 | +@prefix-cls: ~'qx-name-editor'; | ||
2 | + | ||
3 | +.@{prefix-cls} { | ||
4 | + width: auto; | ||
5 | + height: 32px; | ||
6 | + display: flex; | ||
7 | + align-items: center; | ||
8 | + justify-content: flex-start; | ||
9 | + font-size: 16px; | ||
10 | + | ||
11 | + :global { | ||
12 | + .anticon { | ||
13 | + font-size: 16px; | ||
14 | + color: @N7; | ||
15 | + } | ||
16 | + } | ||
17 | + | ||
18 | + .@{prefix-cls}__title--edit { | ||
19 | + cursor: pointer; | ||
20 | + | ||
21 | + &:hover { | ||
22 | + color: @B8; | ||
23 | + } | ||
24 | + } | ||
25 | + | ||
26 | + .@{prefix-cls}__input { | ||
27 | + margin-left: 8px !important; | ||
28 | + } | ||
29 | +} | ||
30 | + | ||
31 | +.@{prefix-cls}__title { | ||
32 | + height: 100%; | ||
33 | + display: inline-flex; | ||
34 | + align-items: center; | ||
35 | + justify-content: flex-start; | ||
36 | + | ||
37 | + :global { | ||
38 | + .ant-typography { | ||
39 | + margin: 0 8px; | ||
40 | + font-size: 16px; | ||
41 | + line-height: 1.5; | ||
42 | + color: @N9; | ||
43 | + font-weight: 600; | ||
44 | + } | ||
45 | + } | ||
46 | +} |
src/components/name-editor/index.tsx
0 → 100644
1 | +// 数据流名称编辑通用组件 | ||
2 | +import React, { useEffect, useState } from 'react'; | ||
3 | +import { Input, Typography } from 'antd'; | ||
4 | +import { QxIcon } from '@/components'; | ||
5 | + | ||
6 | +const { Text } = Typography; | ||
7 | +// 样式 | ||
8 | +import cls from 'classnames'; | ||
9 | +import styles from './index.module.less'; | ||
10 | + | ||
11 | +const prefix = 'qx-name-editor'; | ||
12 | + | ||
13 | +interface FlowNameEditorProps { | ||
14 | + noValueView?: string; // 无名称 展示文本 | ||
15 | + nodeName: string; // 节点名称 | ||
16 | + nodeIcon?: React.ReactNode; // 节点 icon | ||
17 | + editTitle: boolean; // 可编辑 参数 | ||
18 | + setEditTitle: (val: boolean) => void; // 可编辑 参数 | ||
19 | + onChange: (data: string) => void; // 名称变化调用方法 | ||
20 | + nameMaxLength?: number; // 名称最长多少 展示省略号 默认 8 | ||
21 | +} | ||
22 | + | ||
23 | +const FlowNameEditor: React.FC<FlowNameEditorProps> = (props) => { | ||
24 | + const { | ||
25 | + noValueView = '未命名', | ||
26 | + nodeName, | ||
27 | + nodeIcon, | ||
28 | + editTitle, | ||
29 | + setEditTitle, | ||
30 | + onChange, | ||
31 | + nameMaxLength, | ||
32 | + } = props; | ||
33 | + | ||
34 | + const [nameLocal, setNameLocal] = useState<string>(''); | ||
35 | + | ||
36 | + const inputRef = React.useRef<any>(null); | ||
37 | + | ||
38 | + useEffect(() => { | ||
39 | + setNameLocal(nodeName); | ||
40 | + }, [nodeName]); | ||
41 | + | ||
42 | + useEffect(() => { | ||
43 | + if (Boolean(editTitle)) { | ||
44 | + inputRef.current!.focus({ | ||
45 | + cursor: 'end', | ||
46 | + }); | ||
47 | + } | ||
48 | + }, [editTitle]); | ||
49 | + | ||
50 | + return ( | ||
51 | + <div className={cls(styles[`${prefix}`])}> | ||
52 | + {!!nodeIcon ? nodeIcon : null} | ||
53 | + {editTitle ? ( | ||
54 | + <Input | ||
55 | + ref={inputRef} | ||
56 | + className={cls(styles[`${prefix}__input`])} | ||
57 | + style={{ | ||
58 | + maxWidth: (nameMaxLength || 8) * 16 * 1.2 + 20 + 'px', | ||
59 | + }} | ||
60 | + value={nameLocal} | ||
61 | + maxLength={20} | ||
62 | + onChange={(e) => setNameLocal(e.target.value)} | ||
63 | + onPressEnter={() => { | ||
64 | + setEditTitle(false); | ||
65 | + if (!nameLocal) { | ||
66 | + setNameLocal(nodeName); | ||
67 | + } | ||
68 | + onChange(nameLocal || nodeName); | ||
69 | + }} | ||
70 | + onBlur={() => { | ||
71 | + setEditTitle(false); | ||
72 | + if (!nameLocal) { | ||
73 | + setNameLocal(nodeName); | ||
74 | + } | ||
75 | + onChange(nameLocal || nodeName); | ||
76 | + }} | ||
77 | + /> | ||
78 | + ) : ( | ||
79 | + <> | ||
80 | + <div | ||
81 | + className={cls(styles[`${prefix}__title`])} | ||
82 | + onClick={() => setEditTitle(true)} | ||
83 | + > | ||
84 | + <Text | ||
85 | + style={{ maxWidth: (nameMaxLength || 8) * 16 * 1.2 + 'px' }} | ||
86 | + ellipsis={{ | ||
87 | + tooltip: nameLocal ? nameLocal : noValueView ? noValueView : '', | ||
88 | + }} | ||
89 | + > | ||
90 | + {nameLocal ? nameLocal : noValueView ? noValueView : ''} | ||
91 | + </Text> | ||
92 | + <QxIcon | ||
93 | + className={cls(styles[`${prefix}__title--edit`])} | ||
94 | + type={'icon-frame-edit'} | ||
95 | + /> | ||
96 | + </div> | ||
97 | + </> | ||
98 | + )} | ||
99 | + </div> | ||
100 | + ); | ||
101 | +}; | ||
102 | + | ||
103 | +export default FlowNameEditor; |
1 | +import React from 'react'; | ||
2 | +import { NodeProps, NodeItemsProps } from './node-list'; | ||
3 | +import { QxIcon } from '@/components'; | ||
4 | +import { CloseOutlined } from '@ant-design/icons'; | ||
5 | +import { useAction } from '@qx/flow'; | ||
6 | + | ||
7 | +const prefix = 'qx-flow-node-panel'; | ||
8 | +const NodeItem: React.FC<NodeItemProps> = (props) => { | ||
9 | + const { nodeItem, isManage, setNodeList, index, parentId } = props; | ||
10 | + | ||
11 | + const { addNode } = useAction(); | ||
12 | + const handleAdd = (nodeType: string) => { | ||
13 | + addNode(nodeType); | ||
14 | + }; | ||
15 | + | ||
16 | + const deleteNode = () => { | ||
17 | + setNodeList((nodeItems: NodeItemsProps[]) => { | ||
18 | + const tempList = [...nodeItems]; | ||
19 | + tempList.forEach((el) => { | ||
20 | + if (el.id === parentId) { | ||
21 | + el.nodes?.splice(index, 1); | ||
22 | + } | ||
23 | + }); | ||
24 | + return tempList; | ||
25 | + }); | ||
26 | + }; | ||
27 | + | ||
28 | + return ( | ||
29 | + <div | ||
30 | + className={`${prefix}__item`} | ||
31 | + onClick={() => { | ||
32 | + if (isManage) return; | ||
33 | + console.log('点击我了,lllll'); | ||
34 | + handleAdd(nodeItem.nodeType); | ||
35 | + }} | ||
36 | + > | ||
37 | + <QxIcon className={`${prefix}__item-icon`} type={'icon-frame-todolist'} /> | ||
38 | + <span className={`${prefix}__item-name`}>{nodeItem.nodeName}</span> | ||
39 | + {isManage ? ( | ||
40 | + <CloseOutlined | ||
41 | + className={`${prefix}__item--close`} | ||
42 | + onClick={() => deleteNode()} | ||
43 | + /> | ||
44 | + ) : null} | ||
45 | + </div> | ||
46 | + ); | ||
47 | +}; | ||
48 | + | ||
49 | +interface NodeItemProps { | ||
50 | + nodeItem: NodeProps; | ||
51 | + isManage?: boolean; | ||
52 | + setNodeList?: any; | ||
53 | + index: number; | ||
54 | + parentId?: string; | ||
55 | +} | ||
56 | + | ||
57 | +export default NodeItem; |
1 | +import React, { useEffect, useState } from 'react'; | ||
2 | +import NodeItem from './node-item'; | ||
3 | +import { QxIcon } from '@/components'; | ||
4 | +import { ReactSortable } from 'react-sortablejs'; | ||
5 | +import { Input } from 'antd'; | ||
6 | + | ||
7 | +import empty from '../img/empty.png'; | ||
8 | + | ||
9 | +const prefix = 'qx-flow-node-panel'; | ||
10 | + | ||
11 | +const NodeList: React.FC<NodeListProps> = (props) => { | ||
12 | + const { listItem, isManage, setNodeList, nodeIndex, setNodeEdit } = props; | ||
13 | + const [open, setOpen] = useState(true); | ||
14 | + const [name, setName] = useState(listItem.name); | ||
15 | + const [edit, setEdit] = useState(listItem?.edit || false); | ||
16 | + | ||
17 | + useEffect(() => { | ||
18 | + setNodeEdit(edit); | ||
19 | + }, [edit]); | ||
20 | + | ||
21 | + const sortableOptionsComponent = { | ||
22 | + disabled: !isManage || edit, | ||
23 | + animation: 150, | ||
24 | + fallbackOnBody: true, | ||
25 | + swapThreshold: 0.65, | ||
26 | + ghostClass: 'qx-node-panel--drag', | ||
27 | + group: 'component', | ||
28 | + onSort: () => { | ||
29 | + if (!open) { | ||
30 | + setOpen(!open); | ||
31 | + } | ||
32 | + }, | ||
33 | + }; | ||
34 | + | ||
35 | + const editName = (_value: string) => { | ||
36 | + if (_value) { | ||
37 | + setNodeList((nodeItems: NodeItemsProps[]) => { | ||
38 | + const tempList = [...nodeItems]; | ||
39 | + tempList.forEach((el) => { | ||
40 | + if (el.id === listItem.id) { | ||
41 | + el.name = _value; | ||
42 | + } | ||
43 | + if (el.edit) delete el.edit; | ||
44 | + }); | ||
45 | + return tempList; | ||
46 | + }); | ||
47 | + } | ||
48 | + setEdit(false); | ||
49 | + }; | ||
50 | + | ||
51 | + const deleteGroup = () => { | ||
52 | + setNodeList((nodeItems: NodeItemsProps[]) => { | ||
53 | + const tempList = [...nodeItems]; | ||
54 | + tempList.splice(nodeIndex, 1); | ||
55 | + return tempList; | ||
56 | + }); | ||
57 | + }; | ||
58 | + | ||
59 | + return ( | ||
60 | + <div className={`${prefix}__list`}> | ||
61 | + <div className={`${prefix}__list-title`}> | ||
62 | + <div className={`${prefix}__list-title-left`}> | ||
63 | + <QxIcon | ||
64 | + className={`${prefix}__left-icon`} | ||
65 | + type={open ? 'icon-down' : 'icon-right'} | ||
66 | + onClick={() => setOpen(!open)} | ||
67 | + /> | ||
68 | + {edit ? ( | ||
69 | + <Input | ||
70 | + defaultValue={name} | ||
71 | + value={name} | ||
72 | + autoFocus | ||
73 | + onChange={(e) => setName(e.target.value)} | ||
74 | + onBlur={(e) => { | ||
75 | + const _value = e.target.value; | ||
76 | + editName(_value); | ||
77 | + }} | ||
78 | + onPressEnter={(e: any) => { | ||
79 | + const _value = e.target.value; | ||
80 | + editName(_value); | ||
81 | + }} | ||
82 | + /> | ||
83 | + ) : ( | ||
84 | + <span | ||
85 | + className={`${prefix}__left-span`} | ||
86 | + onClick={() => { | ||
87 | + if (!isManage) return; | ||
88 | + setEdit(true); | ||
89 | + }} | ||
90 | + > | ||
91 | + {listItem.name} | ||
92 | + </span> | ||
93 | + )} | ||
94 | + </div> | ||
95 | + {!listItem.nodes?.length ? ( | ||
96 | + <QxIcon | ||
97 | + className={`${prefix}__list-title-right`} | ||
98 | + type={'icon-frame-delete'} | ||
99 | + onClick={() => deleteGroup()} | ||
100 | + /> | ||
101 | + ) : null} | ||
102 | + </div> | ||
103 | + <ReactSortable | ||
104 | + className={`${prefix}__list-box`} | ||
105 | + list={listItem?.nodes || []} | ||
106 | + setList={(list) => { | ||
107 | + setNodeList((nodeItems: NodeItemsProps[]) => { | ||
108 | + const tempList = [...nodeItems]; | ||
109 | + tempList[nodeIndex]['nodes'] = list; | ||
110 | + return tempList; | ||
111 | + }); | ||
112 | + }} | ||
113 | + {...sortableOptionsComponent} | ||
114 | + > | ||
115 | + {!open ? null : ( | ||
116 | + <> | ||
117 | + {listItem?.nodes?.length ? ( | ||
118 | + <> | ||
119 | + {(listItem?.nodes || []).map((item, index) => { | ||
120 | + return ( | ||
121 | + <NodeItem | ||
122 | + key={item.id} | ||
123 | + nodeItem={item || []} | ||
124 | + isManage={isManage} | ||
125 | + setNodeList={setNodeList} | ||
126 | + index={index} | ||
127 | + parentId={listItem.id} | ||
128 | + /> | ||
129 | + ); | ||
130 | + })} | ||
131 | + </> | ||
132 | + ) : ( | ||
133 | + <div className={`${prefix}--empty`}> | ||
134 | + <img src={empty} alt={'暂无数据'} /> | ||
135 | + <span>暂无数据</span> | ||
136 | + </div> | ||
137 | + )} | ||
138 | + </> | ||
139 | + )} | ||
140 | + </ReactSortable> | ||
141 | + </div> | ||
142 | + ); | ||
143 | +}; | ||
144 | +export interface NodeProps { | ||
145 | + id: string; | ||
146 | + groupId: string; | ||
147 | + nodeMode: string; | ||
148 | + nodeType: string; | ||
149 | + nodeName: string; | ||
150 | + configJson: any; // 待规划 | ||
151 | +} | ||
152 | +export interface NodeItemsProps { | ||
153 | + id: string; | ||
154 | + name: string; | ||
155 | + nodes?: NodeProps[]; | ||
156 | + disabled: boolean; | ||
157 | + edit?: boolean; | ||
158 | +} | ||
159 | +interface NodeListProps { | ||
160 | + listItem: NodeItemsProps; | ||
161 | + isManage?: boolean; | ||
162 | + setNodeList?: any; | ||
163 | + nodeIndex: number; | ||
164 | + setNodeEdit: (status: boolean) => void; | ||
165 | +} | ||
166 | + | ||
167 | +export default NodeList; |
src/components/nodes-panel/img/empty.png
0 → 100644
13.1 KB
src/components/nodes-panel/index.less
0 → 100644
1 | +.qx-flow-node-panel { | ||
2 | + .ant-drawer-body { | ||
3 | + padding: 0; | ||
4 | + overflow: hidden; | ||
5 | + } | ||
6 | + &__title { | ||
7 | + height: 56px; | ||
8 | + padding: 16px; | ||
9 | + display: flex; | ||
10 | + align-items: center; | ||
11 | + justify-content: space-between; | ||
12 | + color: @N9; | ||
13 | + border-bottom: 1px solid @N4; | ||
14 | + .qx-flow-node-panel__title-left { | ||
15 | + font-size: 16px; | ||
16 | + font-weight: 600; | ||
17 | + } | ||
18 | + .qx-flow-node-panel__title-right { | ||
19 | + font-size: 14px; | ||
20 | + span { | ||
21 | + cursor: pointer; | ||
22 | + margin-left: 12px; | ||
23 | + transition: all 0.3s; | ||
24 | + } | ||
25 | + } | ||
26 | + } | ||
27 | + | ||
28 | + &--save { | ||
29 | + color: @B8; | ||
30 | + &:hover { | ||
31 | + color: @B7; | ||
32 | + } | ||
33 | + } | ||
34 | + | ||
35 | + &--manage { | ||
36 | + &:hover { | ||
37 | + color: @B8; | ||
38 | + } | ||
39 | + } | ||
40 | + | ||
41 | + &__tabs { | ||
42 | + .ant-tabs-nav { | ||
43 | + margin: 0 !important; | ||
44 | + &::before { | ||
45 | + border: none; | ||
46 | + } | ||
47 | + .ant-tabs-nav-list { | ||
48 | + width: 100%; | ||
49 | + | ||
50 | + .ant-tabs-tab { | ||
51 | + flex: 1; | ||
52 | + display: flex; | ||
53 | + align-items: center; | ||
54 | + justify-content: center; | ||
55 | + font-weight: 600; | ||
56 | + color: @N8; | ||
57 | + padding: 9px 0; | ||
58 | + .ant-tabs-tab-btn { | ||
59 | + } | ||
60 | + } | ||
61 | + .ant-tabs-tab-active { | ||
62 | + position: relative; | ||
63 | + &::before { | ||
64 | + content: ''; | ||
65 | + position: absolute; | ||
66 | + left: 50%; | ||
67 | + bottom: 0; | ||
68 | + width: 88px; | ||
69 | + height: 2px; | ||
70 | + margin-left: -44px; | ||
71 | + border-radius: 2px; | ||
72 | + background-color: @B8; | ||
73 | + transition: | ||
74 | + width 0.3s, | ||
75 | + left 0.3s, | ||
76 | + right 0.3s; | ||
77 | + } | ||
78 | + } | ||
79 | + | ||
80 | + .ant-tabs-ink-bar { | ||
81 | + display: none; | ||
82 | + } | ||
83 | + } | ||
84 | + } | ||
85 | + } | ||
86 | + | ||
87 | + &__content { | ||
88 | + height: calc(100vh - 56px - 40px); | ||
89 | + overflow-y: auto; | ||
90 | + padding: 16px; | ||
91 | + | ||
92 | + &--manage { | ||
93 | + height: calc(100vh - 56px - 40px - 44px); | ||
94 | + } | ||
95 | + } | ||
96 | + | ||
97 | + &__list { | ||
98 | + margin-bottom: 16px; | ||
99 | + width: 100%; | ||
100 | + | ||
101 | + .qx-flow-node-panel__list-title { | ||
102 | + height: 32px; | ||
103 | + display: flex; | ||
104 | + align-items: center; | ||
105 | + justify-content: space-between; | ||
106 | + color: @N7; | ||
107 | + font-size: 14px; | ||
108 | + width: 100%; | ||
109 | + } | ||
110 | + | ||
111 | + .qx-flow-node-panel__list-title-left { | ||
112 | + display: flex; | ||
113 | + align-items: center; | ||
114 | + flex: 1; | ||
115 | + | ||
116 | + input { | ||
117 | + flex: 1; | ||
118 | + } | ||
119 | + .qx-flow-node-panel__left-icon { | ||
120 | + margin-right: 4px; | ||
121 | + width: 14px; | ||
122 | + height: 14px; | ||
123 | + } | ||
124 | + | ||
125 | + .qx-flow-node-panel__left-span { | ||
126 | + cursor: pointer; | ||
127 | + font-weight: 600; | ||
128 | + flex: 1; | ||
129 | + overflow: hidden; | ||
130 | + text-overflow: ellipsis; | ||
131 | + white-space: nowrap; | ||
132 | + width: 300px; | ||
133 | + } | ||
134 | + } | ||
135 | + | ||
136 | + .qx-flow-node-panel__list-title-right { | ||
137 | + margin-left: 18px; | ||
138 | + cursor: pointer; | ||
139 | + } | ||
140 | + | ||
141 | + .qx-flow-node-panel__list-box { | ||
142 | + display: flex; | ||
143 | + flex-wrap: wrap; | ||
144 | + align-items: center; | ||
145 | + transition: all 0.3s; | ||
146 | + } | ||
147 | + } | ||
148 | + &__item { | ||
149 | + margin: 8px 8px 0 0; | ||
150 | + width: 30%; | ||
151 | + height: 40px; | ||
152 | + border-radius: 4px; | ||
153 | + border: 1px solid @N4; | ||
154 | + background: @N2; | ||
155 | + padding: 0 8px; | ||
156 | + display: flex; | ||
157 | + align-items: center; | ||
158 | + max-width: 142px; | ||
159 | + flex: 1; | ||
160 | + position: relative; | ||
161 | + cursor: pointer; | ||
162 | + | ||
163 | + &:nth-child(3n) { | ||
164 | + margin-right: 0; | ||
165 | + } | ||
166 | + | ||
167 | + .qx-flow-node-panel__item-icon { | ||
168 | + width: 28px; | ||
169 | + height: 28px; | ||
170 | + border-radius: 14px; | ||
171 | + background-color: pink; | ||
172 | + color: #fff; | ||
173 | + font-size: 16px; | ||
174 | + display: inline-flex; | ||
175 | + align-items: center; | ||
176 | + justify-content: center; | ||
177 | + } | ||
178 | + | ||
179 | + .qx-flow-node-panel__item-name { | ||
180 | + margin-left: 8px; | ||
181 | + flex: 1; | ||
182 | + overflow: hidden; | ||
183 | + text-overflow: ellipsis; | ||
184 | + white-space: nowrap; | ||
185 | + color: @N9; | ||
186 | + font-size: 14px; | ||
187 | + } | ||
188 | + } | ||
189 | + | ||
190 | + .qx-flow-node-panel__item--close { | ||
191 | + position: absolute; | ||
192 | + width: 16px; | ||
193 | + height: 16px; | ||
194 | + border-radius: 8px; | ||
195 | + color: #fff; | ||
196 | + background-color: @E3; | ||
197 | + top: -8px; | ||
198 | + right: -8px; | ||
199 | + font-size: 8px; | ||
200 | + display: flex; | ||
201 | + align-items: center; | ||
202 | + justify-content: center; | ||
203 | + cursor: pointer; | ||
204 | + } | ||
205 | + | ||
206 | + &--empty { | ||
207 | + width: 100%; | ||
208 | + display: flex; | ||
209 | + align-items: center; | ||
210 | + justify-content: center; | ||
211 | + flex-direction: column; | ||
212 | + margin-top: 8px; | ||
213 | + img { | ||
214 | + width: 190px; | ||
215 | + height: 126px; | ||
216 | + } | ||
217 | + span { | ||
218 | + margin-top: 4px; | ||
219 | + line-height: 22px; | ||
220 | + font-weight: 600; | ||
221 | + color: @N7; | ||
222 | + } | ||
223 | + | ||
224 | + &-max { | ||
225 | + margin-top: 24px; | ||
226 | + } | ||
227 | + } | ||
228 | + | ||
229 | + .qx-flow-node-panel__loading { | ||
230 | + width: 100%; | ||
231 | + margin-top: 100px; | ||
232 | + } | ||
233 | + | ||
234 | + &__opt { | ||
235 | + position: static; | ||
236 | + padding: 16px 16px 0; | ||
237 | + height: 44px; | ||
238 | + background-color: #fff; | ||
239 | + color: @B8; | ||
240 | + display: flex; | ||
241 | + align-items: center; | ||
242 | + | ||
243 | + &-icon { | ||
244 | + margin-right: 16px; | ||
245 | + cursor: pointer; | ||
246 | + &:hover { | ||
247 | + color: @B7; | ||
248 | + } | ||
249 | + > span { | ||
250 | + margin-right: 4px; | ||
251 | + } | ||
252 | + } | ||
253 | + } | ||
254 | +} |
src/components/nodes-panel/index.tsx
0 → 100644
1 | +import React, { memo, useContext, useEffect, useState } from 'react'; | ||
2 | +import { BuilderContext } from '@qx/flow'; | ||
3 | +import { Drawer, Spin, Tabs } from 'antd'; | ||
4 | +import NodeList, { NodeItemsProps } from './components/node-list'; | ||
5 | +import { useLocation, useSearchParams } from 'react-router-dom'; | ||
6 | +import { getListGroupNode } from '@/services'; | ||
7 | +import { QxIcon } from '@/components'; | ||
8 | +import { ReactSortable } from 'react-sortablejs'; | ||
9 | +import { cloneDeep } from 'lodash-es'; | ||
10 | +import { v4 as uuid } from 'uuid'; | ||
11 | +import type { INodePanelProps } from '@qx/flow'; | ||
12 | +import Empty from './img/empty.png'; | ||
13 | + | ||
14 | +import './index.less'; | ||
15 | + | ||
16 | +const prefix = 'qx-flow-node-panel'; | ||
17 | + | ||
18 | +const panelTabs = [ | ||
19 | + { | ||
20 | + name: '系统节点', | ||
21 | + key: 'SYSTEM', | ||
22 | + }, | ||
23 | + { | ||
24 | + name: '扩展节点', | ||
25 | + key: 'EXTEND', | ||
26 | + }, | ||
27 | + { | ||
28 | + name: '开发节点', | ||
29 | + key: 'DEVELOP', | ||
30 | + }, | ||
31 | +]; | ||
32 | + | ||
33 | +const NodePanel: React.FC<NodePanelProps> = (props) => { | ||
34 | + const { visible, onClose } = props; | ||
35 | + // @ts-ignore | ||
36 | + const [searchParams] = useSearchParams(); | ||
37 | + const appId = searchParams.get('appId'); | ||
38 | + const { registerNodes } = useContext(BuilderContext); | ||
39 | + const [loading, setLoading] = useState(false); | ||
40 | + const [activeKey, setActiveKey] = useState('SYSTEM'); | ||
41 | + const [isManage, setIsManage] = useState<boolean>(false); | ||
42 | + const [nodeList, setNodeList] = useState<NodeItemsProps[]>([]); | ||
43 | + const [originalNodeList, setOriginalNodeList] = useState<NodeItemsProps[]>( | ||
44 | + [], | ||
45 | + ); | ||
46 | + const [nodeEdit, setNodeEdit] = useState(false); | ||
47 | + const [nameList, setNameList] = useState<string[]>([]); | ||
48 | + // console.log('registerNodes', registerNodes, props); | ||
49 | + | ||
50 | + useEffect(() => { | ||
51 | + if (!isManage) return; | ||
52 | + if (!nodeList?.length) { | ||
53 | + setNameList([]); | ||
54 | + } else { | ||
55 | + const _list: string[] = []; | ||
56 | + nodeList.forEach((el) => { | ||
57 | + if (el.name.includes('未命名分组(')) { | ||
58 | + _list.push(el.name); | ||
59 | + } | ||
60 | + }); | ||
61 | + setNameList(_list); | ||
62 | + } | ||
63 | + }, [JSON.stringify(nodeList), isManage]); | ||
64 | + | ||
65 | + useEffect(() => { | ||
66 | + setIsManage(false); | ||
67 | + setNameList([]); | ||
68 | + if (!appId || !visible) return; | ||
69 | + setLoading(true); | ||
70 | + // TODO | ||
71 | + getListGroupNode(appId, { | ||
72 | + type: activeKey, | ||
73 | + }) | ||
74 | + .then((data: NodeItemsProps[]) => { | ||
75 | + setLoading(false); | ||
76 | + if (data?.length) { | ||
77 | + setOriginalNodeList(cloneDeep(data)); | ||
78 | + setNodeList(data.filter((el) => !el.disabled) || []); | ||
79 | + return; | ||
80 | + } | ||
81 | + setNodeList([]); | ||
82 | + setOriginalNodeList([]); | ||
83 | + }) | ||
84 | + .catch(() => { | ||
85 | + setLoading(false); | ||
86 | + setNodeList([]); | ||
87 | + setOriginalNodeList([]); | ||
88 | + }); | ||
89 | + }, [activeKey, visible]); | ||
90 | + | ||
91 | + useEffect(() => { | ||
92 | + if (!visible) { | ||
93 | + setActiveKey('SYSTEM'); | ||
94 | + } | ||
95 | + }, [visible]); | ||
96 | + | ||
97 | + // const detailNodeList = (_isManage: boolean) => { | ||
98 | + // return nodeList.filter((el) => _isManage || !el.disabled) || []; | ||
99 | + // }; | ||
100 | + | ||
101 | + const sortableOptionsPage = { | ||
102 | + disabled: !isManage || nodeEdit, | ||
103 | + animation: 150, | ||
104 | + fallbackOnBody: true, | ||
105 | + swapThreshold: 0.65, | ||
106 | + ghostClass: 'qx-node-panel--drag', | ||
107 | + group: 'page', | ||
108 | + }; | ||
109 | + | ||
110 | + const onExit = () => { | ||
111 | + setIsManage(false); | ||
112 | + setNodeList(cloneDeep(originalNodeList)); | ||
113 | + }; | ||
114 | + | ||
115 | + useEffect(() => { | ||
116 | + const _data = cloneDeep(originalNodeList); | ||
117 | + setNodeList(_data.filter((el) => isManage || !el.disabled) || []); | ||
118 | + }, [isManage]); | ||
119 | + const generateUniqueName = () => { | ||
120 | + if (nameList?.length) { | ||
121 | + let numberList: number[] = []; | ||
122 | + nameList.forEach((el) => { | ||
123 | + const a = /未命名分组\((.*?)\)/gi; | ||
124 | + const b = el.match(a); | ||
125 | + const c = el.split(b?.[0] || ''); // 判断前后是否还有字符 | ||
126 | + const _firstStr = b?.[0].replace(a, '$1'); | ||
127 | + if (!isNaN(Number(_firstStr)) && !c[1] && !c[0]) { | ||
128 | + numberList.push(Number(_firstStr)); | ||
129 | + } | ||
130 | + }); | ||
131 | + if (numberList.length) { | ||
132 | + numberList = numberList.sort(); | ||
133 | + const _length = numberList.length; | ||
134 | + for (let i = 1; i <= numberList[_length - 1]; i++) { | ||
135 | + if (!numberList.includes(i)) { | ||
136 | + return `未命名分组(${i})`; | ||
137 | + } | ||
138 | + } | ||
139 | + return `未命名分组(${_length + 1})`; | ||
140 | + } | ||
141 | + return '未命名分组(1)'; | ||
142 | + } | ||
143 | + return '未命名分组(1)'; | ||
144 | + }; | ||
145 | + | ||
146 | + // 新增分组 | ||
147 | + const addGroup = () => { | ||
148 | + setNodeList((list) => { | ||
149 | + const _list = cloneDeep(list || []); | ||
150 | + _list.push({ | ||
151 | + id: uuid(), | ||
152 | + name: generateUniqueName(), | ||
153 | + disabled: false, | ||
154 | + nodes: [], | ||
155 | + edit: true, | ||
156 | + }); | ||
157 | + return _list; | ||
158 | + }); | ||
159 | + }; | ||
160 | + | ||
161 | + const saveData = () => { | ||
162 | + console.log(nodeList, 'llllllnodeList'); | ||
163 | + }; | ||
164 | + | ||
165 | + return ( | ||
166 | + <Drawer | ||
167 | + visible={visible} | ||
168 | + onClose={onClose} | ||
169 | + width={475} | ||
170 | + className={`${prefix}`} | ||
171 | + title={null} | ||
172 | + closable={false} | ||
173 | + > | ||
174 | + <div className={`${prefix}__title`}> | ||
175 | + <div className={`${prefix}__title-left`}>节点类型</div> | ||
176 | + <div className={`${prefix}__title-right`}> | ||
177 | + {activeKey !== 'SYSTEM' && !isManage ? ( | ||
178 | + <span | ||
179 | + className={`${prefix}--manage`} | ||
180 | + onClick={() => setIsManage(true)} | ||
181 | + > | ||
182 | + 管理 | ||
183 | + </span> | ||
184 | + ) : null} | ||
185 | + {isManage ? ( | ||
186 | + <> | ||
187 | + <span onClick={() => onExit()}>退出</span> | ||
188 | + <span className={`${prefix}--save`} onClick={() => saveData()}> | ||
189 | + 保存 | ||
190 | + </span> | ||
191 | + </> | ||
192 | + ) : null} | ||
193 | + </div> | ||
194 | + </div> | ||
195 | + <Tabs | ||
196 | + activeKey={activeKey} | ||
197 | + className={`${prefix}__tabs`} | ||
198 | + onChange={(key) => setActiveKey(key)} | ||
199 | + > | ||
200 | + {panelTabs.map((item) => { | ||
201 | + return ( | ||
202 | + <Tabs.TabPane tab={item.name} key={item.key}> | ||
203 | + {isManage && !loading ? ( | ||
204 | + <div className={`${prefix}__opt`}> | ||
205 | + <span | ||
206 | + className={`${prefix}__opt-icon`} | ||
207 | + onClick={() => addGroup()} | ||
208 | + > | ||
209 | + <QxIcon | ||
210 | + className={`${prefix}__left-icon`} | ||
211 | + type={'icon-frame-todolist'} | ||
212 | + /> | ||
213 | + 分组 | ||
214 | + </span> | ||
215 | + {activeKey === 'DEVELOP' ? ( | ||
216 | + <span className={`${prefix}__opt-icon`} onClick={() => {}}> | ||
217 | + <QxIcon | ||
218 | + className={`${prefix}__left-icon`} | ||
219 | + type={'icon-frame-todolist'} | ||
220 | + /> | ||
221 | + 开发节点管理 | ||
222 | + </span> | ||
223 | + ) : null} | ||
224 | + </div> | ||
225 | + ) : null} | ||
226 | + <div | ||
227 | + className={`${prefix}__content ${ | ||
228 | + isManage ? `${prefix}__content--manage` : '' | ||
229 | + }`} | ||
230 | + > | ||
231 | + {loading ? ( | ||
232 | + <Spin className={`${prefix}__loading`} /> | ||
233 | + ) : ( | ||
234 | + <> | ||
235 | + {nodeList?.length ? ( | ||
236 | + <ReactSortable | ||
237 | + list={nodeList} | ||
238 | + setList={setNodeList} | ||
239 | + {...sortableOptionsPage} | ||
240 | + > | ||
241 | + {nodeList.map((bl, index) => { | ||
242 | + return ( | ||
243 | + <NodeList | ||
244 | + key={bl.id} | ||
245 | + listItem={bl} | ||
246 | + isManage={isManage} | ||
247 | + setNodeList={setNodeList} | ||
248 | + nodeIndex={index} | ||
249 | + setNodeEdit={setNodeEdit} | ||
250 | + /> | ||
251 | + ); | ||
252 | + })} | ||
253 | + </ReactSortable> | ||
254 | + ) : ( | ||
255 | + <div className={`${prefix}--empty ${prefix}--empty-max`}> | ||
256 | + <img src={Empty} alt={'暂无数据'} /> | ||
257 | + <span>暂无数据</span> | ||
258 | + </div> | ||
259 | + )} | ||
260 | + </> | ||
261 | + )} | ||
262 | + </div> | ||
263 | + </Tabs.TabPane> | ||
264 | + ); | ||
265 | + })} | ||
266 | + </Tabs> | ||
267 | + </Drawer> | ||
268 | + ); | ||
269 | +}; | ||
270 | + | ||
271 | +interface NodePanelProps extends INodePanelProps {} | ||
272 | + | ||
273 | +export default memo(NodePanel); |
src/components/qx-icon/index.ts
0 → 100644
src/hooks/index.ts
0 → 100644
1 | +export * from './useTheme'; |
src/hooks/useSdk.ts
0 → 100644
src/hooks/useTheme.tsx
0 → 100644
1 | +import { useEffect } from 'react'; | ||
2 | +import { ConfigProvider } from 'antd'; | ||
3 | +import { Outlet } from 'react-router-dom'; | ||
4 | +import { defaultTheme, setTheme } from '@qx/ui'; | ||
5 | + | ||
6 | +export const useTheme = () => { | ||
7 | + const userTheme = JSON.parse( | ||
8 | + window.localStorage.getItem('qx-system-theme') || 'null', | ||
9 | + ); | ||
10 | + | ||
11 | + const theme = { | ||
12 | + token: { | ||
13 | + colorPrimary: userTheme?.themeColor || defaultTheme.themeColor, | ||
14 | + borderRadius: 4, | ||
15 | + }, | ||
16 | + }; | ||
17 | + | ||
18 | + const handleGenerateThemeColors = () => { | ||
19 | + if (!userTheme) { | ||
20 | + setTheme(defaultTheme.themeColor); | ||
21 | + } | ||
22 | + }; | ||
23 | + | ||
24 | + useEffect(() => { | ||
25 | + handleGenerateThemeColors(); | ||
26 | + ConfigProvider.config({ | ||
27 | + theme, | ||
28 | + }); | ||
29 | + }, []); | ||
30 | + | ||
31 | + return ( | ||
32 | + <ConfigProvider theme={theme}> | ||
33 | + <Outlet /> | ||
34 | + </ConfigProvider> | ||
35 | + ); | ||
36 | +}; |
src/interface.ts
0 → 100644
1 | +export type FlowVersionStatusProps = | ||
2 | + | 'DRAFT' | ||
3 | + | 'DISABLED' | ||
4 | + | 'PUBLISHED' | ||
5 | + | 'EDIT' | ||
6 | + | 'DELETED'; | ||
7 | +export type FlowStatusProps = | ||
8 | + | 'UNPUBLISHED' | ||
9 | + | 'DISABLED' | ||
10 | + | 'PUBLISHED' | ||
11 | + | 'MODIFY' | ||
12 | + | 'DELETED'; | ||
13 | + | ||
14 | +export const FlowVersionStatusEnums = { | ||
15 | + DRAFT: '未发布', | ||
16 | + DISABLED: '已停用', | ||
17 | + PUBLISHED: '已发布', | ||
18 | + EDIT: '有修改', | ||
19 | + DELETED: '已删除', | ||
20 | +}; | ||
21 | + | ||
22 | +export const FlowStatusEnums = { | ||
23 | + UNPUBLISHED: '未发布', | ||
24 | + DISABLED: '已停用', | ||
25 | + PUBLISHED: '已发布', | ||
26 | + MODIFY: '有修改', | ||
27 | + DELETED: '已删除', | ||
28 | +}; | ||
29 | + | ||
30 | +export enum NodeTypes { | ||
31 | + /** | ||
32 | + * 开始 | ||
33 | + */ | ||
34 | + START = 'DF_START', | ||
35 | + /** | ||
36 | + * 结束 | ||
37 | + */ | ||
38 | + END = 'DF_END', | ||
39 | + /** | ||
40 | + * 查询单条记录 | ||
41 | + */ | ||
42 | + DQ_MODEL = 'DF_DQ_MODEL', | ||
43 | + /** | ||
44 | + * 查询多条记录 | ||
45 | + */ | ||
46 | + DQ_MODEL_MORE = 'DF_DQ_MODEL_MORE', | ||
47 | + /** | ||
48 | + * 新增记录 | ||
49 | + */ | ||
50 | + DO_C = 'DF_DO_C', | ||
51 | + /** | ||
52 | + * 查询部门 | ||
53 | + */ | ||
54 | + DQ_ORG = 'DF_DQ_ORG', | ||
55 | + /** | ||
56 | + * 查询人员 | ||
57 | + */ | ||
58 | + DQ_USER = 'DF_DQ_USER', | ||
59 | + /** | ||
60 | + * 填写记录 | ||
61 | + */ | ||
62 | + DO_FILL = 'DF_DO_FILL', | ||
63 | + /** | ||
64 | + * 延迟执行 | ||
65 | + */ | ||
66 | + APRV_DLAY = 'DF_APRV_DLAY', | ||
67 | + /** | ||
68 | + * 服务调用 | ||
69 | + */ | ||
70 | + WEB_HOOK = 'DF_WEB_HOOK', | ||
71 | + /** | ||
72 | + * 脚本节点 | ||
73 | + */ | ||
74 | + CODE = 'DF_CODE', | ||
75 | + /** | ||
76 | + * 删除记录 | ||
77 | + */ | ||
78 | + DO_D = 'DF_DO_D', | ||
79 | + /** | ||
80 | + * 更新记录 | ||
81 | + */ | ||
82 | + DO_U = 'DF_DO_U', | ||
83 | + /** | ||
84 | + * 设置参数 | ||
85 | + */ | ||
86 | + DO_PARAM = 'DF_DO_PARAM', | ||
87 | + /** | ||
88 | + * 公式运算 | ||
89 | + */ | ||
90 | + DO_CAL = 'DF_DO_CAL', | ||
91 | +} | ||
92 | + | ||
93 | +export interface ProcessItemProps { | ||
94 | + name?: string; | ||
95 | + appId?: string; | ||
96 | + id?: string; | ||
97 | + funCode?: string; | ||
98 | + processJson?: string; | ||
99 | + remark?: string; | ||
100 | + funId?: string; | ||
101 | + latestConfigStatus?: FlowStatusProps; | ||
102 | + curVersion?: string | number; | ||
103 | +} |
1 | import React, { memo } from 'react'; | 1 | import React, { memo } from 'react'; |
2 | -import './styles.less'; | ||
3 | 2 | ||
4 | const About: React.FC<AboutProps> = (props) => { | 3 | const About: React.FC<AboutProps> = (props) => { |
5 | - return <div className="about">About</div>; | 4 | + return <div>About</div>; |
6 | }; | 5 | }; |
7 | 6 | ||
8 | interface AboutProps {} | 7 | interface AboutProps {} |
src/page/about/styles.less
deleted
100644 → 0
src/page/designer/index.tsx
0 → 100644
1 | +import React, { useEffect, useRef, useState, useContext } from 'react'; | ||
2 | +import FlowBuilder, { IQxNode, dataConversion, BuilderContext } from '@qx/flow'; | ||
3 | +import { | ||
4 | + Button, | ||
5 | + Drawer, | ||
6 | + message, | ||
7 | + Popconfirm, | ||
8 | + Popover, | ||
9 | + Space, | ||
10 | + Tooltip, | ||
11 | + Typography, | ||
12 | + Dropdown, | ||
13 | + Menu, | ||
14 | + Modal, | ||
15 | +} from 'antd'; | ||
16 | +import { Start, End } from './nodes'; | ||
17 | +import { NodeTypes, ProcessItemProps, FlowStatusEnums } from '@/interface'; | ||
18 | +import { handleWindowOpen } from '@qx/utils'; | ||
19 | +// import { history } from '@@/core/history'; | ||
20 | +import { useLocation, useSearchParams } from 'react-router-dom'; | ||
21 | +import { useTitle } from 'ahooks'; | ||
22 | +// 头部相关 | ||
23 | +import { | ||
24 | + FlowHeader, | ||
25 | + FlowVersion, | ||
26 | + NameDescriptionSetting, | ||
27 | + NodesPanel, | ||
28 | + ConfigDrawer, | ||
29 | +} from '@/components'; | ||
30 | +import cls from 'classnames'; | ||
31 | +import styles from '@/components/header/index.module.less'; | ||
32 | +import { getIcon } from '@/utils'; | ||
33 | +import { QxIcon } from '@/components'; | ||
34 | +import { | ||
35 | + getProcessDetailsById, | ||
36 | + publishProcessById, | ||
37 | + saveProcessSetting, | ||
38 | +} from '@/services'; | ||
39 | +import type { IRegisterNode, INode, IRegisterRemoteNode } from '@qx/flow'; | ||
40 | + | ||
41 | +const prefix = 'qx-data-flow'; | ||
42 | + | ||
43 | +const { Text } = Typography; | ||
44 | + | ||
45 | +const nameMaxLength = 20; // 数据流名称 最长文字 限制 | ||
46 | + | ||
47 | +// TODO: 后续 systemNodes 要删掉,所有节点都是 registerRemoteNode | ||
48 | +const systemNodes: IRegisterNode[] = [ | ||
49 | + { | ||
50 | + type: NodeTypes.START, | ||
51 | + name: '开始节点', | ||
52 | + // displayComponent: Start.DisplayComponent, | ||
53 | + // configComponent: Start.ConfigComponent, | ||
54 | + isStart: true, | ||
55 | + }, | ||
56 | + { | ||
57 | + type: NodeTypes.END, | ||
58 | + name: '结束节点', | ||
59 | + // displayComponent: End.DisplayComponent, | ||
60 | + // configComponent: End.ConfigComponent, | ||
61 | + isEnd: true, | ||
62 | + }, | ||
63 | + // { | ||
64 | + // type: 'condition', | ||
65 | + // name: '条件节点', | ||
66 | + // displayComponent: Condition.DisplayComponent, | ||
67 | + // configComponent: Condition.ConfigComponent, | ||
68 | + // }, | ||
69 | +]; | ||
70 | + | ||
71 | +export default () => { | ||
72 | + const [nodes, setNodes] = useState<INode[]>([]); | ||
73 | + const [remoteNodes, setRemoteNodes] = useState<IRegisterRemoteNode[]>([]); | ||
74 | + // @ts-ignore | ||
75 | + const [searchParams] = useSearchParams(); | ||
76 | + const [showEditTitle, setShowEditTitle] = useState(false); | ||
77 | + const { | ||
78 | + appId, // 应用id | ||
79 | + funCode, // 表单id | ||
80 | + returnUrl, // 来自页面跳转返回可跳转 | ||
81 | + processId, // 流程Id preview | ||
82 | + type, // 流程类型 edit 编辑 preview 预览 | ||
83 | + } = { | ||
84 | + appId: searchParams.get('appId'), | ||
85 | + funCode: searchParams.get('funCode'), | ||
86 | + returnUrl: searchParams.get('returnUrl'), | ||
87 | + processId: searchParams.get('processId'), | ||
88 | + type: searchParams.get('type'), | ||
89 | + }; | ||
90 | + | ||
91 | + const [processDetails, setProcessDetails] = useState<ProcessItemProps>({ | ||
92 | + id: '', | ||
93 | + appId: '', | ||
94 | + name: '', | ||
95 | + remark: '', | ||
96 | + }); // 数据流 详细数据 | ||
97 | + | ||
98 | + const [showExtendModal, setShowExtendModal] = useState(false); | ||
99 | + const [showSimple, setShowSimple] = useState<boolean>(false); | ||
100 | + | ||
101 | + useTitle(processDetails?.name || '未命名流程'); // 动态变化 文档标题 | ||
102 | + | ||
103 | + const formModalRef = useRef({ | ||
104 | + open: (data?: any) => {}, | ||
105 | + }); | ||
106 | + | ||
107 | + // 获取详情 | ||
108 | + const getDetailsData = (_id: string, _data?: ProcessItemProps) => { | ||
109 | + getProcessDetailsById(_id) | ||
110 | + .then((res: any) => { | ||
111 | + setProcessDetails({ | ||
112 | + ..._data, | ||
113 | + ...(res || {}), | ||
114 | + }); | ||
115 | + }) | ||
116 | + .catch((err: { msg?: string }) => { | ||
117 | + message.warning(err?.msg || '流程详情数据请求失败', 5); | ||
118 | + setProcessDetails( | ||
119 | + _data || { | ||
120 | + id: _id, | ||
121 | + name: '未命名流程', | ||
122 | + appId, | ||
123 | + }, | ||
124 | + ); | ||
125 | + }); | ||
126 | + }; | ||
127 | + | ||
128 | + // 点击事件 | ||
129 | + const handleClick = ( | ||
130 | + type: string, | ||
131 | + data?: any, | ||
132 | + e?: React.MouseEvent<HTMLElement, MouseEvent>, | ||
133 | + ) => { | ||
134 | + const { host, pathname, protocol, hash } = location; | ||
135 | + let _params: ProcessItemProps = {}; // 接口调用参数 | ||
136 | + | ||
137 | + if (!!e) { | ||
138 | + e.stopPropagation(); | ||
139 | + } | ||
140 | + | ||
141 | + _params = { | ||
142 | + appId: processDetails?.appId, | ||
143 | + }; | ||
144 | + if (!!processDetails?.id) { | ||
145 | + _params.id = processDetails?.id; | ||
146 | + } | ||
147 | + if (!!processDetails?.funCode) { | ||
148 | + _params.funCode = processDetails?.funCode; | ||
149 | + } | ||
150 | + | ||
151 | + switch (type) { | ||
152 | + case 'RETURN': | ||
153 | + // 返回上一页 | ||
154 | + if (!!returnUrl) { | ||
155 | + handleWindowOpen( | ||
156 | + `${protocol}//${host}${pathname}${returnUrl}`, | ||
157 | + '_self', | ||
158 | + ); | ||
159 | + } else { | ||
160 | + window.history.back(); | ||
161 | + } | ||
162 | + break; | ||
163 | + case 'SAVE': | ||
164 | + // TODO 保存草稿 保存节点相关 参数 processJson | ||
165 | + console.log('SAVE === ', dataConversion(nodes)); | ||
166 | + _params.processJson = JSON.stringify({ | ||
167 | + dataJson: [...dataConversion(nodes)], | ||
168 | + }); | ||
169 | + saveProcessSetting(_params) | ||
170 | + .then((res: string) => { | ||
171 | + console.log('res ===', res); | ||
172 | + if (!!res) { | ||
173 | + // setProcessDetails({ | ||
174 | + // ...processDetails, | ||
175 | + // id: processDetails?.id || res, | ||
176 | + // name: data.name, | ||
177 | + // remark: data.remark, | ||
178 | + // }); | ||
179 | + // if (!processDetails?.id) { | ||
180 | + // history.replace({ | ||
181 | + // query: { | ||
182 | + // ...query, | ||
183 | + // processId: res, | ||
184 | + // } | ||
185 | + // }); | ||
186 | + // } | ||
187 | + } | ||
188 | + }) | ||
189 | + .catch((err: { msg?: string }) => { | ||
190 | + message.warning(err?.msg || '节点配置不完善', 5); | ||
191 | + }); | ||
192 | + break; | ||
193 | + case 'DEBUG': | ||
194 | + // 调试 | ||
195 | + console.log('DEBUG === '); | ||
196 | + break; | ||
197 | + case 'RELEASE': | ||
198 | + // 发布流程 | ||
199 | + // TODO 要先校验是否保存 才能发布 | ||
200 | + publishProcessById(processDetails.id || '') | ||
201 | + .then((res) => { | ||
202 | + console.log('res ====', res); | ||
203 | + }) | ||
204 | + .catch((err: { msg?: string }) => { | ||
205 | + message.warning(err?.msg || '节点配置不完善', 5); | ||
206 | + }); | ||
207 | + break; | ||
208 | + case 'EDIT': | ||
209 | + // 编辑数据流名称 | ||
210 | + const obj: ProcessItemProps = { | ||
211 | + id: processDetails.id || '', | ||
212 | + name: processDetails.name || '', | ||
213 | + remark: processDetails.remark || '', | ||
214 | + }; | ||
215 | + formModalRef.current.open(obj); | ||
216 | + break; | ||
217 | + case 'VERSION': | ||
218 | + // 版本管理跳转 | ||
219 | + handleWindowOpen( | ||
220 | + `${protocol}//${host}${pathname}#/design/data-flow-view/version?processId=${ | ||
221 | + data?.id || '' | ||
222 | + }&name=${data?.name || ''}&returnUrl=${hash}`, | ||
223 | + '_self', | ||
224 | + ); | ||
225 | + break; | ||
226 | + case 'SAVE_NAME': | ||
227 | + _params.name = data.name; | ||
228 | + _params.remark = data?.remark || ''; | ||
229 | + // 保存 名称、说明 | ||
230 | + saveProcessSetting(_params) | ||
231 | + .then((res: string) => { | ||
232 | + if (!!res) { | ||
233 | + setProcessDetails({ | ||
234 | + ...processDetails, | ||
235 | + id: processDetails?.id || res, | ||
236 | + name: data.name, | ||
237 | + remark: data.remark, | ||
238 | + }); | ||
239 | + if (!processDetails?.id) { | ||
240 | + history.replace({ | ||
241 | + query: { | ||
242 | + ...query, | ||
243 | + processId: res, | ||
244 | + }, | ||
245 | + }); | ||
246 | + } | ||
247 | + } | ||
248 | + }) | ||
249 | + .catch((err: { msg?: string }) => { | ||
250 | + message.warning(err?.msg || '当前保存操作失败', 5); | ||
251 | + }); | ||
252 | + break; | ||
253 | + case 'SIMPLE': | ||
254 | + // 精简模式 | ||
255 | + setShowSimple(!showSimple); | ||
256 | + break; | ||
257 | + default: | ||
258 | + break; | ||
259 | + } | ||
260 | + }; | ||
261 | + | ||
262 | + // 头部 左侧 自定义 | ||
263 | + const renderLeft = () => { | ||
264 | + return ( | ||
265 | + <section className={cls(styles[`${prefix}__header__left-custom`])}> | ||
266 | + <div | ||
267 | + className={cls(styles[`${prefix}__header__left`])} | ||
268 | + onClick={() => { | ||
269 | + handleClick('RETURN'); | ||
270 | + }} | ||
271 | + > | ||
272 | + <QxIcon | ||
273 | + className={cls(styles[`${prefix}__header__left-return`])} | ||
274 | + type={'icon-common-arrow'} | ||
275 | + /> | ||
276 | + <Text | ||
277 | + className={cls(styles[`${prefix}__header__left-title`])} | ||
278 | + style={{ maxWidth: nameMaxLength * 16 * 1.2 + 'px' }} | ||
279 | + ellipsis={{ tooltip: processDetails?.name || '' }} | ||
280 | + > | ||
281 | + {processDetails?.name || ''} | ||
282 | + </Text> | ||
283 | + </div> | ||
284 | + <div className={cls(styles[`${prefix}__header__left-custom__opt`])}> | ||
285 | + {!!processDetails?.remark ? ( | ||
286 | + <Tooltip placement="bottom" title={processDetails?.remark}> | ||
287 | + <Button type="link" icon={<QxIcon type={'icon-frame-help'} />} /> | ||
288 | + </Tooltip> | ||
289 | + ) : null} | ||
290 | + {type === 'preview' ? null : ( | ||
291 | + <Tooltip placement="bottom" title={'编辑名称与说明'}> | ||
292 | + <Button | ||
293 | + type="link" | ||
294 | + icon={<QxIcon type={'icon-frame-edit'} />} | ||
295 | + onClick={() => { | ||
296 | + handleClick('EDIT'); | ||
297 | + }} | ||
298 | + /> | ||
299 | + </Tooltip> | ||
300 | + )} | ||
301 | + </div> | ||
302 | + <Space size={8}> | ||
303 | + {type === 'preview' ? ( | ||
304 | + <FlowVersion versionText={processDetails?.curVersion || 1} /> | ||
305 | + ) : ( | ||
306 | + <FlowVersion | ||
307 | + versionText={processDetails?.curVersion || 1} | ||
308 | + handleClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => { | ||
309 | + handleClick( | ||
310 | + 'VERSION', | ||
311 | + { | ||
312 | + id: processDetails?.id, | ||
313 | + name: processDetails?.name, | ||
314 | + }, | ||
315 | + e, | ||
316 | + ); | ||
317 | + }} | ||
318 | + /> | ||
319 | + )} | ||
320 | + <FlowVersion | ||
321 | + status={processDetails?.latestConfigStatus || 'DRAFT'} | ||
322 | + statusEnums={FlowStatusEnums} | ||
323 | + /> | ||
324 | + </Space> | ||
325 | + </section> | ||
326 | + ); | ||
327 | + }; | ||
328 | + | ||
329 | + const renderRight = () => { | ||
330 | + return ( | ||
331 | + <Space size={12}> | ||
332 | + <Button | ||
333 | + onClick={() => { | ||
334 | + handleClick && handleClick('SIMPLE'); | ||
335 | + }} | ||
336 | + > | ||
337 | + {!showSimple ? '精简模式' : '退出精简'} | ||
338 | + </Button> | ||
339 | + <Button | ||
340 | + disabled={false} // 需要校验 是否禁用 | ||
341 | + onClick={() => { | ||
342 | + handleClick && handleClick('DEBUG'); | ||
343 | + }} | ||
344 | + > | ||
345 | + 调试 | ||
346 | + </Button> | ||
347 | + <Button | ||
348 | + disabled={false} // 需要校验 是否禁用 | ||
349 | + onClick={() => { | ||
350 | + handleClick && handleClick('SAVE'); | ||
351 | + }} | ||
352 | + > | ||
353 | + 保存 | ||
354 | + </Button> | ||
355 | + <Button | ||
356 | + disabled={false} // 需要校验 是否禁用 | ||
357 | + type="primary" | ||
358 | + // loading={false} | ||
359 | + onClick={() => { | ||
360 | + handleClick && handleClick('RELEASE'); | ||
361 | + }} | ||
362 | + > | ||
363 | + 发布流程 | ||
364 | + </Button> | ||
365 | + </Space> | ||
366 | + ); | ||
367 | + }; | ||
368 | + | ||
369 | + const handleChange = (nodes: INode[]) => { | ||
370 | + setNodes(nodes); | ||
371 | + }; | ||
372 | + | ||
373 | + const handleGetRemoteNodes = () => { | ||
374 | + setRemoteNodes([ | ||
375 | + { | ||
376 | + url: 'https://unpkg.com/react-flow-builder-pkg-demo@1.2.0/dist/index.umd.js', | ||
377 | + }, | ||
378 | + ]); | ||
379 | + }; | ||
380 | + | ||
381 | + useEffect(() => { | ||
382 | + // setNodes(dataConversion(defaultNodes as IQxNode[], true)); | ||
383 | + handleGetRemoteNodes(); | ||
384 | + }, []); | ||
385 | + | ||
386 | + useEffect(() => { | ||
387 | + console.log('nodes ==== ', nodes); | ||
388 | + }, [nodes]); | ||
389 | + useEffect(() => { | ||
390 | + let _data: ProcessItemProps = {}; | ||
391 | + _data.name = '未命名流程'; | ||
392 | + _data.appId = appId; | ||
393 | + if (!!funCode) { | ||
394 | + _data.funCode = funCode; | ||
395 | + } else { | ||
396 | + if (!!_data.funCode) delete _data.funCode; | ||
397 | + } | ||
398 | + if (!processId) { | ||
399 | + setProcessDetails(_data); | ||
400 | + } else { | ||
401 | + getDetailsData(processId, _data); | ||
402 | + } | ||
403 | + }, [appId, funCode, processId]); | ||
404 | + | ||
405 | + const handleOpenExtendModal = () => { | ||
406 | + setShowExtendModal(true); | ||
407 | + }; | ||
408 | + | ||
409 | + const handleCloseExtendModal = () => { | ||
410 | + setShowExtendModal(false); | ||
411 | + }; | ||
412 | + | ||
413 | + const getNodeTool = ( | ||
414 | + node: INode, | ||
415 | + nodes: INode[], | ||
416 | + defaultTools: INode[], | ||
417 | + actions: any, | ||
418 | + ) => { | ||
419 | + const { copyNodeAndChildren } = actions; | ||
420 | + switch (node.type) { | ||
421 | + case 'condition': | ||
422 | + return [ | ||
423 | + ...defaultTools, | ||
424 | + { | ||
425 | + name: '复制分支', | ||
426 | + icon: '复制分支', | ||
427 | + onClick: () => copyNodeAndChildren(), | ||
428 | + }, | ||
429 | + ]; | ||
430 | + default: | ||
431 | + return [ | ||
432 | + ...defaultTools, | ||
433 | + { | ||
434 | + name: '封装节点', | ||
435 | + icon: '封装节点', | ||
436 | + onClick: () => handleOpenExtendModal(), | ||
437 | + }, | ||
438 | + ]; | ||
439 | + } | ||
440 | + }; | ||
441 | + | ||
442 | + return ( | ||
443 | + <div className={cls(styles[`${prefix}__container`])}> | ||
444 | + <FlowHeader | ||
445 | + renderLeft={renderLeft()} | ||
446 | + handleClick={handleClick} | ||
447 | + renderRight={type === 'preview' ? null : renderRight()} | ||
448 | + /> | ||
449 | + <FlowBuilder | ||
450 | + readonly={type === 'preview'} | ||
451 | + nodes={nodes} | ||
452 | + registerNodes={systemNodes} | ||
453 | + historyTool | ||
454 | + zoomTool | ||
455 | + expandAllTool | ||
456 | + DrawerComponent={Drawer} | ||
457 | + PopoverComponent={Popover} | ||
458 | + PopconfirmComponent={Popconfirm} | ||
459 | + DropdownComponent={Dropdown} | ||
460 | + MenuComponent={Menu} | ||
461 | + // CommonDisplay={CommonDisplay} | ||
462 | + onChange={handleChange} | ||
463 | + registerRemoteNodes={remoteNodes} | ||
464 | + ConfigDrawerContainer={ConfigDrawer} | ||
465 | + NodePanel={NodesPanel} | ||
466 | + getNodeTool={getNodeTool} | ||
467 | + /> | ||
468 | + <NameDescriptionSetting cRef={formModalRef} handleClick={handleClick} /> | ||
469 | + <Modal | ||
470 | + title="扩展节点" | ||
471 | + visible={showExtendModal} | ||
472 | + onCancel={handleCloseExtendModal} | ||
473 | + /> | ||
474 | + </div> | ||
475 | + ); | ||
476 | +}; |
1 | +import React, { useContext, memo, useImperativeHandle } from 'react'; | ||
2 | +import { useForm } from '@qx/form-render'; | ||
3 | +import QxFormRender from '@/packages/qx-form-generator/src/form-render'; | ||
4 | +import { conditionSetting } from '../../setting-configs'; | ||
5 | +import styles from './index.less'; | ||
6 | +import type { IConfigComponent } from '@qx/flow'; | ||
7 | + | ||
8 | +const StartConfigComponent = React.forwardRef<any, StartConfigComponentProps>( | ||
9 | + (props, ref) => { | ||
10 | + const form = useForm(); | ||
11 | + | ||
12 | + const handleValuesChange = (values: any) => { | ||
13 | + props?.onChange(values); | ||
14 | + }; | ||
15 | + | ||
16 | + useImperativeHandle(ref, () => ({ | ||
17 | + validateFields: form.validateFields, | ||
18 | + })); | ||
19 | + | ||
20 | + return ( | ||
21 | + <div className={styles['qx-flow-single-records-config']}> | ||
22 | + <QxFormRender | ||
23 | + form={form} | ||
24 | + schema={conditionSetting} | ||
25 | + widgets={{}} | ||
26 | + watch={{}} | ||
27 | + onValuesChange={handleValuesChange} | ||
28 | + /> | ||
29 | + </div> | ||
30 | + ); | ||
31 | + }, | ||
32 | +); | ||
33 | + | ||
34 | +interface StartConfigComponentProps extends IConfigComponent { | ||
35 | + onChange: (values: any) => void; | ||
36 | +} | ||
37 | + | ||
38 | +export default memo(StartConfigComponent); |
1 | +import React, { memo, useContext } from 'react'; | ||
2 | +import { useAction } from '@qx/flow'; | ||
3 | +import type { IDisplayComponent } from '@qx/flow'; | ||
4 | + | ||
5 | +const ConditionDisplayComponent: React.FC<ConditionDisplayComponentProps> = ( | ||
6 | + props, | ||
7 | +) => { | ||
8 | + return <>条件节点内容</>; | ||
9 | +}; | ||
10 | + | ||
11 | +interface ConditionDisplayComponentProps extends IDisplayComponent {} | ||
12 | + | ||
13 | +export default memo(ConditionDisplayComponent); |
src/page/designer/nodes/condition/index.less
0 → 100644
src/page/designer/nodes/condition/index.tsx
0 → 100644
1 | +import React, { useContext, memo } from 'react'; | ||
2 | +import { useForm } from '@qx/form-render'; | ||
3 | +import { useDrawer } from '@qx/flow'; | ||
4 | +import { ConfigComponentFooter } from '../../components'; | ||
5 | +import QxFormRender from '@/packages/qx-form-generator/src/form-render'; | ||
6 | +import { startSetting } from '../../setting-configs'; | ||
7 | +import styles from './index.less'; | ||
8 | +import type { IConfigComponent } from '@qx/flow'; | ||
9 | + | ||
10 | +const EndConfigComponent: React.FC<EndConfigComponentProps> = (props) => { | ||
11 | + const form = useForm(); | ||
12 | + const { closeDrawer: cancel, saveDrawer: save } = useDrawer(); | ||
13 | + | ||
14 | + const onSave = async () => { | ||
15 | + try { | ||
16 | + await form.submit(); | ||
17 | + const values = form.getValues(); | ||
18 | + console.log('values', values); | ||
19 | + save(values); | ||
20 | + } catch (error) { | ||
21 | + console.log(error); | ||
22 | + } | ||
23 | + }; | ||
24 | + | ||
25 | + return ( | ||
26 | + <div className={styles['qx-flow-single-records-config']}> | ||
27 | + <QxFormRender form={form} schema={startSetting} widgets={{}} watch={{}} /> | ||
28 | + <ConfigComponentFooter onSave={onSave} onCancel={cancel} /> | ||
29 | + </div> | ||
30 | + ); | ||
31 | +}; | ||
32 | + | ||
33 | +interface EndConfigComponentProps extends IConfigComponent {} | ||
34 | + | ||
35 | +export default memo(EndConfigComponent); |
1 | +import React, { memo } from 'react'; | ||
2 | +// import { ReactComponent as Icon } from "../../public/icons/outlined.svg"; | ||
3 | +import type { IDisplayComponent, INode } from '@qx/flow'; | ||
4 | +import './index.less'; | ||
5 | + | ||
6 | +const DisplayComponent: React.FC<DisplayComponentProps> = (props) => { | ||
7 | + const { node, nodes } = props; | ||
8 | + | ||
9 | + return ( | ||
10 | + <div className="qx-flow-default-node"> | ||
11 | + <div className="qx-flow-default-node__header"> | ||
12 | + <span className="qx-flow-default-node__title"> | ||
13 | + <span className="qx-flow-default-node__title--icon"> | ||
14 | + {/* <Icon /> */} | ||
15 | + </span> | ||
16 | + <span className="qx-flow-default-node__title--name">{node.name}</span> | ||
17 | + </span> | ||
18 | + </div> | ||
19 | + <div className="qx-flow-default-node__content"> | ||
20 | + {!node.data ? ( | ||
21 | + <span className="qx-flow-start-node__button">设置入参</span> | ||
22 | + ) : ( | ||
23 | + <span>节点配置内容</span> | ||
24 | + )} | ||
25 | + </div> | ||
26 | + </div> | ||
27 | + ); | ||
28 | +}; | ||
29 | + | ||
30 | +interface DisplayComponentProps extends IDisplayComponent { | ||
31 | + onChange?: (nodes: INode[]) => void; | ||
32 | +} | ||
33 | + | ||
34 | +export default memo(DisplayComponent); |
src/page/designer/nodes/end/index.less
0 → 100644
src/page/designer/nodes/end/index.tsx
0 → 100644
src/page/designer/nodes/index.tsx
0 → 100644
1 | +import React, { useContext, memo } from 'react'; | ||
2 | +import { useForm } from '@qx/form-render'; | ||
3 | +import { useDrawer } from '@qx/flow'; | ||
4 | +import { ConfigComponentFooter } from '../../components'; | ||
5 | +import QxFormRender from '@/packages/qx-form-generator/src/form-render'; | ||
6 | +import { loopSetting } from '../../setting-configs'; | ||
7 | +import styles from './index.less'; | ||
8 | +import type { IConfigComponent } from '@qx/flow'; | ||
9 | + | ||
10 | +const LoopConfigComponent: React.FC<LoopConfigComponentProps> = (props) => { | ||
11 | + const form = useForm(); | ||
12 | + const { closeDrawer: cancel, saveDrawer: save } = useDrawer(); | ||
13 | + | ||
14 | + const onSave = async () => { | ||
15 | + try { | ||
16 | + await form.submit(); | ||
17 | + const values = form.getValues(); | ||
18 | + console.log('values', values); | ||
19 | + save(values); | ||
20 | + } catch (error) { | ||
21 | + console.log(error); | ||
22 | + } | ||
23 | + }; | ||
24 | + | ||
25 | + return ( | ||
26 | + <div className={styles['qx-flow-single-records-config']}> | ||
27 | + <QxFormRender form={form} schema={LoopSetting} widgets={{}} watch={{}} /> | ||
28 | + <ConfigComponentFooter onSave={onSave} onCancel={cancel} /> | ||
29 | + </div> | ||
30 | + ); | ||
31 | +}; | ||
32 | + | ||
33 | +interface LoopConfigComponentProps extends IConfigComponent {} | ||
34 | + | ||
35 | +export default memo(LoopConfigComponent); |
1 | +import React, { memo, useContext } from 'react'; | ||
2 | +import { useAction } from '@qx/flow'; | ||
3 | +import type { IDisplayComponent } from '@qx/flow'; | ||
4 | + | ||
5 | +const ConditionDisplayComponent: React.FC<ConditionDisplayComponentProps> = ( | ||
6 | + props, | ||
7 | +) => { | ||
8 | + return <>条件节点内容</>; | ||
9 | +}; | ||
10 | + | ||
11 | +interface ConditionDisplayComponentProps extends IDisplayComponent {} | ||
12 | + | ||
13 | +export default memo(ConditionDisplayComponent); |
src/page/designer/nodes/loop/index.less
0 → 100644
src/page/designer/nodes/loop/index.tsx
0 → 100644
1 | +import React, { | ||
2 | + useContext, | ||
3 | + memo, | ||
4 | + useState, | ||
5 | + useEffect, | ||
6 | + useImperativeHandle, | ||
7 | +} from 'react'; | ||
8 | +import { Checkbox, Form, Input, Radio, Space, Tooltip } from 'antd'; | ||
9 | +import type { CheckboxChangeEvent, RadioChangeEvent } from 'antd/es/checkbox'; | ||
10 | + | ||
11 | +import type { IConfigComponent } from '@qx/flow'; | ||
12 | +import { QxParameterSetting } from '@qx/common'; | ||
13 | +import { QxIcon } from '@/components'; | ||
14 | +import _ from 'lodash'; | ||
15 | + | ||
16 | +import { isolationList, propagationList } from '../../setting-configs/start'; | ||
17 | +interface PropagationSettingProps { | ||
18 | + value: string; | ||
19 | + onChange: (val: string) => void; | ||
20 | + optionsList: { | ||
21 | + value: string; | ||
22 | + label: string; | ||
23 | + tips: string; | ||
24 | + }[]; | ||
25 | +} | ||
26 | +const PropagationSetting: React.FC<PropagationSettingProps> = (props) => { | ||
27 | + const { value, onChange, optionsList } = props; | ||
28 | + | ||
29 | + return ( | ||
30 | + <Radio.Group | ||
31 | + onChange={(e: RadioChangeEvent) => { | ||
32 | + onChange(e.target.value); | ||
33 | + }} | ||
34 | + value={value} | ||
35 | + > | ||
36 | + <Space direction="vertical"> | ||
37 | + {(optionsList || []).map( | ||
38 | + (_options: { value: string; label: string; tips: string }) => { | ||
39 | + return ( | ||
40 | + <Radio value={_options.value}> | ||
41 | + <span>{_options.label}</span> | ||
42 | + <Tooltip placement="right" title={_options.tips}> | ||
43 | + <QxIcon | ||
44 | + className={'qx-data-flow-tooltip-opt'} | ||
45 | + type={'icon-frame-help'} | ||
46 | + /> | ||
47 | + </Tooltip> | ||
48 | + </Radio> | ||
49 | + ); | ||
50 | + }, | ||
51 | + )} | ||
52 | + </Space> | ||
53 | + </Radio.Group> | ||
54 | + ); | ||
55 | +}; | ||
56 | + | ||
57 | +const StartConfigComponent: React.FC<StartConfigComponentProps> = | ||
58 | + React.forwardRef((props, ref) => { | ||
59 | + const [form] = Form.useForm(); | ||
60 | + | ||
61 | + useImperativeHandle(ref, () => ({ | ||
62 | + validateFields: form.validateFields, | ||
63 | + })); | ||
64 | + | ||
65 | + const [values, setValues] = useState<{ | ||
66 | + params: any[]; | ||
67 | + enablePropagation: boolean; | ||
68 | + propagation: string; | ||
69 | + isolation: string; | ||
70 | + }>({ | ||
71 | + params: [], | ||
72 | + enablePropagation: false, | ||
73 | + propagation: 'REQUIRED', | ||
74 | + isolation: 'REPEATABLE_READ', | ||
75 | + }); | ||
76 | + | ||
77 | + useEffect(() => { | ||
78 | + if (_.isEmpty(props?.node?.data)) { | ||
79 | + return; | ||
80 | + } | ||
81 | + if (_.isEqual(props?.node?.data, values)) { | ||
82 | + return; | ||
83 | + } | ||
84 | + setValues(props?.node?.data); | ||
85 | + }, [props?.node?.data]); | ||
86 | + | ||
87 | + const onCheckedChange = (e: CheckboxChangeEvent) => { | ||
88 | + const _cloneVal = _.cloneDeep(values); | ||
89 | + setValues({ | ||
90 | + ..._cloneVal, | ||
91 | + enablePropagation: e.target.checked, | ||
92 | + }); | ||
93 | + }; | ||
94 | + | ||
95 | + useEffect(() => { | ||
96 | + if (_.isEqual(props?.node?.data, values)) { | ||
97 | + return; | ||
98 | + } | ||
99 | + props.onChange(values); | ||
100 | + }, [values]); | ||
101 | + | ||
102 | + return ( | ||
103 | + <Form | ||
104 | + form={form} | ||
105 | + layout="vertical" | ||
106 | + initialValues={{ | ||
107 | + enablePropagation: false, | ||
108 | + propagation: 'REQUIRED', | ||
109 | + isolation: 'REPEATABLE_READ', | ||
110 | + }} | ||
111 | + > | ||
112 | + <Form.Item name="params"> | ||
113 | + <h3 className={'qx-flow-form__label-title'}>入参设置</h3> | ||
114 | + <p className={'qx-flow-form__label-tips'}> | ||
115 | + 参数可以被后面的所有数据流节点使用 | ||
116 | + </p> | ||
117 | + <QxParameterSetting | ||
118 | + value={values.params} | ||
119 | + onChange={(_v: any) => { | ||
120 | + console.log('参数 === ', _v); | ||
121 | + }} | ||
122 | + /> | ||
123 | + </Form.Item> | ||
124 | + <Form.Item name="enablePropagation"> | ||
125 | + <h3 className={'qx-flow-form__label-title'}>事务控制</h3> | ||
126 | + <Checkbox | ||
127 | + checked={values.enablePropagation} | ||
128 | + onChange={onCheckedChange} | ||
129 | + > | ||
130 | + 设置事务的传播行为和隔离级别 | ||
131 | + </Checkbox> | ||
132 | + </Form.Item> | ||
133 | + <Form.Item | ||
134 | + hidden={!values.enablePropagation} | ||
135 | + label="事务传播行为" | ||
136 | + name="propagation" | ||
137 | + > | ||
138 | + <PropagationSetting | ||
139 | + optionsList={propagationList} | ||
140 | + value={'REQUIRED'} | ||
141 | + onChange={(_v: string) => { | ||
142 | + const _cloneVal = _.cloneDeep(values); | ||
143 | + setValues({ | ||
144 | + ..._cloneVal, | ||
145 | + propagation: _v, | ||
146 | + }); | ||
147 | + }} | ||
148 | + /> | ||
149 | + </Form.Item> | ||
150 | + <Form.Item | ||
151 | + hidden={!values.enablePropagation} | ||
152 | + label="事务隔离级别" | ||
153 | + name="isolation" | ||
154 | + > | ||
155 | + <PropagationSetting | ||
156 | + optionsList={isolationList} | ||
157 | + value={'REPEATABLE_READ'} | ||
158 | + onChange={(_v: string) => { | ||
159 | + const _cloneVal = _.cloneDeep(values); | ||
160 | + setValues({ | ||
161 | + ..._cloneVal, | ||
162 | + isolation: _v, | ||
163 | + }); | ||
164 | + }} | ||
165 | + /> | ||
166 | + </Form.Item> | ||
167 | + </Form> | ||
168 | + ); | ||
169 | + }); | ||
170 | + | ||
171 | +interface StartConfigComponentProps extends IConfigComponent { | ||
172 | + onChange: (values: any) => void; | ||
173 | +} | ||
174 | + | ||
175 | +export default memo(StartConfigComponent); |
1 | +import React, { memo } from 'react'; | ||
2 | +// import { ReactComponent as Icon } from '../../public/icons/outlined.svg'; | ||
3 | +import type { IDisplayComponent, INode } from '@qx/flow'; | ||
4 | +// import './index.less'; | ||
5 | + | ||
6 | +const DisplayComponent: React.FC<DisplayComponentProps> = (props) => { | ||
7 | + const { node, nodes } = props; | ||
8 | + | ||
9 | + const handleChangeName = (newValue: string) => { | ||
10 | + node.name = newValue; | ||
11 | + props.onChange?.([...nodes]); | ||
12 | + }; | ||
13 | + | ||
14 | + return ( | ||
15 | + <div className="qx-flow-default-node"> | ||
16 | + <div className="qx-flow-default-node__header"> | ||
17 | + <span className="qx-flow-default-node__title"> | ||
18 | + <span className="qx-flow-default-node__title--icon"> | ||
19 | + {/* <Icon /> */} | ||
20 | + </span> | ||
21 | + <span className="qx-flow-default-node__title--name">{node.name}</span> | ||
22 | + </span> | ||
23 | + </div> | ||
24 | + <div className="qx-flow-default-node__content"> | ||
25 | + {!node.data ? ( | ||
26 | + <span className="qx-flow-end-node__button">设置入参</span> | ||
27 | + ) : ( | ||
28 | + <span>节点配置内容</span> | ||
29 | + )} | ||
30 | + </div> | ||
31 | + </div> | ||
32 | + ); | ||
33 | +}; | ||
34 | + | ||
35 | +interface DisplayComponentProps extends IDisplayComponent { | ||
36 | + onChange?: (nodes: INode[]) => void; | ||
37 | +} | ||
38 | + | ||
39 | +export default memo(DisplayComponent); |
src/page/designer/nodes/start/index.tsx
0 → 100644
1 | import React, { memo } from 'react'; | 1 | import React, { memo } from 'react'; |
2 | -import { Outlet } from 'react-router-dom'; | 2 | +import { useTheme } from '@/hooks'; |
3 | +import '@/utils/actions'; | ||
4 | +import '@/styles/normalize.less'; | ||
3 | 5 | ||
4 | const Root: React.FC = (props) => { | 6 | const Root: React.FC = (props) => { |
5 | - return ( | ||
6 | - <div> | ||
7 | - <Outlet /> | ||
8 | - </div> | ||
9 | - ); | 7 | + const ThemeConfigProvider = useTheme(); |
8 | + return <>{ThemeConfigProvider}</>; | ||
10 | }; | 9 | }; |
11 | 10 | ||
12 | export default memo(Root); | 11 | export default memo(Root); |
1 | +import React, { | ||
2 | + useEffect, | ||
3 | + useImperativeHandle, | ||
4 | + useMemo, | ||
5 | + useState, | ||
6 | +} from 'react'; | ||
7 | +import { Button, Modal, Space, Table, Typography } from 'antd'; | ||
8 | +import { QxIcon } from '@/components'; | ||
9 | +import { isEmpty } from 'lodash-es'; | ||
10 | +import { handleWindowOpen } from '@qx/utils'; | ||
11 | +// import {} from '@/src/services/data-flow'; | ||
12 | +import { getIcon, useFlowTableScrollY } from '@/utils'; | ||
13 | +import { useDebounceEffect } from 'ahooks'; | ||
14 | + | ||
15 | +const { Text } = Typography; | ||
16 | + | ||
17 | +import '@/styles/modal-reset.less'; | ||
18 | +import cls from 'classnames'; | ||
19 | +import commonStyles from '@/page/view/index.module.less'; | ||
20 | +import './index.less'; | ||
21 | + | ||
22 | +const prefix = 'qx-flow-setting'; | ||
23 | + | ||
24 | +interface FlowQuoteSettingProps { | ||
25 | + cRef: any; | ||
26 | +} | ||
27 | + | ||
28 | +interface ParamsDataProps { | ||
29 | + id?: string; | ||
30 | + name?: string; | ||
31 | + pageSize?: number; | ||
32 | + pageNum?: number; | ||
33 | +} | ||
34 | + | ||
35 | +const FlowQuoteSetting: React.FC<FlowQuoteSettingProps> = (props) => { | ||
36 | + const { cRef } = props; | ||
37 | + const [visible, setVisible] = useState(false); | ||
38 | + const [paramsData, setParamsData] = useState<ParamsDataProps>({ | ||
39 | + id: '', | ||
40 | + name: '', | ||
41 | + pageSize: 10, | ||
42 | + pageNum: 1, | ||
43 | + }); | ||
44 | + const [pageData, setPageData] = useState<any[]>([]); | ||
45 | + // 表格是否添加scroll 属性 | ||
46 | + const [isScrollY, setIsScrollY] = useState<boolean>(false); | ||
47 | + const [tableLoading, setTableLoading] = useState<boolean>(true); // 表格loading | ||
48 | + | ||
49 | + useImperativeHandle(cRef, () => ({ | ||
50 | + // 暴露给父组件 | ||
51 | + open: (values: ParamsDataProps) => { | ||
52 | + if (isEmpty(values)) { | ||
53 | + return; | ||
54 | + } | ||
55 | + // 打开弹框 传参 | ||
56 | + setParamsData({ | ||
57 | + id: values?.id || '', | ||
58 | + name: values?.name || '', | ||
59 | + }); | ||
60 | + setVisible(true); | ||
61 | + }, | ||
62 | + })); | ||
63 | + | ||
64 | + // list 操作 | ||
65 | + const handleClick = ( | ||
66 | + type: string, | ||
67 | + data?: any, | ||
68 | + e?: React.MouseEvent<HTMLElement, MouseEvent>, | ||
69 | + ) => { | ||
70 | + const { host, pathname, protocol, hash } = location; | ||
71 | + if (!!e) { | ||
72 | + e.stopPropagation(); | ||
73 | + } | ||
74 | + console.log('type ==== ', type, data); | ||
75 | + | ||
76 | + switch (type) { | ||
77 | + case 'JUMP': | ||
78 | + // 功能名称 跳转 详情 | ||
79 | + // handleWindowOpen( | ||
80 | + // `${protocol}//${host}${pathname}#/design/data-flow?appId=${data?.appId || ''}&funCode=${ | ||
81 | + // data?.funCode || '' | ||
82 | + // }&funId=${data?.funId || ''}&type=edit&processId=${data.id}&returnUrl=${hash}`, | ||
83 | + // '_self', | ||
84 | + // ); | ||
85 | + break; | ||
86 | + default: | ||
87 | + break; | ||
88 | + } | ||
89 | + }; | ||
90 | + | ||
91 | + const cols = useMemo(() => { | ||
92 | + return [ | ||
93 | + { | ||
94 | + title: () => { | ||
95 | + return ( | ||
96 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
97 | + 应用名称 | ||
98 | + </span> | ||
99 | + ); | ||
100 | + }, | ||
101 | + dataIndex: 'name', | ||
102 | + width: 230, | ||
103 | + className: 'qx-flow-table-cell', | ||
104 | + render: (text: any, row: any) => { | ||
105 | + return ( | ||
106 | + <span | ||
107 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
108 | + style={{ width: '100%' }} | ||
109 | + > | ||
110 | + <Text | ||
111 | + style={{ | ||
112 | + marginRight: 4, | ||
113 | + maxWidth: '100%', | ||
114 | + }} | ||
115 | + ellipsis={{ tooltip: text || '' }} | ||
116 | + > | ||
117 | + {text} | ||
118 | + </Text> | ||
119 | + </span> | ||
120 | + ); | ||
121 | + }, | ||
122 | + }, | ||
123 | + { | ||
124 | + title: () => { | ||
125 | + return ( | ||
126 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
127 | + 表单名称 | ||
128 | + </span> | ||
129 | + ); | ||
130 | + }, | ||
131 | + dataIndex: 'curVersion', | ||
132 | + render: (text: any, row: any) => { | ||
133 | + return ( | ||
134 | + <span | ||
135 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
136 | + style={{ width: '100%' }} | ||
137 | + > | ||
138 | + <Text | ||
139 | + style={{ | ||
140 | + marginRight: 4, | ||
141 | + maxWidth: '100%', | ||
142 | + }} | ||
143 | + ellipsis={{ tooltip: text || '' }} | ||
144 | + > | ||
145 | + {text} | ||
146 | + </Text> | ||
147 | + </span> | ||
148 | + ); | ||
149 | + }, | ||
150 | + width: 160, | ||
151 | + className: 'qx-flow-table-cell', | ||
152 | + }, | ||
153 | + { | ||
154 | + title: () => { | ||
155 | + return ( | ||
156 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
157 | + 使用功能 | ||
158 | + </span> | ||
159 | + ); | ||
160 | + }, | ||
161 | + dataIndex: 'latestConfigStatus', | ||
162 | + className: 'qx-flow-table-cell', | ||
163 | + render: (text: any, row: any) => { | ||
164 | + return ( | ||
165 | + <span | ||
166 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
167 | + style={{ width: '100%' }} | ||
168 | + > | ||
169 | + <Text | ||
170 | + style={{ | ||
171 | + marginRight: 4, | ||
172 | + maxWidth: '100%', | ||
173 | + }} | ||
174 | + ellipsis={{ tooltip: text || '' }} | ||
175 | + > | ||
176 | + {text} | ||
177 | + </Text> | ||
178 | + </span> | ||
179 | + ); | ||
180 | + }, | ||
181 | + width: 120, | ||
182 | + }, | ||
183 | + { | ||
184 | + title: () => { | ||
185 | + return ( | ||
186 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
187 | + 功能名称 | ||
188 | + </span> | ||
189 | + ); | ||
190 | + }, | ||
191 | + dataIndex: 'updatedAt', | ||
192 | + width: 140, | ||
193 | + className: 'qx-flow-table-cell', | ||
194 | + render: (text: any, row: any) => { | ||
195 | + // console.log('流程使用情况 === 1', text); | ||
196 | + // console.log('流程使用情况 === 2', row); | ||
197 | + | ||
198 | + return ( | ||
199 | + <span | ||
200 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
201 | + style={{ width: '100%' }} | ||
202 | + > | ||
203 | + <Button | ||
204 | + type="link" | ||
205 | + size="small" | ||
206 | + icon={getIcon('icon-field-rel')} | ||
207 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => | ||
208 | + handleClick('FLOW_QUOTE', row, e) | ||
209 | + } | ||
210 | + > | ||
211 | + 等待提供字段 | ||
212 | + </Button> | ||
213 | + </span> | ||
214 | + ); | ||
215 | + }, | ||
216 | + }, | ||
217 | + ]; | ||
218 | + }, [JSON.stringify(paramsData), JSON.stringify(pageData)]); | ||
219 | + | ||
220 | + // 查询list | ||
221 | + const handleSearch = (params?: ParamsDataProps) => { | ||
222 | + setTableLoading(true); | ||
223 | + let _cloneParams: ParamsDataProps = {}; | ||
224 | + if (isEmpty(params)) { | ||
225 | + _cloneParams = { ...paramsData }; | ||
226 | + } else { | ||
227 | + _cloneParams = { ...paramsData, ...params }; | ||
228 | + } | ||
229 | + setParamsData(_cloneParams); | ||
230 | + // @ts-ignore | ||
231 | + // window.qx.actions.cancelToken(); // 多次请求 取最新请求 其他取消 | ||
232 | + /*getListProcess(_cloneParams) | ||
233 | + .then((res: any) => { | ||
234 | + // 处理数据 符合表格渲染格式 | ||
235 | + const _cloneList: any[] = []; | ||
236 | + const getColumnList = (data: any[]) => { | ||
237 | + data.forEach((item: any) => { | ||
238 | + _cloneList.push(_.cloneDeep(item)); | ||
239 | + if (!!item?.processes?.length) { | ||
240 | + getColumnList(item?.processes); | ||
241 | + } | ||
242 | + }); | ||
243 | + }; | ||
244 | + getColumnList(_.cloneDeep(res || [])); | ||
245 | + setPageData(_.cloneDeep(_cloneList)); | ||
246 | + // setExpandedKeys(_cloneList.map((item: any) => item.key)); | ||
247 | + setTableLoading(false); | ||
248 | + }) | ||
249 | + .catch(() => { | ||
250 | + console.warn('数据列表获取失败'); | ||
251 | + setPageData([]); | ||
252 | + setTableLoading(false); | ||
253 | + });*/ | ||
254 | + }; | ||
255 | + | ||
256 | + // 表格滚动高度获取 | ||
257 | + const [scrollY] = useFlowTableScrollY({ | ||
258 | + columns: cols, | ||
259 | + listProps: { | ||
260 | + paramsData, | ||
261 | + pageData, | ||
262 | + }, | ||
263 | + hasPagination: true, | ||
264 | + }) as any[]; | ||
265 | + | ||
266 | + useEffect(() => { | ||
267 | + if (!paramsData?.id) { | ||
268 | + return; | ||
269 | + } | ||
270 | + setPageData([]); | ||
271 | + setTableLoading(false); | ||
272 | + | ||
273 | + // handleSearch({id: paramsData?.id}); | ||
274 | + }, [paramsData]); | ||
275 | + | ||
276 | + useDebounceEffect( | ||
277 | + () => { | ||
278 | + if (scrollY >= 100 && !!pageData?.length) { | ||
279 | + const itemHeaderHeight = 41; | ||
280 | + setIsScrollY(pageData.length * itemHeaderHeight >= scrollY); | ||
281 | + } | ||
282 | + }, | ||
283 | + [pageData, scrollY], | ||
284 | + { wait: 200 }, | ||
285 | + ); | ||
286 | + | ||
287 | + return ( | ||
288 | + <div> | ||
289 | + <Modal | ||
290 | + width={720} | ||
291 | + centered | ||
292 | + title="流程使用情况" | ||
293 | + onCancel={() => setVisible(false)} | ||
294 | + visible={visible} | ||
295 | + footer={null} | ||
296 | + closeIcon={ | ||
297 | + <QxIcon | ||
298 | + className={'qx-data-flow__modal__close'} | ||
299 | + type={'icon-close'} | ||
300 | + /> | ||
301 | + } | ||
302 | + destroyOnClose | ||
303 | + wrapClassName={'qx-data-flow__modal qx-flow-quote-container'} | ||
304 | + > | ||
305 | + <section className={'qx-flow-quote__header'}> | ||
306 | + 【{paramsData?.name || ''}】正在使用中 | ||
307 | + </section> | ||
308 | + <div | ||
309 | + className={cls( | ||
310 | + commonStyles[`${prefix}-container`], | ||
311 | + 'qx-data-flow-table', | ||
312 | + )} | ||
313 | + > | ||
314 | + <Table | ||
315 | + loading={tableLoading} | ||
316 | + columns={cols} | ||
317 | + dataSource={pageData} | ||
318 | + scroll={ | ||
319 | + isScrollY | ||
320 | + ? { | ||
321 | + x: '100%', | ||
322 | + y: scrollY, | ||
323 | + } | ||
324 | + : { x: '100%' } | ||
325 | + } | ||
326 | + size={'small'} | ||
327 | + bordered | ||
328 | + rowKey={(record: any) => { | ||
329 | + return `${record?.type || ''}${record?.id || ''}${Math.random() | ||
330 | + .toString(36) | ||
331 | + .replaceAll(/[0-9]/g, '') | ||
332 | + .slice(2, 4)}`; | ||
333 | + }} | ||
334 | + /> | ||
335 | + </div> | ||
336 | + </Modal> | ||
337 | + </div> | ||
338 | + ); | ||
339 | +}; | ||
340 | + | ||
341 | +export default FlowQuoteSetting; |
1 | +.qx-flow-setting { | ||
2 | + &__top { | ||
3 | + height: 32px; | ||
4 | + margin-bottom: 16px; | ||
5 | + display: flex; | ||
6 | + align-items: center; | ||
7 | + justify-content: space-between; | ||
8 | + } | ||
9 | + | ||
10 | + &__top-tip { | ||
11 | + height: 32px; | ||
12 | + line-height: 32px; | ||
13 | + color: @N8; | ||
14 | + } | ||
15 | + | ||
16 | + &__top-tip__opt { | ||
17 | + display: inline-block; | ||
18 | + position: relative; | ||
19 | + padding-left: 20px; | ||
20 | + margin-left: 8px; | ||
21 | + color: @N8; | ||
22 | + height: 32px; | ||
23 | + line-height: 32px; | ||
24 | + cursor: pointer; | ||
25 | + | ||
26 | + :global { | ||
27 | + .qx-data-flow__icon { | ||
28 | + position: absolute; | ||
29 | + top: 50%; | ||
30 | + left: 0; | ||
31 | + margin-top: -8px; | ||
32 | + width: 16px; | ||
33 | + height: 16px; | ||
34 | + font-size: 16px; | ||
35 | + color: @N6; | ||
36 | + } | ||
37 | + } | ||
38 | + | ||
39 | + &:hover { | ||
40 | + color: @N9; | ||
41 | + | ||
42 | + :global { | ||
43 | + .qx-data-flow__icon { | ||
44 | + color: @N8; | ||
45 | + } | ||
46 | + } | ||
47 | + } | ||
48 | + } | ||
49 | + | ||
50 | + &__top-search { | ||
51 | + } | ||
52 | + | ||
53 | + &__top-search__input { | ||
54 | + :global { | ||
55 | + .qx-data-flow__icon { | ||
56 | + color: @N6; | ||
57 | + } | ||
58 | + } | ||
59 | + } | ||
60 | +} |
1 | +import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||
2 | +import { | ||
3 | + Button, | ||
4 | + Input, | ||
5 | + message, | ||
6 | + Modal, | ||
7 | + Space, | ||
8 | + Table, | ||
9 | + Tooltip, | ||
10 | + Typography, | ||
11 | + Avatar, | ||
12 | +} from 'antd'; | ||
13 | +import { useDebounceEffect, useDebounceFn } from 'ahooks'; | ||
14 | +// TODO: 去掉 view-render | ||
15 | +// import { QxIconAppImg } from '@qx/view-render'; | ||
16 | +import { isEmpty, cloneDeep } from 'lodash-es'; | ||
17 | + | ||
18 | +import { FlowSettingProps } from '@/page/view/constant'; | ||
19 | +import { getIcon, useFlowTableScrollY } from '@/utils'; | ||
20 | +import actions from '@/utils/actions'; | ||
21 | + | ||
22 | +import { copyProcessById, deleteProcessById, getListProcess } from '@/services'; | ||
23 | +import { handleWindowOpen } from '@qx/utils'; | ||
24 | +import { FlowVersion } from '@/components'; | ||
25 | +import FlowQuoteSetting from '../flow-quote-setting'; | ||
26 | +import cls from 'classnames'; | ||
27 | +import styles from './index.module.less'; | ||
28 | +import commonStyles from '../../index.module.less'; | ||
29 | +import { FlowStatusEnums } from '@/interface'; | ||
30 | +const prefix = 'qx-flow-setting'; | ||
31 | +const { Text } = Typography; | ||
32 | + | ||
33 | +interface ListParamsProps { | ||
34 | + appId?: string; // 所属应用 必需 | ||
35 | + funId?: string; // 表单 funId | ||
36 | + funCode?: string; // 表单 funCode | ||
37 | + name?: string; //流程名称 模糊查询 | ||
38 | +} | ||
39 | + | ||
40 | +// 表格 可收起 使用该方法 | ||
41 | +const getColumnListExpand = (data: any[]) => { | ||
42 | + const _cloneData: any[] = []; | ||
43 | + data.forEach((item: any) => { | ||
44 | + if (!!item?.processes?.length) { | ||
45 | + _cloneData.push({ | ||
46 | + key: item?.id || new Date().getTime().toString(), | ||
47 | + children: getColumnListExpand(item?.processes), | ||
48 | + ...item, | ||
49 | + }); | ||
50 | + } else { | ||
51 | + _cloneData.push({ | ||
52 | + key: item?.id || new Date().getTime().toString(), | ||
53 | + ...item, | ||
54 | + }); | ||
55 | + } | ||
56 | + }); | ||
57 | + return _cloneData; | ||
58 | +}; | ||
59 | + | ||
60 | +const FlowSetting: React.FC<FlowSettingProps> = (props) => { | ||
61 | + const { params, type } = props; | ||
62 | + const [queryParams, setQueryParams] = useState<ListParamsProps>({ | ||
63 | + appId: '', | ||
64 | + }); | ||
65 | + | ||
66 | + const [pageData, setPageData] = useState<any[]>([]); | ||
67 | + // 表格是否添加scroll 属性 | ||
68 | + const [isScrollY, setIsScrollY] = useState<boolean>(false); | ||
69 | + const [expandedKeys, setExpandedKeys] = useState<readonly React.Key[]>([]); // 表格 多级展开 | ||
70 | + const [tableLoading, setTableLoading] = useState<boolean>(true); // 表格loading | ||
71 | + const [keyword, setKeyword] = useState(''); // 搜索框字段 | ||
72 | + // 流程引用弹框 | ||
73 | + const quoteModalRef = useRef({ | ||
74 | + open: (data?: any) => {}, | ||
75 | + }); | ||
76 | + | ||
77 | + // list 操作 | ||
78 | + const handleClick = ( | ||
79 | + type: string, | ||
80 | + data?: any, | ||
81 | + e?: React.MouseEvent<HTMLElement, MouseEvent>, | ||
82 | + ) => { | ||
83 | + const { host, pathname, protocol, hash } = location; | ||
84 | + if (!!e) { | ||
85 | + e.stopPropagation(); | ||
86 | + } | ||
87 | + console.log('type ==== ', type, data); | ||
88 | + | ||
89 | + switch (type) { | ||
90 | + case 'EDIT': | ||
91 | + // 编辑数据流 跳转 | ||
92 | + handleWindowOpen( | ||
93 | + `${protocol}//${host}${pathname}#/design/data-flow?appId=${ | ||
94 | + params?.appId || '' | ||
95 | + }&funCode=${params?.funCode || ''}&funId=${ | ||
96 | + data?.funId || '' | ||
97 | + }&type=edit&processId=${data.id}&returnUrl=${hash}`, | ||
98 | + '_self', | ||
99 | + ); | ||
100 | + break; | ||
101 | + case 'FLOW_QUOTE': | ||
102 | + // 流程使用 弹框打开 | ||
103 | + quoteModalRef.current.open(data || {}); | ||
104 | + break; | ||
105 | + case 'VERSION': | ||
106 | + // 版本管理跳转 | ||
107 | + handleWindowOpen( | ||
108 | + `${protocol}//${host}${pathname}#/design/data-flow-view/version?processId=${ | ||
109 | + data?.id || '' | ||
110 | + }&name=${data?.name || ''}&returnUrl=${hash}`, | ||
111 | + '_self', | ||
112 | + ); | ||
113 | + break; | ||
114 | + case 'DELETE': | ||
115 | + // 删除 | ||
116 | + if (!!data?.id) { | ||
117 | + Modal.confirm({ | ||
118 | + title: '删除', | ||
119 | + content: `确定删除【${data?.name || ''}】?`, | ||
120 | + cancelText: '取消', | ||
121 | + okText: '确定', | ||
122 | + onOk: () => { | ||
123 | + deleteProcessById(data?.id) | ||
124 | + .then(() => { | ||
125 | + message.success('删除成功'); | ||
126 | + handleSearch(); | ||
127 | + }) | ||
128 | + .catch((err: any) => { | ||
129 | + console.log('err ====', err); | ||
130 | + message.success('删除失败'); | ||
131 | + }); | ||
132 | + }, | ||
133 | + }); | ||
134 | + } | ||
135 | + break; | ||
136 | + case 'COPY': | ||
137 | + // 复制 | ||
138 | + if (!!data?.id) { | ||
139 | + Modal.confirm({ | ||
140 | + title: '复制', | ||
141 | + content: `确定复制【${data?.name || ''}】的所有节点与配置?`, | ||
142 | + cancelText: '取消', | ||
143 | + okText: '确定', | ||
144 | + onOk: () => { | ||
145 | + copyProcessById(data?.id) | ||
146 | + .then(() => { | ||
147 | + message.success('复制成功'); | ||
148 | + handleSearch(); | ||
149 | + }) | ||
150 | + .catch((err: any) => { | ||
151 | + console.log('err ====', err); | ||
152 | + message.success('复制失败'); | ||
153 | + }); | ||
154 | + }, | ||
155 | + }); | ||
156 | + } | ||
157 | + break; | ||
158 | + default: | ||
159 | + break; | ||
160 | + } | ||
161 | + }; | ||
162 | + | ||
163 | + const cols = useMemo(() => { | ||
164 | + return [ | ||
165 | + { | ||
166 | + title: () => { | ||
167 | + return ( | ||
168 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
169 | + 流程名称 | ||
170 | + </span> | ||
171 | + ); | ||
172 | + }, | ||
173 | + dataIndex: 'name', | ||
174 | + width: 230, | ||
175 | + className: 'qx-flow-table-cell', | ||
176 | + render: (text: any, row: any) => { | ||
177 | + let _maxWidth: string = ''; | ||
178 | + let _classNameStr: string = ''; | ||
179 | + if (['APP', 'FUN'].includes(row?.type || '')) { | ||
180 | + _maxWidth = 'calc(100% - 24px)'; | ||
181 | + _classNameStr = ' '; | ||
182 | + } else { | ||
183 | + _classNameStr = `${prefix}__colspan-flow`; | ||
184 | + if (!!row?.remark) { | ||
185 | + _maxWidth = 'calc(100% - 20px)'; | ||
186 | + } else { | ||
187 | + _maxWidth = '100%'; | ||
188 | + } | ||
189 | + } | ||
190 | + | ||
191 | + return ( | ||
192 | + <span | ||
193 | + className={cls( | ||
194 | + commonStyles[`${prefix}__colspan`], | ||
195 | + commonStyles[_classNameStr], | ||
196 | + )} | ||
197 | + style={{ width: '100%' }} | ||
198 | + > | ||
199 | + {['APP', 'FUN'].includes(row?.type || '') ? ( | ||
200 | + <span className={cls(commonStyles[`${prefix}__colspan-icon`])}> | ||
201 | + {/* <QxIconAppImg | ||
202 | + item={{ | ||
203 | + icon: row?.icon || '', | ||
204 | + iconColor: row?.iconColor || '', | ||
205 | + }} | ||
206 | + /> */} | ||
207 | + </span> | ||
208 | + ) : null} | ||
209 | + <Text | ||
210 | + style={{ | ||
211 | + marginRight: 4, | ||
212 | + maxWidth: _maxWidth, | ||
213 | + }} | ||
214 | + ellipsis={{ tooltip: text || '' }} | ||
215 | + > | ||
216 | + {text} | ||
217 | + </Text> | ||
218 | + {row?.remark ? ( | ||
219 | + <Tooltip placement="top" title={row?.remark}> | ||
220 | + <span>{getIcon('icon-frame-help')}</span> | ||
221 | + </Tooltip> | ||
222 | + ) : null} | ||
223 | + </span> | ||
224 | + ); | ||
225 | + }, | ||
226 | + onCell: (record: any) => { | ||
227 | + return { | ||
228 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 7 : 1, | ||
229 | + }; | ||
230 | + }, | ||
231 | + }, | ||
232 | + { | ||
233 | + title: () => { | ||
234 | + return ( | ||
235 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
236 | + 当前版本 | ||
237 | + </span> | ||
238 | + ); | ||
239 | + }, | ||
240 | + dataIndex: 'curVersion', | ||
241 | + render: (text: any, row: any) => { | ||
242 | + return ( | ||
243 | + <span | ||
244 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
245 | + style={{ width: '100%' }} | ||
246 | + > | ||
247 | + <FlowVersion | ||
248 | + versionText={text} | ||
249 | + handleClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => { | ||
250 | + handleClick('VERSION', row, e); | ||
251 | + }} | ||
252 | + /> | ||
253 | + </span> | ||
254 | + ); | ||
255 | + }, | ||
256 | + width: 160, | ||
257 | + className: 'qx-flow-table-cell', | ||
258 | + onCell: (record: any) => { | ||
259 | + return { | ||
260 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 0 : 1, | ||
261 | + }; | ||
262 | + }, | ||
263 | + }, | ||
264 | + { | ||
265 | + title: () => { | ||
266 | + return ( | ||
267 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
268 | + 状态 | ||
269 | + </span> | ||
270 | + ); | ||
271 | + }, | ||
272 | + dataIndex: 'latestConfigStatus', | ||
273 | + className: 'qx-flow-table-cell', | ||
274 | + render: (text: any) => { | ||
275 | + return ( | ||
276 | + <span | ||
277 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
278 | + style={{ width: '100%' }} | ||
279 | + > | ||
280 | + <FlowVersion status={text} statusEnums={FlowStatusEnums} /> | ||
281 | + </span> | ||
282 | + ); | ||
283 | + }, | ||
284 | + width: 120, | ||
285 | + onCell: (record: any) => { | ||
286 | + return { | ||
287 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 0 : 1, | ||
288 | + }; | ||
289 | + }, | ||
290 | + }, | ||
291 | + { | ||
292 | + title: () => { | ||
293 | + return ( | ||
294 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
295 | + 流程使用情况 | ||
296 | + </span> | ||
297 | + ); | ||
298 | + }, | ||
299 | + dataIndex: 'updatedAt', | ||
300 | + width: 140, | ||
301 | + className: 'qx-flow-table-cell', | ||
302 | + render: (text: any, row: any) => { | ||
303 | + // console.log('流程使用情况 === 1', text); | ||
304 | + // console.log('流程使用情况 === 2', row); | ||
305 | + | ||
306 | + return ( | ||
307 | + <span | ||
308 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
309 | + style={{ width: '100%' }} | ||
310 | + > | ||
311 | + <Button | ||
312 | + type="link" | ||
313 | + size="small" | ||
314 | + icon={getIcon('icon-field-rel')} | ||
315 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => | ||
316 | + handleClick('FLOW_QUOTE', row, e) | ||
317 | + } | ||
318 | + > | ||
319 | + 等待提供字段 | ||
320 | + </Button> | ||
321 | + </span> | ||
322 | + ); | ||
323 | + }, | ||
324 | + onCell: (record: any) => { | ||
325 | + return { | ||
326 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 0 : 1, | ||
327 | + }; | ||
328 | + }, | ||
329 | + }, | ||
330 | + { | ||
331 | + title: () => { | ||
332 | + return ( | ||
333 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
334 | + 更新时间 | ||
335 | + </span> | ||
336 | + ); | ||
337 | + }, | ||
338 | + dataIndex: 'updatedAt', | ||
339 | + width: 200, | ||
340 | + className: 'qx-flow-table-cell', | ||
341 | + render: (text: any) => { | ||
342 | + return ( | ||
343 | + <span | ||
344 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
345 | + style={{ width: '100%' }} | ||
346 | + > | ||
347 | + {text} | ||
348 | + </span> | ||
349 | + ); | ||
350 | + }, | ||
351 | + onCell: (record: any) => { | ||
352 | + return { | ||
353 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 0 : 1, | ||
354 | + }; | ||
355 | + }, | ||
356 | + }, | ||
357 | + { | ||
358 | + title: () => { | ||
359 | + return ( | ||
360 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
361 | + 创建人 | ||
362 | + </span> | ||
363 | + ); | ||
364 | + }, | ||
365 | + dataIndex: 'createdByUser', | ||
366 | + className: 'qx-flow-table-cell qx-flow-table-cell-user', | ||
367 | + render: (text: any, row: any) => { | ||
368 | + if (isEmpty(text)) { | ||
369 | + return '--'; | ||
370 | + } | ||
371 | + let _imgError = false; | ||
372 | + return ( | ||
373 | + <span | ||
374 | + className={cls(commonStyles[`${prefix}__colspan-user`])} | ||
375 | + style={{ width: '100%' }} | ||
376 | + > | ||
377 | + <Avatar | ||
378 | + className={'qx-flow-table-cell__avatar'} | ||
379 | + size={24} | ||
380 | + alt="avatar" | ||
381 | + src={row?.createdByUser?.faceUrl || ''} | ||
382 | + onError={() => { | ||
383 | + _imgError = true; | ||
384 | + return true; | ||
385 | + }} | ||
386 | + > | ||
387 | + {(!!row?.createdByUser?.faceUrl && _imgError) || | ||
388 | + !row?.createdByUser?.faceUrl | ||
389 | + ? !!row?.createdByUser?.name?.length | ||
390 | + ? row?.createdByUser?.name[ | ||
391 | + row?.createdByUser?.name?.length - 1 | ||
392 | + ] | ||
393 | + : null | ||
394 | + : null} | ||
395 | + </Avatar> | ||
396 | + <Text | ||
397 | + className={'qx-flow-table-cell__avatar-name'} | ||
398 | + ellipsis={{ tooltip: row?.createdByUser?.name || '' }} | ||
399 | + > | ||
400 | + {row?.createdByUser?.name || ''} | ||
401 | + </Text> | ||
402 | + </span> | ||
403 | + ); | ||
404 | + }, | ||
405 | + width: 200, | ||
406 | + onCell: (record: any) => { | ||
407 | + return { | ||
408 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 0 : 1, | ||
409 | + }; | ||
410 | + }, | ||
411 | + }, | ||
412 | + { | ||
413 | + title: () => { | ||
414 | + return ( | ||
415 | + <span className={cls(commonStyles[`${prefix}__col-title`])}> | ||
416 | + 操作 | ||
417 | + </span> | ||
418 | + ); | ||
419 | + }, | ||
420 | + dataIndex: 'control', | ||
421 | + render: (text: any, row: any) => ( | ||
422 | + <Space | ||
423 | + size={8} | ||
424 | + className={cls(commonStyles[`${prefix}__colspan`])} | ||
425 | + style={{ width: '100%' }} | ||
426 | + > | ||
427 | + <Button | ||
428 | + type="link" | ||
429 | + size="small" | ||
430 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => { | ||
431 | + e.stopPropagation(); | ||
432 | + handleClick('EDIT', row); | ||
433 | + }} | ||
434 | + > | ||
435 | + 编辑 | ||
436 | + </Button> | ||
437 | + <Button | ||
438 | + type="link" | ||
439 | + size="small" | ||
440 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => | ||
441 | + handleClick('COPY', row, e) | ||
442 | + } | ||
443 | + > | ||
444 | + 复制 | ||
445 | + </Button> | ||
446 | + {/* TODO 字段待确定 有流程引用 不能删除*/} | ||
447 | + <Button | ||
448 | + type="link" | ||
449 | + size="small" | ||
450 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => | ||
451 | + handleClick('DELETE', row, e) | ||
452 | + } | ||
453 | + > | ||
454 | + 删除 | ||
455 | + </Button> | ||
456 | + </Space> | ||
457 | + ), | ||
458 | + width: 140, | ||
459 | + className: 'qx-flow-table-cell', | ||
460 | + onCell: (record: any) => { | ||
461 | + return { | ||
462 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 0 : 1, | ||
463 | + }; | ||
464 | + }, | ||
465 | + }, | ||
466 | + ]; | ||
467 | + }, [JSON.stringify(queryParams), JSON.stringify(pageData)]); | ||
468 | + | ||
469 | + // 查询list | ||
470 | + const handleSearch = (params?: ListParamsProps) => { | ||
471 | + setTableLoading(true); | ||
472 | + let _cloneParams: ListParamsProps = {}; | ||
473 | + if (isEmpty(params)) { | ||
474 | + _cloneParams = { ...queryParams }; | ||
475 | + } else { | ||
476 | + _cloneParams = { ...queryParams, ...params }; | ||
477 | + } | ||
478 | + setQueryParams(_cloneParams); | ||
479 | + | ||
480 | + actions.cancelToken(); // 多次请求 取最新请求 其他取消 | ||
481 | + | ||
482 | + getListProcess(_cloneParams) | ||
483 | + .then((res: any) => { | ||
484 | + // 处理数据 符合表格渲染格式 | ||
485 | + const _cloneList: any[] = []; | ||
486 | + const getColumnList = (data: any[]) => { | ||
487 | + data.forEach((item: any) => { | ||
488 | + _cloneList.push(cloneDeep(item)); | ||
489 | + if (!!item?.processes?.length) { | ||
490 | + getColumnList(item?.processes); | ||
491 | + } | ||
492 | + }); | ||
493 | + }; | ||
494 | + getColumnList(cloneDeep(res || [])); | ||
495 | + setPageData(cloneDeep(_cloneList)); | ||
496 | + // setExpandedKeys(_cloneList.map((item: any) => item.key)); | ||
497 | + setTableLoading(false); | ||
498 | + }) | ||
499 | + .catch(() => { | ||
500 | + console.warn('数据列表获取失败'); | ||
501 | + setPageData([]); | ||
502 | + setTableLoading(false); | ||
503 | + }); | ||
504 | + }; | ||
505 | + | ||
506 | + // 表格滚动高度获取 | ||
507 | + const [scrollY] = useFlowTableScrollY({ | ||
508 | + columns: cols, | ||
509 | + listProps: { | ||
510 | + queryParams, | ||
511 | + pageData, | ||
512 | + }, | ||
513 | + }) as any[]; | ||
514 | + | ||
515 | + // 搜索框查询 | ||
516 | + const { run: changeSearch } = useDebounceFn( | ||
517 | + (keyword) => { | ||
518 | + handleSearch({ name: keyword }); | ||
519 | + }, | ||
520 | + { | ||
521 | + wait: 300, | ||
522 | + }, | ||
523 | + ); | ||
524 | + | ||
525 | + useEffect(() => { | ||
526 | + if (!params?.appId) { | ||
527 | + return; | ||
528 | + } | ||
529 | + if (!!params?.appId) { | ||
530 | + if (!!params?.funCode) { | ||
531 | + handleSearch({ appId: params?.appId, funCode: params?.funCode }); | ||
532 | + } else { | ||
533 | + handleSearch({ appId: params?.appId }); | ||
534 | + } | ||
535 | + } | ||
536 | + }, [params]); | ||
537 | + | ||
538 | + useDebounceEffect( | ||
539 | + () => { | ||
540 | + if (scrollY >= 100 && !!pageData?.length) { | ||
541 | + const itemHeaderHeight = 41; | ||
542 | + setIsScrollY(pageData.length * itemHeaderHeight >= scrollY); | ||
543 | + } | ||
544 | + }, | ||
545 | + [pageData, scrollY], | ||
546 | + { wait: 200 }, | ||
547 | + ); | ||
548 | + | ||
549 | + return ( | ||
550 | + <> | ||
551 | + <header className={cls(styles[`${prefix}__top`])}> | ||
552 | + <section className={cls(styles[`${prefix}__top-tip`])}> | ||
553 | + <span> | ||
554 | + 数据流是一个强大的数据处理编排器,接受约定的参数传入,流程执行后输出结果参数。 | ||
555 | + </span> | ||
556 | + <span | ||
557 | + className={cls(styles[`${prefix}__top-tip__opt`])} | ||
558 | + onClick={() => { | ||
559 | + // 点击跳转 | ||
560 | + }} | ||
561 | + > | ||
562 | + {getIcon('icon-frame-help')}帮助 | ||
563 | + </span> | ||
564 | + </section> | ||
565 | + <section className={cls(styles[`${prefix}__top-search`])}> | ||
566 | + {type === 'form' ? ( | ||
567 | + <Button | ||
568 | + type="primary" | ||
569 | + // loading={false} | ||
570 | + onClick={() => { | ||
571 | + const { host, pathname, protocol, hash } = location; | ||
572 | + handleWindowOpen( | ||
573 | + `${protocol}//${host}${pathname}#/design/data-flow?appId=${ | ||
574 | + params?.appId || '' | ||
575 | + }&type=edit&funCode=${ | ||
576 | + params?.funCode || '' | ||
577 | + }&returnUrl=${hash}`, | ||
578 | + '_self', | ||
579 | + ); | ||
580 | + }} | ||
581 | + > | ||
582 | + 新建数据流 | ||
583 | + </Button> | ||
584 | + ) : ( | ||
585 | + <Input | ||
586 | + className={cls(styles[`${prefix}__top-search__input`])} | ||
587 | + placeholder={'流程名称搜索'} | ||
588 | + value={keyword} | ||
589 | + allowClear | ||
590 | + prefix={getIcon('icon-search')} | ||
591 | + onChange={(e) => { | ||
592 | + let _v = e.target.value; | ||
593 | + if (_v) { | ||
594 | + _v = _v.trim(); | ||
595 | + } | ||
596 | + setKeyword(_v); | ||
597 | + changeSearch(_v); | ||
598 | + }} | ||
599 | + /> | ||
600 | + )} | ||
601 | + </section> | ||
602 | + </header> | ||
603 | + | ||
604 | + <div | ||
605 | + className={cls( | ||
606 | + commonStyles[`${prefix}-container`], | ||
607 | + 'qx-data-flow-table', | ||
608 | + )} | ||
609 | + > | ||
610 | + <Table | ||
611 | + loading={tableLoading} | ||
612 | + columns={cols} | ||
613 | + dataSource={pageData} | ||
614 | + scroll={ | ||
615 | + isScrollY | ||
616 | + ? { | ||
617 | + x: '100%', | ||
618 | + y: scrollY, | ||
619 | + } | ||
620 | + : { x: '100%' } | ||
621 | + } | ||
622 | + pagination={false} | ||
623 | + size={'small'} | ||
624 | + bordered | ||
625 | + expandable={{ | ||
626 | + indentSize: 0, | ||
627 | + // defaultExpandAllRows: true, | ||
628 | + // expandedRowKeys: expandedKeys, | ||
629 | + // onExpandedRowsChange: (expandedRows: readonly React.Key[]) => { | ||
630 | + // setExpandedKeys(expandedRows); | ||
631 | + // }, | ||
632 | + }} | ||
633 | + rowKey={(record: any) => { | ||
634 | + return `${record?.type || ''}${record?.id || ''}${Math.random() | ||
635 | + .toString(36) | ||
636 | + .replaceAll(/[0-9]/g, '') | ||
637 | + .slice(2, 4)}`; | ||
638 | + }} | ||
639 | + /> | ||
640 | + </div> | ||
641 | + <FlowQuoteSetting cRef={quoteModalRef} /> | ||
642 | + </> | ||
643 | + ); | ||
644 | +}; | ||
645 | + | ||
646 | +export default FlowSetting; |
src/page/view/constant.tsx
0 → 100644
src/page/view/index.module.less
0 → 100644
1 | +.qx-data-flow { | ||
2 | + width: 100%; | ||
3 | + height: 100%; | ||
4 | + background-color: @body-bg; | ||
5 | +} | ||
6 | + | ||
7 | +.qx-data-flow__img.ant-image, | ||
8 | +.qx-data-flow__icon { | ||
9 | + display: flex; | ||
10 | + align-items: center; | ||
11 | + justify-content: center; | ||
12 | + min-width: 16px; | ||
13 | + height: 100%; | ||
14 | + font-size: 16px; | ||
15 | + line-height: 1; | ||
16 | + color: inherit; | ||
17 | +} | ||
18 | + | ||
19 | +.qx-data-flow__main { | ||
20 | + display: flex; | ||
21 | + padding: 16px; | ||
22 | + width: 100%; | ||
23 | + height: calc(100% - 57px); | ||
24 | + | ||
25 | + /*.main-aside { | ||
26 | + width: 240px; | ||
27 | + border-radius: 8px; | ||
28 | + background-color: #fff; | ||
29 | + } | ||
30 | + | ||
31 | + .main-aside-menus { | ||
32 | + padding-top: 12px; | ||
33 | + height: 100%; | ||
34 | + border-radius: 8px; | ||
35 | + | ||
36 | + // 菜单覆盖 | ||
37 | + :global { | ||
38 | + .ant-menu-item { | ||
39 | + width: calc(100% - 24px); | ||
40 | + margin: 0 6px 0 12px; | ||
41 | + padding-top: 0; | ||
42 | + padding-bottom: 0; | ||
43 | + border-radius: 4px; | ||
44 | + | ||
45 | + &:hover { | ||
46 | + color: @N8; | ||
47 | + background-color: @N3; | ||
48 | + } | ||
49 | + | ||
50 | + &:not(:last-child) { | ||
51 | + margin-bottom: 4px; | ||
52 | + } | ||
53 | + | ||
54 | + &.ant-menu-item-selected { | ||
55 | + color: @B8; | ||
56 | + background-color: @B3; | ||
57 | + | ||
58 | + a { | ||
59 | + color: @B8; | ||
60 | + } | ||
61 | + | ||
62 | + &:hover { | ||
63 | + color: @B8; | ||
64 | + background-color: @B3; | ||
65 | + } | ||
66 | + } | ||
67 | + } | ||
68 | + } | ||
69 | + }*/ | ||
70 | + | ||
71 | + .main-content { | ||
72 | + flex: 1; | ||
73 | + width: 100%; | ||
74 | + //width: calc(100% - 240px - 16px); | ||
75 | + height: 100%; | ||
76 | + padding: 16px; | ||
77 | + min-height: 0; | ||
78 | + //margin-left: 16px; | ||
79 | + background-color: #fff; | ||
80 | + border-radius: 8px; | ||
81 | + overflow: hidden; | ||
82 | + } | ||
83 | +} | ||
84 | + | ||
85 | +.qx-flow-setting { | ||
86 | + &-container { | ||
87 | + width: 100%; | ||
88 | + min-width: 100%; | ||
89 | + max-width: 100%; | ||
90 | + height: calc(100% - 48px); | ||
91 | + | ||
92 | + :global { | ||
93 | + // 创建人 单元格 样式迭代 | ||
94 | + td.ant-table-cell.qx-flow-table-cell { | ||
95 | + height: 40px; | ||
96 | + line-height: initial !important; | ||
97 | + | ||
98 | + &.qx-flow-table-cell-user { | ||
99 | + padding: 0 8px !important; | ||
100 | + height: 100% !important; | ||
101 | + } | ||
102 | + | ||
103 | + .anticon.qx-data-flow__icon { | ||
104 | + font-size: 16px; | ||
105 | + color: @N7; | ||
106 | + | ||
107 | + &:hover { | ||
108 | + color: @N9; | ||
109 | + } | ||
110 | + } | ||
111 | + | ||
112 | + .ant-btn.ant-btn-link.ant-btn-sm { | ||
113 | + flex: 1; | ||
114 | + display: inline-flex; | ||
115 | + align-items: center; | ||
116 | + justify-content: flex-start; | ||
117 | + height: 24px !important; | ||
118 | + padding: 0 !important; | ||
119 | + color: @B8; | ||
120 | + | ||
121 | + span { | ||
122 | + height: 100%; | ||
123 | + display: inline-flex; | ||
124 | + align-items: center; | ||
125 | + justify-content: flex-start; | ||
126 | + } | ||
127 | + | ||
128 | + &:hover { | ||
129 | + color: @B7; | ||
130 | + | ||
131 | + .anticon.qx-data-flow__icon { | ||
132 | + color: @B7; | ||
133 | + } | ||
134 | + } | ||
135 | + | ||
136 | + .anticon.qx-data-flow__icon { | ||
137 | + color: @B8; | ||
138 | + | ||
139 | + &:hover { | ||
140 | + color: @B7; | ||
141 | + } | ||
142 | + } | ||
143 | + } | ||
144 | + } | ||
145 | + | ||
146 | + // 覆盖单元格 hover样式变化 | ||
147 | + td.qx-flow-table-cell.ant-table-cell-row-hover { | ||
148 | + //cursor: pointer; | ||
149 | + | ||
150 | + .qx-flow-version__default, | ||
151 | + .qx-flow-version__default-operational { | ||
152 | + background-color: @N4 !important; | ||
153 | + border-color: @N4 !important; | ||
154 | + } | ||
155 | + | ||
156 | + .qx-flow-version__default-operational:hover { | ||
157 | + border-color: @B8 !important; | ||
158 | + } | ||
159 | + | ||
160 | + .qx-flow-table-cell__avatar-name.ant-typography { | ||
161 | + background-color: @N4 !important; | ||
162 | + } | ||
163 | + } | ||
164 | + } | ||
165 | + } | ||
166 | + | ||
167 | + &__col-title, | ||
168 | + &__colspan, | ||
169 | + &__colspan-user { | ||
170 | + height: 100%; | ||
171 | + padding: 0 8px; | ||
172 | + display: inline-flex; | ||
173 | + align-items: center; | ||
174 | + justify-content: flex-start; | ||
175 | + } | ||
176 | + | ||
177 | + &__colspan { | ||
178 | + :global { | ||
179 | + .ant-typography { | ||
180 | + color: @N8; | ||
181 | + } | ||
182 | + } | ||
183 | + } | ||
184 | + | ||
185 | + &__colspan-flow { | ||
186 | + :global { | ||
187 | + .ant-typography { | ||
188 | + color: @N9; | ||
189 | + } | ||
190 | + } | ||
191 | + } | ||
192 | + | ||
193 | + &__colspan-icon { | ||
194 | + width: 20px; | ||
195 | + height: 100%; | ||
196 | + display: inline-flex; | ||
197 | + align-items: center; | ||
198 | + justify-content: center; | ||
199 | + margin-right: 4px; | ||
200 | + | ||
201 | + :global { | ||
202 | + .qx-icon-aggregate-img, | ||
203 | + .anticon.qx-icon-aggregate { | ||
204 | + width: 20px; | ||
205 | + height: 20px; | ||
206 | + border-radius: 4px; | ||
207 | + font-size: 14px; | ||
208 | + overflow: hidden; | ||
209 | + line-height: 1; | ||
210 | + } | ||
211 | + } | ||
212 | + } | ||
213 | + | ||
214 | + &__colspan-user { | ||
215 | + display: inline-block; | ||
216 | + width: 100%; | ||
217 | + max-width: 100%; | ||
218 | + min-width: 100%; | ||
219 | + position: relative; | ||
220 | + | ||
221 | + :global { | ||
222 | + //覆盖头像样式 | ||
223 | + .qx-flow-table-cell__avatar.ant-avatar { | ||
224 | + position: absolute; | ||
225 | + font-size: 12px !important; | ||
226 | + top: 50%; | ||
227 | + margin-top: -12px; | ||
228 | + left: 8px; | ||
229 | + background-color: @B8; | ||
230 | + } | ||
231 | + | ||
232 | + .qx-flow-table-cell__avatar-name.ant-typography { | ||
233 | + padding-left: 28px; | ||
234 | + padding-right: 8px; | ||
235 | + max-width: 144px; | ||
236 | + height: 100%; | ||
237 | + line-height: 24px; | ||
238 | + font-size: 12px; | ||
239 | + color: @N9; | ||
240 | + background-color: @N3; | ||
241 | + border-radius: 100px; | ||
242 | + } | ||
243 | + } | ||
244 | + } | ||
245 | +} |
src/page/view/list/index.tsx
0 → 100644
1 | +import React from 'react'; | ||
2 | +// import { history } from '@@/core/history'; | ||
3 | +import { useSearchParams } from 'react-router-dom'; | ||
4 | +import { useTitle } from 'ahooks'; | ||
5 | +// @ts-ignore; | ||
6 | +import { handleWindowOpen } from '@qx/utils'; | ||
7 | +import { FlowHeader, QxIcon } from '@/components'; | ||
8 | +import FlowSetting from '../components/flow-setting'; | ||
9 | + | ||
10 | +// 样式 | ||
11 | +import cls from 'classnames'; | ||
12 | +import styles from '@/page/view/index.module.less'; | ||
13 | +const prefix = 'qx-data-flow'; | ||
14 | + | ||
15 | +const DataFlowView: React.FC = ({}) => { | ||
16 | + const [searchParams] = useSearchParams(); | ||
17 | + const { | ||
18 | + appId, // 应用id | ||
19 | + funCode, // 表单id | ||
20 | + returnUrl, // 来自页面跳转返回可跳转 | ||
21 | + } = { | ||
22 | + appId: searchParams.get('appId'), | ||
23 | + funCode: searchParams.get('funCode'), | ||
24 | + returnUrl: searchParams.get('returnUrl'), | ||
25 | + }; | ||
26 | + | ||
27 | + // 添加标题 | ||
28 | + if (!!appId && !funCode) { | ||
29 | + useTitle('数据流'); | ||
30 | + } | ||
31 | + | ||
32 | + const handleClick = (type: string) => { | ||
33 | + const { host, pathname, protocol, hash } = location; | ||
34 | + | ||
35 | + switch (type) { | ||
36 | + case 'RETURN': | ||
37 | + // 返回上一页 | ||
38 | + if (!!returnUrl) { | ||
39 | + const { host, pathname, protocol } = location; | ||
40 | + // 返回上一页 | ||
41 | + handleWindowOpen( | ||
42 | + `${protocol}//${host}${pathname}${returnUrl}`, | ||
43 | + '_self', | ||
44 | + ); | ||
45 | + } else { | ||
46 | + // history.goBack(); | ||
47 | + window.history.back(); | ||
48 | + } | ||
49 | + break; | ||
50 | + case 'ADD': | ||
51 | + // 新建数据流 | ||
52 | + handleWindowOpen( | ||
53 | + `${protocol}//${host}${pathname}#/designer?appId=${ | ||
54 | + appId || '' | ||
55 | + }&type=edit&returnUrl=${hash}`, | ||
56 | + '_self', | ||
57 | + ); | ||
58 | + break; | ||
59 | + default: | ||
60 | + break; | ||
61 | + } | ||
62 | + }; | ||
63 | + | ||
64 | + return ( | ||
65 | + <div className={cls(styles[prefix])}> | ||
66 | + <FlowHeader handleClick={handleClick} type={'FLOW'} /> | ||
67 | + <section className={cls(styles[`${prefix}__main`])}> | ||
68 | + <section className={cls(styles['main-content'])}> | ||
69 | + <FlowSetting | ||
70 | + params={{ | ||
71 | + appId: appId || '', | ||
72 | + funCode: funCode || '', | ||
73 | + }} | ||
74 | + /> | ||
75 | + </section> | ||
76 | + </section> | ||
77 | + </div> | ||
78 | + ); | ||
79 | +}; | ||
80 | + | ||
81 | +export default DataFlowView; |
src/page/view/util.tsx
0 → 100644
1 | +import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||
2 | + | ||
3 | +import { Button, Image, message, Modal, Tooltip, Upload } from 'antd'; | ||
4 | +import { QxIcon } from '@/components'; | ||
5 | + | ||
6 | +import defaultImg from '@/components/default/default_cover.png'; | ||
7 | + | ||
8 | +const defaultIcon: Record<string, string> = { | ||
9 | + form: 'icon-app-file-fill', | ||
10 | + group: 'icon-app-folder-2-fill', | ||
11 | + report: 'icon-app-performance-fill', | ||
12 | + url: 'icon-app-share-forward-fill', | ||
13 | + page: 'icon-app-tablicon-app-fill', | ||
14 | + dataset: 'icon-app-presentation-2-fill', | ||
15 | + datasetCube: 'icon-app-box-3-fill', | ||
16 | + app: 'icon-app-grid-2-fill', | ||
17 | +}; | ||
18 | + | ||
19 | +// 数据流 图标展示 | ||
20 | +export const getIcon = (icon: string, type?: string) => { | ||
21 | + if ((icon as string)?.startsWith('http')) { | ||
22 | + return ( | ||
23 | + <Image | ||
24 | + className={'qx-data-flow__img'} | ||
25 | + src={icon} | ||
26 | + fallback={defaultImg} | ||
27 | + preview={false} | ||
28 | + width={'16px'} | ||
29 | + height={'16px'} | ||
30 | + style={{ | ||
31 | + width: '16px', | ||
32 | + height: '16px', | ||
33 | + borderRadius: '4px', | ||
34 | + }} | ||
35 | + alt="" | ||
36 | + /> | ||
37 | + ); | ||
38 | + } else { | ||
39 | + return ( | ||
40 | + <QxIcon | ||
41 | + className={'qx-data-flow__icon'} | ||
42 | + type={icon || defaultIcon[type || 'form']} | ||
43 | + /> | ||
44 | + ); | ||
45 | + } | ||
46 | +}; | ||
47 | + | ||
48 | +// 表格 Scroll 高度计算 | ||
49 | +export const useFlowTableScrollY = ( | ||
50 | + deps: { | ||
51 | + columns: any[]; | ||
52 | + size?: 'small' | 'middle' | 'default'; | ||
53 | + tableBoxClassName?: string; | ||
54 | + classNames?: string[]; // 表格DOM内嵌元素 列名 添加在此, | ||
55 | + hasPagination?: boolean; | ||
56 | + listProps?: any; | ||
57 | + } = { | ||
58 | + size: 'small', | ||
59 | + tableBoxClassName: '', | ||
60 | + classNames: [], | ||
61 | + hasPagination: false, | ||
62 | + columns: [], | ||
63 | + listProps: {}, | ||
64 | + }, | ||
65 | +) => { | ||
66 | + const itemHeaderHeight = | ||
67 | + (deps?.size === 'middle' ? 48 : deps?.size === 'default' ? 56 : 40) + 1; | ||
68 | + const TABLE_VIEW_LIST = deps?.tableBoxClassName || 'qx-data-flow-table'; | ||
69 | + let tableHeader = itemHeaderHeight; | ||
70 | + const tableViewListDefaultHeight = 400; | ||
71 | + const defaultPagePadding = 0; | ||
72 | + | ||
73 | + const $ = (selectors: string) => | ||
74 | + document.querySelector<HTMLElement>(selectors); | ||
75 | + | ||
76 | + const defaultScrollY = useMemo( | ||
77 | + () => document.body.clientHeight, | ||
78 | + [tableHeader], | ||
79 | + ); | ||
80 | + | ||
81 | + const [scrollY, setScrollY] = useState<number>(defaultScrollY ?? 200); | ||
82 | + | ||
83 | + const timer = useRef<NodeJS.Timer>(); | ||
84 | + | ||
85 | + const getScrollY = () => { | ||
86 | + const tableViewList = $(`.${TABLE_VIEW_LIST}`); | ||
87 | + | ||
88 | + const tableViewListClientHeight = | ||
89 | + tableViewList?.clientHeight || tableViewListDefaultHeight; | ||
90 | + // 分页大小 目前已经固定 所以直接用56px | ||
91 | + const antPaginationHeight = deps?.hasPagination ? 56 : 0; | ||
92 | + | ||
93 | + const _domHeightByClassName = (deps?.classNames || []).map( | ||
94 | + (_v: string) => $(`.${_v}`)?.clientHeight ?? 0, | ||
95 | + ); | ||
96 | + let result = | ||
97 | + tableViewListClientHeight - | ||
98 | + tableHeader - | ||
99 | + defaultPagePadding - | ||
100 | + antPaginationHeight; | ||
101 | + if (!!_domHeightByClassName?.length) { | ||
102 | + result = result - eval(_domHeightByClassName.join('+')); | ||
103 | + } | ||
104 | + | ||
105 | + if (tableViewList) { | ||
106 | + tableViewList.style.overflowY = result < 200 ? 'auto' : 'hidden'; | ||
107 | + } | ||
108 | + | ||
109 | + setScrollY(result < 100 ? 100 : result); | ||
110 | + }; | ||
111 | + | ||
112 | + const observer = () => { | ||
113 | + window.addEventListener('resize', getScrollY); | ||
114 | + }; | ||
115 | + | ||
116 | + const removeObserver = () => { | ||
117 | + if (timer.current) window.clearTimeout(timer.current); | ||
118 | + window.removeEventListener('resize', getScrollY); | ||
119 | + }; | ||
120 | + | ||
121 | + useEffect(() => { | ||
122 | + observer(); | ||
123 | + return () => { | ||
124 | + removeObserver(); | ||
125 | + }; | ||
126 | + }, [tableHeader]); | ||
127 | + | ||
128 | + useEffect(() => { | ||
129 | + if (deps) { | ||
130 | + timer.current = setTimeout(() => { | ||
131 | + getScrollY(); | ||
132 | + }, 100); | ||
133 | + } | ||
134 | + }, [JSON.stringify(deps)]); | ||
135 | + | ||
136 | + return [scrollY, getScrollY]; | ||
137 | +}; |
src/page/view/version/index.tsx
0 → 100644
1 | +import React, { useEffect, useMemo, useState } from 'react'; | ||
2 | +import { | ||
3 | + Button, | ||
4 | + Input, | ||
5 | + message, | ||
6 | + Modal, | ||
7 | + Space, | ||
8 | + Table, | ||
9 | + Tooltip, | ||
10 | + Typography, | ||
11 | + Avatar, | ||
12 | +} from 'antd'; | ||
13 | + | ||
14 | +import { SettingOutlined } from '@ant-design/icons/lib'; | ||
15 | +import { useSearchParams } from 'react-router-dom'; | ||
16 | +import { useTitle } from 'ahooks'; | ||
17 | +import { QxIcon } from '@/components'; | ||
18 | +import { handleWindowOpen } from '@qx/utils'; | ||
19 | +import { FlowHeader, FlowVersion } from '@/components'; | ||
20 | + | ||
21 | +import { useFlowTableScrollY } from '@/utils'; | ||
22 | +import actions from '@/utils/actions'; | ||
23 | + | ||
24 | +import { isEmpty, cloneDeep } from 'lodash-es'; | ||
25 | + | ||
26 | +const { Text } = Typography; | ||
27 | + | ||
28 | +// 样式 | ||
29 | +import cls from 'classnames'; | ||
30 | +import styles from '../index.module.less'; | ||
31 | +import { getProcessVersions } from '@/services'; | ||
32 | +import { FlowVersionStatusEnums } from '@/interface'; | ||
33 | +const prefix = 'qx-flow-setting'; | ||
34 | + | ||
35 | +const DataFlowVersion: React.FC = ({}) => { | ||
36 | + // @ts-ignore | ||
37 | + const [searchParams] = useSearchParams(); | ||
38 | + const { | ||
39 | + processId, // 流程ID | ||
40 | + name, // 流程名称 | ||
41 | + returnUrl, // 来自页面跳转返回可跳转 | ||
42 | + } = { | ||
43 | + processId: searchParams.get('processId'), | ||
44 | + name: searchParams.get('name'), | ||
45 | + returnUrl: searchParams.get('returnUrl'), | ||
46 | + }; | ||
47 | + const [pageData, setPageData] = useState<any[]>([]); | ||
48 | + const [tableLoading, setTableLoading] = useState<boolean>(true); // 表格loading | ||
49 | + // 表格是否添加scroll 属性 | ||
50 | + const [isScrollY, setIsScrollY] = useState<boolean>(false); | ||
51 | + // 添加标题 | ||
52 | + useTitle(name || '未命名流程'); | ||
53 | + | ||
54 | + const handleClick = ( | ||
55 | + type: string, | ||
56 | + data?: any, | ||
57 | + e?: React.MouseEvent<HTMLElement, MouseEvent>, | ||
58 | + ) => { | ||
59 | + const { host, pathname, protocol, hash } = location; | ||
60 | + | ||
61 | + if (!!e) { | ||
62 | + e.stopPropagation(); | ||
63 | + } | ||
64 | + | ||
65 | + switch (type) { | ||
66 | + case 'RETURN': | ||
67 | + // 返回上一页 | ||
68 | + if (!!returnUrl) { | ||
69 | + // 返回上一页 | ||
70 | + handleWindowOpen( | ||
71 | + `${protocol}//${host}${pathname}${returnUrl}`, | ||
72 | + '_self', | ||
73 | + ); | ||
74 | + } else { | ||
75 | + // history.goBack(); | ||
76 | + window.history.back(); | ||
77 | + } | ||
78 | + break; | ||
79 | + case 'VIEW': | ||
80 | + // 查看 | ||
81 | + console.log('VIEW === ', data); | ||
82 | + break; | ||
83 | + case 'RESTORE': | ||
84 | + // 恢复 | ||
85 | + console.log('RESTORE === ', data); | ||
86 | + break; | ||
87 | + default: | ||
88 | + break; | ||
89 | + } | ||
90 | + }; | ||
91 | + | ||
92 | + const cols = useMemo(() => { | ||
93 | + return [ | ||
94 | + { | ||
95 | + title: () => { | ||
96 | + return ( | ||
97 | + <span className={cls(styles[`${prefix}__col-title`])}> | ||
98 | + 流程版本 | ||
99 | + </span> | ||
100 | + ); | ||
101 | + }, | ||
102 | + dataIndex: 'version', | ||
103 | + render: (text: any, row: any) => { | ||
104 | + return ( | ||
105 | + <span | ||
106 | + className={cls(styles[`${prefix}__colspan`])} | ||
107 | + style={{ width: '100%' }} | ||
108 | + > | ||
109 | + <FlowVersion versionText={text} /> | ||
110 | + </span> | ||
111 | + ); | ||
112 | + }, | ||
113 | + width: 170, | ||
114 | + className: 'qx-flow-table-cell', | ||
115 | + }, | ||
116 | + { | ||
117 | + title: () => { | ||
118 | + return ( | ||
119 | + <span className={cls(styles[`${prefix}__col-title`])}>状态</span> | ||
120 | + ); | ||
121 | + }, | ||
122 | + dataIndex: 'status', | ||
123 | + className: 'qx-flow-table-cell', | ||
124 | + render: (text: any) => { | ||
125 | + return ( | ||
126 | + <span | ||
127 | + className={cls(styles[`${prefix}__colspan`])} | ||
128 | + style={{ width: '100%' }} | ||
129 | + > | ||
130 | + <FlowVersion status={text} statusEnums={FlowVersionStatusEnums} /> | ||
131 | + </span> | ||
132 | + ); | ||
133 | + }, | ||
134 | + width: 170, | ||
135 | + }, | ||
136 | + { | ||
137 | + title: () => { | ||
138 | + return ( | ||
139 | + <span className={cls(styles[`${prefix}__col-title`])}> | ||
140 | + 更新时间 | ||
141 | + </span> | ||
142 | + ); | ||
143 | + }, | ||
144 | + dataIndex: 'updatedAt', | ||
145 | + width: 200, | ||
146 | + className: 'qx-flow-table-cell', | ||
147 | + render: (text: any) => { | ||
148 | + return ( | ||
149 | + <span | ||
150 | + className={cls(styles[`${prefix}__colspan`])} | ||
151 | + style={{ width: '100%' }} | ||
152 | + > | ||
153 | + {text} | ||
154 | + </span> | ||
155 | + ); | ||
156 | + }, | ||
157 | + onCell: (record: any) => { | ||
158 | + return { | ||
159 | + colSpan: ['APP', 'FUN'].includes(record?.type || '') ? 0 : 1, | ||
160 | + }; | ||
161 | + }, | ||
162 | + }, | ||
163 | + { | ||
164 | + title: () => { | ||
165 | + return ( | ||
166 | + <span className={cls(styles[`${prefix}__col-title`])}>更新人</span> | ||
167 | + ); | ||
168 | + }, | ||
169 | + dataIndex: 'updatedByUser', | ||
170 | + className: 'qx-flow-table-cell qx-flow-table-cell-user', | ||
171 | + render: (text: any, row: any) => { | ||
172 | + if (isEmpty(text)) { | ||
173 | + return '--'; | ||
174 | + } | ||
175 | + let _imgError = false; | ||
176 | + return ( | ||
177 | + <span | ||
178 | + className={cls(styles[`${prefix}__colspan-user`])} | ||
179 | + style={{ width: '100%' }} | ||
180 | + > | ||
181 | + <Avatar | ||
182 | + className={'qx-flow-table-cell__avatar'} | ||
183 | + size={24} | ||
184 | + alt="avatar" | ||
185 | + src={row?.updatedByUser?.faceUrl || ''} | ||
186 | + onError={() => { | ||
187 | + _imgError = true; | ||
188 | + return true; | ||
189 | + }} | ||
190 | + > | ||
191 | + {(!!row?.updatedByUser?.faceUrl && _imgError) || | ||
192 | + !row?.updatedByUser?.faceUrl | ||
193 | + ? !!row?.updatedByUser?.name?.length | ||
194 | + ? row?.updatedByUser?.name[ | ||
195 | + row?.updatedByUser?.name?.length - 1 | ||
196 | + ] | ||
197 | + : null | ||
198 | + : null} | ||
199 | + </Avatar> | ||
200 | + <Text | ||
201 | + className={'qx-flow-table-cell__avatar-name'} | ||
202 | + ellipsis={{ tooltip: row?.updatedByUser?.name || '' }} | ||
203 | + > | ||
204 | + {row?.updatedByUser?.name || ''} | ||
205 | + </Text> | ||
206 | + </span> | ||
207 | + ); | ||
208 | + }, | ||
209 | + width: 200, | ||
210 | + }, | ||
211 | + { | ||
212 | + title: () => { | ||
213 | + return ( | ||
214 | + <span className={cls(styles[`${prefix}__col-title`])}>操作</span> | ||
215 | + ); | ||
216 | + }, | ||
217 | + dataIndex: 'control', | ||
218 | + render: (text: any, row: any) => ( | ||
219 | + <Space | ||
220 | + size={8} | ||
221 | + className={cls(styles[`${prefix}__colspan`])} | ||
222 | + style={{ width: '100%' }} | ||
223 | + > | ||
224 | + <Button | ||
225 | + type="link" | ||
226 | + size="small" | ||
227 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => { | ||
228 | + e.stopPropagation(); | ||
229 | + handleClick('VIEW', row); | ||
230 | + }} | ||
231 | + > | ||
232 | + 查看 | ||
233 | + </Button> | ||
234 | + <Button | ||
235 | + type="link" | ||
236 | + size="small" | ||
237 | + onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => | ||
238 | + handleClick('RESTORE', row, e) | ||
239 | + } | ||
240 | + > | ||
241 | + 恢复 | ||
242 | + </Button> | ||
243 | + </Space> | ||
244 | + ), | ||
245 | + width: 140, | ||
246 | + className: 'qx-flow-table-cell', | ||
247 | + }, | ||
248 | + ]; | ||
249 | + }, [JSON.stringify(pageData)]); | ||
250 | + | ||
251 | + // 查询list | ||
252 | + const handleSearch = () => { | ||
253 | + setTableLoading(true); | ||
254 | + | ||
255 | + actions.cancelToken(); // 多次请求 取最新请求 其他取消 | ||
256 | + | ||
257 | + getProcessVersions('') | ||
258 | + .then((res: any) => { | ||
259 | + // 处理数据 符合表格渲染格式 | ||
260 | + const _cloneList: any[] = []; | ||
261 | + const getColumnList = (data: any[]) => { | ||
262 | + data.forEach((item: any) => { | ||
263 | + _cloneList.push(cloneDeep(item)); | ||
264 | + if (!!item?.processes?.length) { | ||
265 | + getColumnList(item?.processes); | ||
266 | + } | ||
267 | + }); | ||
268 | + }; | ||
269 | + getColumnList(cloneDeep(res || [])); | ||
270 | + setPageData(cloneDeep(_cloneList)); | ||
271 | + // setExpandedKeys(_cloneList.map((item: any) => item.key)); | ||
272 | + setTableLoading(false); | ||
273 | + }) | ||
274 | + .catch(() => { | ||
275 | + console.warn('数据列表获取失败'); | ||
276 | + setPageData([]); | ||
277 | + setTableLoading(false); | ||
278 | + }); | ||
279 | + }; | ||
280 | + | ||
281 | + // 表格滚动高度获取 | ||
282 | + const [scrollY] = useFlowTableScrollY({ | ||
283 | + columns: cols, | ||
284 | + listProps: { | ||
285 | + pageData, | ||
286 | + }, | ||
287 | + }) as any[]; | ||
288 | + | ||
289 | + return ( | ||
290 | + <div className={cls(styles['qx-data-flow'])}> | ||
291 | + <FlowHeader | ||
292 | + title={name || '未命名流程'} | ||
293 | + handleClick={handleClick} | ||
294 | + type={'VERSION'} | ||
295 | + /> | ||
296 | + <section className={cls(styles['qx-data-flow__main'])}> | ||
297 | + <section className={cls(styles['main-content'])}> | ||
298 | + <div | ||
299 | + className={cls(styles[`${prefix}-container`], 'qx-data-flow-table')} | ||
300 | + > | ||
301 | + <Table | ||
302 | + loading={tableLoading} | ||
303 | + columns={cols} | ||
304 | + dataSource={pageData} | ||
305 | + scroll={ | ||
306 | + isScrollY | ||
307 | + ? { | ||
308 | + x: '100%', | ||
309 | + y: scrollY, | ||
310 | + } | ||
311 | + : { x: '100%' } | ||
312 | + } | ||
313 | + pagination={false} | ||
314 | + size={'small'} | ||
315 | + bordered | ||
316 | + rowKey={(record: any) => { | ||
317 | + return `${record?.type || ''}${record?.id || ''}${Math.random() | ||
318 | + .toString(36) | ||
319 | + .replaceAll(/[0-9]/g, '') | ||
320 | + .slice(2, 4)}`; | ||
321 | + }} | ||
322 | + /> | ||
323 | + </div> | ||
324 | + </section> | ||
325 | + </section> | ||
326 | + </div> | ||
327 | + ); | ||
328 | +}; | ||
329 | + | ||
330 | +export default DataFlowVersion; |
src/request/index.ts
0 → 100644
1 | import { createHashRouter, RouterProvider } from 'react-router-dom'; | 1 | import { createHashRouter, RouterProvider } from 'react-router-dom'; |
2 | import Root from '../page/root'; | 2 | import Root from '../page/root'; |
3 | import About from '../page/about'; | 3 | import About from '../page/about'; |
4 | +import Designer from '../page/designer'; | ||
5 | +import List from '../page/view/list'; | ||
6 | +import Version from '../page/view/version'; | ||
4 | 7 | ||
5 | export default () => ( | 8 | export default () => ( |
6 | <RouterProvider | 9 | <RouterProvider |
@@ -13,6 +16,18 @@ export default () => ( | @@ -13,6 +16,18 @@ export default () => ( | ||
13 | path: '/about', | 16 | path: '/about', |
14 | element: <About />, | 17 | element: <About />, |
15 | }, | 18 | }, |
19 | + { | ||
20 | + path: '/designer', | ||
21 | + element: <Designer />, | ||
22 | + }, | ||
23 | + { | ||
24 | + path: '/list', | ||
25 | + element: <List />, | ||
26 | + }, | ||
27 | + { | ||
28 | + path: '/version', | ||
29 | + element: <Version />, | ||
30 | + }, | ||
16 | ], | 31 | ], |
17 | }, | 32 | }, |
18 | ])} | 33 | ])} |
src/sdk/index.ts
0 → 100644
1 | +import { lazy } from 'react'; | ||
2 | +import { QxSdk } from '@qx/utils'; | ||
3 | +import request from '@/request'; | ||
4 | +const qxSdk = new QxSdk(); | ||
5 | + | ||
6 | +qxSdk.registerSdk({ | ||
7 | + getFormFields(appCode: string, funCode: string, viewCode: string) { | ||
8 | + return request.get( | ||
9 | + `/qx-apaas-lowcode/app/${appCode}/form/${funCode}/field/option/${viewCode}?withRel=true`, | ||
10 | + ); | ||
11 | + }, | ||
12 | + | ||
13 | + getCommonComponent() { | ||
14 | + return { | ||
15 | + // QxAppSelector: lazy(async () => (await import(/* webpackChunkName: "qx-common" */ '@qx/common')).QxOrgSelector) | ||
16 | + }; | ||
17 | + }, | ||
18 | +}); | ||
19 | + | ||
20 | +export default qxSdk; |