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 | +}); | ... | ... |
... | ... | @@ -23,19 +23,36 @@ |
23 | 23 | "author": "", |
24 | 24 | "license": "ISC", |
25 | 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 | 34 | "hox": "^2.1.1", |
35 | + "lodash-es": "^4.17.21", | |
36 | + "normalize.css": "^8.0.1", | |
27 | 37 | "react": "^18.2.0", |
38 | + "react-cookies": "^0.1.1", | |
28 | 39 | "react-dom": "^18.2.0", |
29 | 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 | 46 | "devDependencies": { |
33 | 47 | "@babel/core": "^7.22.9", |
34 | 48 | "@babel/preset-env": "^7.22.9", |
35 | 49 | "@babel/preset-react": "^7.22.5", |
50 | + "@qx/types": "^0.0.2", | |
36 | 51 | "@svgr/webpack": "^8.1.0", |
52 | + "@types/lodash-es": "^4.17.8", | |
37 | 53 | "@types/react": "^18.2.21", |
38 | 54 | "@types/react-dom": "^18.2.7", |
55 | + "@types/uuid": "^9.0.2", | |
39 | 56 | "babel-loader": "^9.1.3", |
40 | 57 | "copy-webpack-plugin": "^11.0.0", |
41 | 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 | +import React, { memo } from 'react'; | |
2 | + | |
3 | +const DynamicComponent: React.FC<DynamicComponentProps> = (props) => { | |
4 | + return <div>DynamicComponent</div>; | |
5 | +}; | |
6 | + | |
7 | +interface DynamicComponentProps { | |
8 | + loader: (path: string) => any; | |
9 | +} | |
10 | + | |
11 | +export default memo(DynamicComponent); | ... | ... |
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 | +} | ... | ... |
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 | 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 | 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 | 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 | 1 | import { createHashRouter, RouterProvider } from 'react-router-dom'; |
2 | 2 | import Root from '../page/root'; |
3 | 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 | 8 | export default () => ( |
6 | 9 | <RouterProvider |
... | ... | @@ -13,6 +16,18 @@ export default () => ( |
13 | 16 | path: '/about', |
14 | 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; | ... | ... |