Commit 914531b047ccf888d2ec26014567b5d178b4756d
1 parent
82127a72
feat:增加qx-btn按钮,qx-progress进度条,搜索框,导出功能。
Showing
16 changed files
with
1155 additions
and
9 deletions
... | ... | @@ -10,6 +10,7 @@ |
10 | 10 | "license": "MIT", |
11 | 11 | "dependencies": { |
12 | 12 | "@ant-design/icons": "^5.2.5", |
13 | + "@qx/icon-btn": "^0.0.13", | |
13 | 14 | "@qx/ui": "0.0.3-beta.1", |
14 | 15 | "ahooks": "^3.7.5", |
15 | 16 | "classnames": "^2.3.2", |
... | ... | @@ -2883,10 +2884,9 @@ |
2883 | 2884 | } |
2884 | 2885 | }, |
2885 | 2886 | "node_modules/@qx/icon-btn": { |
2886 | - "version": "0.0.1", | |
2887 | - "resolved": "http://registry.npm.qgutech.com/@qx/icon-btn/download/@qx/icon-btn-0.0.1.tgz", | |
2888 | - "integrity": "sha1-pW/sqh+v5hyHoxsFkjmpMsumyMk=", | |
2889 | - "dev": true, | |
2887 | + "version": "0.0.13", | |
2888 | + "resolved": "http://registry.npm.qgutech.com/@qx/icon-btn/download/@qx/icon-btn-0.0.13.tgz", | |
2889 | + "integrity": "sha512-y3i0ngFuO22I4jv5ovgOR5nDI9X+64xuie22cKfLeNjI9IZhmNBn+BtDaG7wE9DPXMyxDH0sFBhlsya22hTaqA==", | |
2890 | 2890 | "license": "MIT" |
2891 | 2891 | }, |
2892 | 2892 | "node_modules/@qx/ui": { |
... | ... | @@ -2936,6 +2936,13 @@ |
2936 | 2936 | "react-cookies": ">=0.1.1" |
2937 | 2937 | } |
2938 | 2938 | }, |
2939 | + "node_modules/@qx/utils/node_modules/@qx/icon-btn": { | |
2940 | + "version": "0.0.1", | |
2941 | + "resolved": "http://registry.npm.qgutech.com/@qx/icon-btn/download/@qx/icon-btn-0.0.1.tgz", | |
2942 | + "integrity": "sha1-pW/sqh+v5hyHoxsFkjmpMsumyMk=", | |
2943 | + "dev": true, | |
2944 | + "license": "MIT" | |
2945 | + }, | |
2939 | 2946 | "node_modules/@rc-component/color-picker": { |
2940 | 2947 | "version": "1.4.1", |
2941 | 2948 | "resolved": "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-1.4.1.tgz", |
... | ... | @@ -22525,10 +22532,9 @@ |
22525 | 22532 | } |
22526 | 22533 | }, |
22527 | 22534 | "@qx/icon-btn": { |
22528 | - "version": "0.0.1", | |
22529 | - "resolved": "http://registry.npm.qgutech.com/@qx/icon-btn/download/@qx/icon-btn-0.0.1.tgz", | |
22530 | - "integrity": "sha1-pW/sqh+v5hyHoxsFkjmpMsumyMk=", | |
22531 | - "dev": true | |
22535 | + "version": "0.0.13", | |
22536 | + "resolved": "http://registry.npm.qgutech.com/@qx/icon-btn/download/@qx/icon-btn-0.0.13.tgz", | |
22537 | + "integrity": "sha512-y3i0ngFuO22I4jv5ovgOR5nDI9X+64xuie22cKfLeNjI9IZhmNBn+BtDaG7wE9DPXMyxDH0sFBhlsya22hTaqA==" | |
22532 | 22538 | }, |
22533 | 22539 | "@qx/ui": { |
22534 | 22540 | "version": "0.0.3-beta.1", |
... | ... | @@ -22559,6 +22565,14 @@ |
22559 | 22565 | "qiankun": "^2.2.1", |
22560 | 22566 | "umi-request": "^1.4.0", |
22561 | 22567 | "yet-another-abortcontroller-polyfill": "^0.0.4" |
22568 | + }, | |
22569 | + "dependencies": { | |
22570 | + "@qx/icon-btn": { | |
22571 | + "version": "0.0.1", | |
22572 | + "resolved": "http://registry.npm.qgutech.com/@qx/icon-btn/download/@qx/icon-btn-0.0.1.tgz", | |
22573 | + "integrity": "sha1-pW/sqh+v5hyHoxsFkjmpMsumyMk=", | |
22574 | + "dev": true | |
22575 | + } | |
22562 | 22576 | } |
22563 | 22577 | }, |
22564 | 22578 | "@rc-component/color-picker": { | ... | ... |
... | ... | @@ -10,5 +10,9 @@ export * from './qx-app-selector'; |
10 | 10 | export * from './utils'; |
11 | 11 | export * from './qx-field'; |
12 | 12 | export * from './qx-field-setter'; |
13 | +export * from './qx-btn'; | |
14 | +export * from './qx-progress'; | |
15 | +export * from './qx-search-input'; | |
16 | +export * from './qx-dynamic-component'; | |
17 | + | |
13 | 18 | |
14 | -// export * from './qx-btn'; | ... | ... |
src/qx-btn/btn-icon.js
0 → 100644
1 | +import { createFromIconfontCN } from '@ant-design/icons/lib'; | |
2 | + | |
3 | +/** | |
4 | + * 使用: | |
5 | + * import QxIcon from '@/packages/qx-icon'; | |
6 | + * <QxIcon type="xxx"/> | |
7 | + * 说明: | |
8 | + * `xxx`为图标唯一标识,`iconfont.cn`对应图标下的“复制代码”所得,建议命名为“icon-xxx_yyy”, | |
9 | + * “icon-”为固定前缀,xxx为模块类型,yyy为图标名。公共类的xxx命名为“comm” | |
10 | + * eg: <QxIcon type="icon-flow_eye"/> | |
11 | + * | |
12 | + * @type {React.FC<IconFontProps<string>>} | |
13 | + */ | |
14 | +const QxIcon = createFromIconfontCN({ | |
15 | + scriptUrl: [ | |
16 | + // require('./svg/icon-font-btn.js'),//功能按钮 专用图标 | |
17 | + // '//at.alicdn.com/t/font_2917361_i07tun4416.js' | |
18 | + ], | |
19 | +}); | |
20 | + | |
21 | +export default QxIcon; | ... | ... |
src/qx-btn/icon.js
0 → 100644
1 | +import { createFromIconfontCN } from '@ant-design/icons/lib'; | |
2 | + | |
3 | +/** | |
4 | + * 使用: | |
5 | + * import QxIcon from '@/packages/qx-icon'; | |
6 | + * <QxIcon type="xxx"/> | |
7 | + * 说明: | |
8 | + * `xxx`为图标唯一标识,`iconfont.cn`对应图标下的“复制代码”所得,建议命名为“icon-xxx_yyy”, | |
9 | + * “icon-”为固定前缀,xxx为模块类型,yyy为图标名。公共类的xxx命名为“comm” | |
10 | + * eg: <QxIcon type="icon-flow_eye"/> | |
11 | + * | |
12 | + * @type {React.FC<IconFontProps<string>>} | |
13 | + */ | |
14 | +const QxIcon = createFromIconfontCN({ | |
15 | + scriptUrl: [require('@qx/icon-btn')], | |
16 | +}); | |
17 | + | |
18 | +export default QxIcon; | ... | ... |
src/qx-btn/index.less
0 → 100644
1 | +.qx-btn(@btn-color) { | |
2 | + color: @btn-color; | |
3 | + background-color: #fff; | |
4 | + border-color: @btn-color; | |
5 | + | |
6 | + &:hover, | |
7 | + &:focus { | |
8 | + color: @btn-color; | |
9 | + background-color: #fff; | |
10 | + border-color: @btn-color; | |
11 | + } | |
12 | + | |
13 | + &:active { | |
14 | + color: @btn-color; | |
15 | + background-color: #fff; | |
16 | + border-color: @btn-color; | |
17 | + } | |
18 | + &.ant-btn-primary { | |
19 | + color: #fff; | |
20 | + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); | |
21 | + background: @btn-color; | |
22 | + border-color: @btn-color; | |
23 | + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045); | |
24 | + | |
25 | + &:hover, | |
26 | + &:focus { | |
27 | + color: #fff; | |
28 | + background: lighten(@btn-color, 10); | |
29 | + border-color: lighten(@btn-color, 10); | |
30 | + } | |
31 | + | |
32 | + &:active { | |
33 | + color: #fff; | |
34 | + background: darken(@btn-color, 7); | |
35 | + border-color: darken(@btn-color, 7); | |
36 | + } | |
37 | + } | |
38 | + | |
39 | + &.ant-tooltip-disabled-compatible-wrapper, | |
40 | + &.ant-btn-link, | |
41 | + &.ant-btn-text { | |
42 | + color: @btn-color; | |
43 | + background: transparent; | |
44 | + border-color: transparent; | |
45 | + box-shadow: none; | |
46 | + | |
47 | + &:hover, | |
48 | + &:focus { | |
49 | + color: darken(@btn-color, 5); | |
50 | + background: transparent; | |
51 | + border-color: transparent; | |
52 | + } | |
53 | + | |
54 | + &:active { | |
55 | + color: darken(@btn-color, 7); | |
56 | + background: transparent; | |
57 | + border-color: transparent; | |
58 | + } | |
59 | + } | |
60 | + | |
61 | + &.ant-btn-dashed { | |
62 | + color: @btn-color; | |
63 | + border-color: @btn-color; | |
64 | + } | |
65 | +} | |
66 | + | |
67 | +@qx_btn_blue: #1764ff; | |
68 | +@qx_btn_green: #52c41a; | |
69 | +@qx_btn_orange: #faad14; | |
70 | +@qx_btn_red: #f5222d; | |
71 | +@qx_btn_purple: #722ed1; | |
72 | +@qx_btn_cyan: #13c2c2; | |
73 | +.qx-btn-blue { | |
74 | + .qx-btn(@qx_btn_blue); | |
75 | +} | |
76 | + | |
77 | +.qx-btn-green { | |
78 | + .qx-btn(@qx_btn_green); | |
79 | +} | |
80 | + | |
81 | +.qx-btn-orange { | |
82 | + .qx-btn(@qx_btn_orange); | |
83 | +} | |
84 | + | |
85 | +.qx-btn-red { | |
86 | + .qx-btn(@qx_btn_red); | |
87 | +} | |
88 | + | |
89 | +.qx-btn-purple { | |
90 | + .qx-btn(@qx_btn_purple); | |
91 | +} | |
92 | + | |
93 | +.qx-btn-cyan { | |
94 | + .qx-btn(@qx_btn_cyan); | |
95 | +} | |
96 | + | |
97 | +.qx-core-drop_down { | |
98 | + .ant-dropdown-menu-item-icon { | |
99 | + min-width: 0; | |
100 | + } | |
101 | +} | |
102 | + | |
103 | +// 更多操作样式 迭代 | |
104 | +.ant-btn.ant-dropdown-trigger { | |
105 | + .qx-core-dropdown-arrow.anticon { | |
106 | + margin-right: 4px; | |
107 | + } | |
108 | + | |
109 | + &.ant-dropdown-open { | |
110 | + .qx-core-dropdown-arrow.anticon { | |
111 | + transform: rotate(180deg); | |
112 | + } | |
113 | + } | |
114 | +} | ... | ... |
src/qx-btn/index.tsx
0 → 100644
1 | +import React from 'react'; | |
2 | +import { Button, Tooltip, Dropdown, Menu } from 'antd'; | |
3 | +import './index.less'; | |
4 | +import { | |
5 | + CloseCircleOutlined, | |
6 | + CloseOutlined, | |
7 | + DeleteOutlined, | |
8 | + EditOutlined, | |
9 | + EllipsisOutlined, | |
10 | + MinusCircleOutlined, | |
11 | + MinusOutlined, | |
12 | + PlusCircleOutlined, | |
13 | + PlusOutlined, | |
14 | + ToolOutlined, | |
15 | + SettingOutlined, | |
16 | + CheckOutlined, | |
17 | + CopyOutlined, | |
18 | + ImportOutlined, | |
19 | + ExportOutlined, | |
20 | + FileTextOutlined, | |
21 | + PlayCircleOutlined, | |
22 | + PauseCircleOutlined, | |
23 | + AppstoreOutlined, | |
24 | + BarsOutlined, | |
25 | + AlertOutlined, | |
26 | + EyeOutlined, | |
27 | + // DownOutlined, | |
28 | + PrinterOutlined, | |
29 | +} from '@ant-design/icons'; | |
30 | +import QxIcon from './icon'; | |
31 | +import type { DropDownProps as DropdownProps, ButtonProps, TooltipProps } from 'antd/lib'; | |
32 | + | |
33 | +export const BUTTON_ICONS_MAP: { [index: string]: React.ReactElement } = { | |
34 | + PlusOutlined: <PlusOutlined />, | |
35 | + MinusOutlined: <MinusOutlined />, | |
36 | + PlusCircleOutlined: <PlusCircleOutlined />, | |
37 | + MinusCircleOutlined: <MinusCircleOutlined />, | |
38 | + CloseCircleOutlined: <CloseCircleOutlined />, | |
39 | + CloseOutlined: <CloseOutlined />, | |
40 | + CheckOutlined: <CheckOutlined />, | |
41 | + DeleteOutlined: <DeleteOutlined />, | |
42 | + EyeOutlined: <EyeOutlined />, | |
43 | + EditOutlined: <EditOutlined />, | |
44 | + FileTextOutlined: <FileTextOutlined />, | |
45 | + EllipsisOutlined: <EllipsisOutlined />, | |
46 | + SettingOutlined: <SettingOutlined />, | |
47 | + ToolOutlined: <ToolOutlined />, | |
48 | + CopyOutlined: <CopyOutlined />, | |
49 | + ImportOutlined: <ImportOutlined />, | |
50 | + ExportOutlined: <ExportOutlined />, | |
51 | + PlayCircleOutlined: <PlayCircleOutlined />, | |
52 | + PauseCircleOutlined: <PauseCircleOutlined />, | |
53 | + AppstoreOutlined: <AppstoreOutlined />, | |
54 | + BarsOutlined: <BarsOutlined />, | |
55 | + AlertOutlined: <AlertOutlined />, | |
56 | + PrinterOutlined: <PrinterOutlined />, | |
57 | +}; | |
58 | +export const BUTTON_COLORS = { | |
59 | + blue: '#1764FF', | |
60 | + green: '#52c41a', | |
61 | + orange: '#faad14', | |
62 | + red: '#f5222d', | |
63 | + purple: '#722ed1', | |
64 | + cyan: '#13c2c2', | |
65 | +}; | |
66 | + | |
67 | +//'#1890ff' primary | |
68 | +//'#52c41a' success | |
69 | +//'#faad14' Warning | |
70 | +// #f5222d error | |
71 | +/*@purple-base: #722ed1; | |
72 | +@cyan-base: #13c2c2;*/ | |
73 | + | |
74 | +export interface DropdownMenuItem { | |
75 | + code?: string; | |
76 | + name?: string; | |
77 | + icon?: string; | |
78 | +} | |
79 | + | |
80 | +export interface QxButtonProps extends ButtonProps { | |
81 | + name?: string; | |
82 | + color?: string; | |
83 | + batch?: boolean; //冗余属性 | |
84 | + flag?: string; //冗余属性 | |
85 | + action?: string; //冗余属性 | |
86 | + line?: boolean; //冗余属性 | |
87 | + code?: string; //冗余属性 | |
88 | + needConfirm?: boolean; //冗余属性 | |
89 | + tooltip?: string; | |
90 | + /** | |
91 | + * 下拉菜单选项 | |
92 | + */ | |
93 | + dropdownMenuItems?: DropdownMenuItem[]; | |
94 | + /** | |
95 | + * 下拉菜单点击事件 | |
96 | + */ | |
97 | + handleMenuClick?: (...args: any) => void; | |
98 | + /** | |
99 | + * 下拉菜单参数 | |
100 | + */ | |
101 | + dropdownProps?: DropdownProps; | |
102 | + /** | |
103 | + * 是否显示下拉菜单,默认 true | |
104 | + */ | |
105 | + showDropdown?: boolean; | |
106 | + // 提示框相关参数 | |
107 | + tooltipProps?: TooltipProps | undefined; | |
108 | + extraDom?: React.ReactNode | undefined; // 额外标签 | |
109 | +} | |
110 | + | |
111 | +const Icon = ({ name }) => { | |
112 | + if (name?.$$typeof) return name; | |
113 | + return name ? ( | |
114 | + String(name).indexOf('http') > -1 ? ( | |
115 | + <img | |
116 | + src={String(name)} | |
117 | + style={{ | |
118 | + width: '16px', | |
119 | + height: '16px', | |
120 | + marginRight: '4px', | |
121 | + verticalAlign: 'text-bottom', | |
122 | + borderRadius: '50%', | |
123 | + }} | |
124 | + alt="" | |
125 | + /> | |
126 | + ) : String(name).indexOf('-') > -1 ? ( | |
127 | + <QxIcon type={String(name)} /> | |
128 | + ) : ( | |
129 | + BUTTON_ICONS_MAP[name + ''] || <PlusOutlined /> | |
130 | + ) | |
131 | + ) : null; | |
132 | +}; | |
133 | + | |
134 | +export const QxButton: React.FC<QxButtonProps> = (props) => { | |
135 | + const { | |
136 | + name, | |
137 | + style, | |
138 | + icon, | |
139 | + batch, | |
140 | + line, | |
141 | + needConfirm, | |
142 | + className = '', | |
143 | + flag, | |
144 | + color, | |
145 | + tooltip = '', | |
146 | + dropdownMenuItems = [], | |
147 | + handleMenuClick, | |
148 | + dropdownProps = {}, | |
149 | + showDropdown = true, | |
150 | + tooltipProps, | |
151 | + extraDom, | |
152 | + ...rest | |
153 | + } = props; | |
154 | + const showMenu = Array.isArray(dropdownMenuItems) && dropdownMenuItems?.length && showDropdown; | |
155 | + | |
156 | + const getMenuItem = (_menuItems: any) => | |
157 | + _menuItems?.map((menu) => { | |
158 | + return { | |
159 | + ...menu, | |
160 | + label: menu?.label || menu?.name, | |
161 | + key: menu?.key || menu?.code, | |
162 | + children: getMenuItem(menu?.dropdownMenuItems), | |
163 | + }; | |
164 | + }); | |
165 | + | |
166 | + const generateMenu = (items: any) => { | |
167 | + return getMenuItem(items)?.map((item) => { | |
168 | + if (item.children?.length) { | |
169 | + return ( | |
170 | + <Menu.SubMenu | |
171 | + icon={ | |
172 | + <span> | |
173 | + <Icon name={item.icon} /> | |
174 | + </span> | |
175 | + } | |
176 | + title={item.label} | |
177 | + key={item.key} | |
178 | + disabled={!!item?.disabled} | |
179 | + > | |
180 | + {generateMenu(item.children)} | |
181 | + </Menu.SubMenu> | |
182 | + ); | |
183 | + } else { | |
184 | + return ( | |
185 | + <Menu.Item | |
186 | + icon={ | |
187 | + <span> | |
188 | + <Icon name={item.icon} /> | |
189 | + </span> | |
190 | + } | |
191 | + key={item.key} | |
192 | + onClick={() => handleMenuClick(item)} | |
193 | + disabled={!!item?.disabled} | |
194 | + > | |
195 | + {item.label} | |
196 | + </Menu.Item> | |
197 | + ); | |
198 | + } | |
199 | + }); | |
200 | + }; | |
201 | + | |
202 | + const menu = () => <Menu>{generateMenu(dropdownMenuItems)}</Menu>; | |
203 | + const btnChildren = name ? name : props.children; | |
204 | + | |
205 | + const button = !!extraDom ? ( | |
206 | + <Button | |
207 | + className={'qx-btn ' + className + ' ' + (color ? `qx-btn-${color}` : '')} | |
208 | + {...rest} | |
209 | + icon={<Icon name={icon} />} | |
210 | + > | |
211 | + {showMenu && btnChildren && props?.type !== 'link' ? ( | |
212 | + <> | |
213 | + <QxIcon type={'icon-common-arrow'} className={'qx-core-dropdown-arrow'} /> | |
214 | + {btnChildren} | |
215 | + </> | |
216 | + ) : ( | |
217 | + btnChildren | |
218 | + )} | |
219 | + {extraDom} | |
220 | + </Button> | |
221 | + ) : ( | |
222 | + <Button | |
223 | + className={'qx-btn ' + className + ' ' + (color ? `qx-btn-${color}` : '')} | |
224 | + {...rest} | |
225 | + icon={<Icon name={icon} />} | |
226 | + > | |
227 | + {showMenu && btnChildren && props?.type !== 'link' ? ( | |
228 | + <> | |
229 | + <QxIcon type={'icon-common-arrow'} className={'qx-core-dropdown-arrow'} /> | |
230 | + {btnChildren} | |
231 | + </> | |
232 | + ) : ( | |
233 | + btnChildren | |
234 | + )} | |
235 | + </Button> | |
236 | + ); | |
237 | + | |
238 | + const _dropdownProps: Omit<DropdownProps, 'overlay'> = { | |
239 | + overlayClassName: 'qx-core-drop_down', | |
240 | + ...dropdownProps, | |
241 | + }; | |
242 | + | |
243 | + const content = showMenu ? ( | |
244 | + <Dropdown overlay={menu} {..._dropdownProps}> | |
245 | + {button} | |
246 | + </Dropdown> | |
247 | + ) : ( | |
248 | + button | |
249 | + ); | |
250 | + | |
251 | + // extraDom 有额外标签时 tooltip不生效 | |
252 | + if (tooltip && !extraDom) { | |
253 | + return ( | |
254 | + <Tooltip {...(tooltipProps || {})} title={tooltip}> | |
255 | + {content} | |
256 | + </Tooltip> | |
257 | + ); | |
258 | + } | |
259 | + | |
260 | + return content; | |
261 | +}; | ... | ... |
src/qx-dynamic-component/fetchComponent.ts
0 → 100644
1 | +import type { ReactNode } from 'react'; | |
2 | + | |
3 | +const cache = {}; | |
4 | +/** | |
5 | + * | |
6 | + * @param url 组件地址 | |
7 | + * @param packages 依赖 | |
8 | + * @param name 组件名称 | |
9 | + * @param fileServer | |
10 | + * @returns | |
11 | + */ | |
12 | +export async function fetchComponent( | |
13 | + url: string, | |
14 | + name: string, | |
15 | + packages: Record<string, any>, | |
16 | + fileServer: boolean, | |
17 | + request?: any, | |
18 | +) { | |
19 | + if (cache[url]) { | |
20 | + const cacheModule = await cache[url]; | |
21 | + | |
22 | + if (name && !cacheModule[name]) { | |
23 | + throw new Error('component not found'); | |
24 | + } | |
25 | + | |
26 | + return { | |
27 | + default: cacheModule[name || 'default'], | |
28 | + }; | |
29 | + } | |
30 | + | |
31 | + let resolve; | |
32 | + let reject; | |
33 | + | |
34 | + cache[url] = new Promise((_resolve, _reject) => { | |
35 | + resolve = _resolve; | |
36 | + reject = _reject; | |
37 | + }); | |
38 | + | |
39 | + try { | |
40 | + let _url = url; | |
41 | + if (fileServer && !url.startsWith('http')) { | |
42 | + const data = await getFileServerPath(request); | |
43 | + if (data) { | |
44 | + _url = data?.endpoint + url; | |
45 | + } | |
46 | + } | |
47 | + | |
48 | + const text = await fetch(_url).then((a) => { | |
49 | + if (!a.ok) { | |
50 | + throw new Error('Network response was not ok'); | |
51 | + } | |
52 | + return a.text(); | |
53 | + }); | |
54 | + | |
55 | + const module = getParsedModule(text, packages); | |
56 | + | |
57 | + const result = { default: {} }; | |
58 | + | |
59 | + if (module.exports?.$$typeof || typeof module.exports === 'function') { | |
60 | + module.exports.default = module.exports as ReactNode; | |
61 | + } | |
62 | + | |
63 | + if (!module.exports?.default && !name) { | |
64 | + throw new Error('module default is undefined'); | |
65 | + } | |
66 | + | |
67 | + result.default = module.exports[name || 'default']; | |
68 | + | |
69 | + if (!result.default) { | |
70 | + throw new Error('component not found'); | |
71 | + } | |
72 | + | |
73 | + resolve(module.exports); | |
74 | + | |
75 | + return result; | |
76 | + } catch (error) { | |
77 | + reject(error); | |
78 | + throw new Error(error); | |
79 | + } | |
80 | +} | |
81 | + | |
82 | +function getParsedModule(code: string, packages: Record<string, any>) { | |
83 | + const module: { | |
84 | + exports: { | |
85 | + default?: React.ReactNode; | |
86 | + $$typeof?: any; | |
87 | + }; | |
88 | + } = { | |
89 | + exports: {}, | |
90 | + }; | |
91 | + | |
92 | + const require = (name: string) => { | |
93 | + return packages[name]; | |
94 | + }; | |
95 | + | |
96 | + try { | |
97 | + Function('require, exports, module, process', code)( | |
98 | + require, | |
99 | + module.exports, | |
100 | + module, | |
101 | + window.process || process, | |
102 | + ); | |
103 | + } catch (error) { | |
104 | + throw new Error(error.message); | |
105 | + } | |
106 | + | |
107 | + return module; | |
108 | +} | |
109 | + | |
110 | +function getFileServerPath(request?) { | |
111 | + return ((window as any)?.qx?.sdk?.request || request)?.get?.( | |
112 | + '/qgyun-service-fs-manager/fileSetting/get', | |
113 | + { | |
114 | + headers: { | |
115 | + businessCode: 'design', | |
116 | + }, | |
117 | + }, | |
118 | + ); | |
119 | +} | |
120 | + | |
121 | +export function isMobile() { | |
122 | + return 'ontouchstart' in document.documentElement; | |
123 | +} | ... | ... |
src/qx-dynamic-component/index.md
0 → 100644
1 | +--- | |
2 | +nav: | |
3 | + path: /component | |
4 | + title: 组件 | |
5 | + order: 1 | |
6 | +group: | |
7 | + path: /view | |
8 | + title: 业务组件 | |
9 | + order: 0 | |
10 | +--- | |
11 | + | |
12 | +## QxDynamicComponent | |
13 | + | |
14 | +### 默认导出 | |
15 | + | |
16 | +```tsx | |
17 | +import React from 'react'; | |
18 | +import { QxDynamicComponent } from '@qx/common'; | |
19 | + | |
20 | +export default () => ( | |
21 | + <QxDynamicComponent url="http://10.9.1.180/qx-apaaspublic/widgets/default/@qx-cw/demo1/index.umd.min.js" /> | |
22 | +); | |
23 | +``` | |
24 | + | |
25 | +### 导出多个组件 | |
26 | + | |
27 | +```tsx | |
28 | +import React from 'react'; | |
29 | +import { QxDynamicComponent } from '@qx/common'; | |
30 | + | |
31 | +export default () => ( | |
32 | + <> | |
33 | + <QxDynamicComponent | |
34 | + url="http://192.168.1.182/qx-apaaspublic/widgets/default/@qx-cw/demo2/index.umd.min.js" | |
35 | + component="QxDemo21" | |
36 | + /> | |
37 | + <QxDynamicComponent | |
38 | + url="http://192.168.1.182/qx-apaaspublic/widgets/default/@qx-cw/demo2/index.umd.min.js" | |
39 | + component="QxDemo22" | |
40 | + /> | |
41 | + </> | |
42 | +); | |
43 | +``` | |
44 | + | |
45 | +<API></API> | ... | ... |
src/qx-dynamic-component/index.tsx
0 → 100644
1 | +import React, { useMemo } from 'react'; | |
2 | +import ReactDOM from 'react-dom'; | |
3 | +import { Skeleton } from 'antd'; | |
4 | +import { fetchComponent, isMobile } from './fetchComponent'; | |
5 | + | |
6 | +const packages = { | |
7 | + react: React, | |
8 | + 'react-dom': ReactDOM, | |
9 | +}; | |
10 | + | |
11 | +export const QxDynamicComponent: React.FC<QxDynamicComponentProps> = (props) => { | |
12 | + const { config = {} } = props; | |
13 | + | |
14 | + const { | |
15 | + profile: _profile = {}, | |
16 | + skeleton = true, | |
17 | + component, | |
18 | + fileServer = false, | |
19 | + request, | |
20 | + url, | |
21 | + ...rest | |
22 | + } = getComponentConfig(); | |
23 | + | |
24 | + const profile = useMemo(() => ({ ...rest, ..._profile }), [_profile, rest]); | |
25 | + | |
26 | + const Comp = useMemo<React.FC<typeof profile>>(() => { | |
27 | + return React.lazy(async () => { | |
28 | + try { | |
29 | + return await fetchComponent(url, component, packages, fileServer, request); | |
30 | + } catch (error) { | |
31 | + console.warn(error); | |
32 | + return { | |
33 | + default: () => <span style={{ color: 'red' }}>加载错误: {error.message}</span>, | |
34 | + }; | |
35 | + } | |
36 | + }); | |
37 | + }, [url]); | |
38 | + | |
39 | + function getComponentConfig(key?: string) { | |
40 | + const mergedConfig: QxDynamicComponentProps = Object.assign( | |
41 | + {}, | |
42 | + props, | |
43 | + config[isMobile() ? 'h5' : 'pc'] || {}, | |
44 | + { | |
45 | + url: isMobile() ? props.h5Url : props.url, | |
46 | + }, | |
47 | + ); | |
48 | + return key ? mergedConfig[key] : mergedConfig; | |
49 | + } | |
50 | + | |
51 | + return ( | |
52 | + <React.Suspense | |
53 | + fallback={ | |
54 | + skeleton ? typeof skeleton === 'boolean' ? <Skeleton.Input active /> : skeleton : null | |
55 | + } | |
56 | + > | |
57 | + <Comp {...profile} /> | |
58 | + </React.Suspense> | |
59 | + ); | |
60 | +}; | |
61 | + | |
62 | +export interface QxDynamicComponentProps extends Record<string, any> { | |
63 | + /** | |
64 | + * 组件地址 | |
65 | + */ | |
66 | + url: string; | |
67 | + /** | |
68 | + * 移动端组件地址 | |
69 | + */ | |
70 | + h5Url: string; | |
71 | + /** | |
72 | + * 组件 props | |
73 | + */ | |
74 | + profile?: Record<string, any>; | |
75 | + /** | |
76 | + * 是否显示加载骨架 | |
77 | + */ | |
78 | + skeleton?: boolean | React.ReactNode; | |
79 | + /** | |
80 | + * 组件名称 | |
81 | + */ | |
82 | + component: string; | |
83 | + /** | |
84 | + * 文件服务,未 true 时会自动拼接文件服务器地址 | |
85 | + */ | |
86 | + fileServer?: boolean; | |
87 | + /** | |
88 | + * 组件生效范围 | |
89 | + */ | |
90 | + scope: Record<'form' | 'search' | 'table' | 'card', boolean>; | |
91 | + /** | |
92 | + * 请求方法 | |
93 | + */ | |
94 | + request?: any; | |
95 | + /** | |
96 | + * 配置项 | |
97 | + */ | |
98 | + config?: Partial<Record<PlatformType, QxDynamicComponentProps>>; | |
99 | +} | |
100 | + | |
101 | +export type PlatformType = 'h5' | 'pc'; | ... | ... |
src/qx-progress/index.less
0 → 100644
1 | +@import '~@qx/ui/src/style/variable.less'; | |
2 | + | |
3 | +.qx-progress__horizontal { | |
4 | + width: 100%; | |
5 | + min-width: 128px; | |
6 | + max-width: 208px; | |
7 | + height: 32px; | |
8 | + | |
9 | + .ant-progress-line { | |
10 | + display: flex; | |
11 | + align-items: center; | |
12 | + justify-content: space-between; | |
13 | + width: 100%; | |
14 | + } | |
15 | + .ant-progress-show-info .ant-progress-outer { | |
16 | + margin-right: unset; | |
17 | + padding-right: unset; | |
18 | + } | |
19 | + .ant-progress-outer { | |
20 | + flex: 1; | |
21 | + } | |
22 | + | |
23 | + .ant-progress-text { | |
24 | + width: 40px; | |
25 | + overflow: hidden; | |
26 | + color: @N9; | |
27 | + white-space: nowrap; | |
28 | + text-overflow: ellipsis; | |
29 | + } | |
30 | + | |
31 | + .ant-progress-bg { | |
32 | + background-color: @B8; | |
33 | + } | |
34 | +} | |
35 | + | |
36 | +.qx-progress__horizontal--look { | |
37 | + min-width: 146px; | |
38 | + max-width: 226px; | |
39 | +} | |
40 | +.qx-progress__annular { | |
41 | + .ant-progress-inner { | |
42 | + svg { | |
43 | + color: @B8; | |
44 | + } | |
45 | + } | |
46 | + .ant-progress-status-success | |
47 | + .ant-progress-inner:not(.ant-progress-circle-gradient) | |
48 | + .ant-progress-circle-path { | |
49 | + stroke: @B8; | |
50 | + } | |
51 | + .ant-progress-circle.ant-progress-status-success .ant-progress-text { | |
52 | + color: @N9; | |
53 | + } | |
54 | + | |
55 | + .qx-pa--main .ant-progress-text { | |
56 | + display: block; | |
57 | + overflow: hidden; | |
58 | + white-space: nowrap; | |
59 | + text-align: center; | |
60 | + text-overflow: ellipsis; | |
61 | + } | |
62 | + .qx-pa--default { | |
63 | + .ant-progress-text { | |
64 | + width: 40px; | |
65 | + padding: 0 2px; | |
66 | + font-size: 12px; | |
67 | + } | |
68 | + } | |
69 | + .qx-pa--middle { | |
70 | + .ant-progress-text { | |
71 | + width: 64px; | |
72 | + margin-top: -5px; | |
73 | + margin-left: -16px; | |
74 | + padding: 0 4px; | |
75 | + font-size: 20px; | |
76 | + text-align: center; | |
77 | + scale: 0.5; | |
78 | + } | |
79 | + } | |
80 | + .qx-pa--small { | |
81 | + .ant-progress-text { | |
82 | + width: 48px; | |
83 | + margin-top: -4px; | |
84 | + margin-left: -12px; | |
85 | + padding: 0 4px; | |
86 | + font-size: 16px; | |
87 | + text-align: center; | |
88 | + scale: 0.5; | |
89 | + } | |
90 | + } | |
91 | +} | |
92 | +.qx-progress__horizontal, | |
93 | +.qx-progress__annular { | |
94 | + display: flex; | |
95 | + align-items: center; | |
96 | +} | |
97 | + | |
98 | +.qx-progress__look { | |
99 | + margin-left: 4px; | |
100 | + color: @N7; | |
101 | + cursor: pointer; | |
102 | + | |
103 | + &:hover { | |
104 | + color: @B8; | |
105 | + } | |
106 | +} | |
107 | + | |
108 | +.qx-pa-tooltip, | |
109 | +.qx-ph-tooltip { | |
110 | + .ant-tooltip-inner { | |
111 | + color: @N9; | |
112 | + } | |
113 | +} | ... | ... |
src/qx-progress/index.md
0 → 100644
1 | +--- | |
2 | +nav: | |
3 | + path: /component | |
4 | + title: 组件 | |
5 | + order: 1 | |
6 | +group: | |
7 | + path: /common | |
8 | + title: 百分比进度条 | |
9 | + order: 0 | |
10 | +--- | |
11 | + | |
12 | +## QxProgress 百分比进度条 | |
13 | + | |
14 | +### 水平进度条 | |
15 | + | |
16 | +```tsx | |
17 | +import React from 'react'; | |
18 | +import { QxProgress } from '@qx/common'; | |
19 | + | |
20 | +export default () => { | |
21 | + return <QxProgress value={34} formatValue={'这是34%'} type={'bar'} />; | |
22 | +}; | |
23 | +``` | |
24 | + | |
25 | +### 环状进度条 | |
26 | + | |
27 | +```tsx | |
28 | +import React from 'react'; | |
29 | +import { QxProgress } from '@qx/common'; | |
30 | + | |
31 | +export default () => { | |
32 | + return <QxProgress value={34} type={'circle'} />; | |
33 | +}; | |
34 | +``` | |
35 | + | |
36 | +### 环状进度条小中大 | |
37 | + | |
38 | +```tsx | |
39 | +import React from 'react'; | |
40 | +import { QxProgress } from '@qx/common'; | |
41 | + | |
42 | +export default () => { | |
43 | + return ( | |
44 | + <div style={{ display: 'flex', alignItems: 'center' }}> | |
45 | + <QxProgress value={34} formatValue={'34%'} type={'circle'} size={'small'} /> | |
46 | + <QxProgress value={34} formatValue={'34%'} type={'circle'} size={'middle'} /> | |
47 | + <QxProgress value={34} formatValue={'34%'} type={'circle'} size={'default'} /> | |
48 | + </div> | |
49 | + ); | |
50 | +}; | |
51 | +``` | |
52 | + | |
53 | +### 隐私保护条形 | |
54 | + | |
55 | +```tsx | |
56 | +import React, { useState } from 'react'; | |
57 | +import { QxProgress } from '@qx/common'; | |
58 | + | |
59 | +export default () => { | |
60 | + return ( | |
61 | + <QxProgress | |
62 | + value={34} | |
63 | + formatValue={'34%'} | |
64 | + type={'bar'} | |
65 | + controlPrivacy={true} | |
66 | + privacy={true} | |
67 | + size={'default'} | |
68 | + /> | |
69 | + ); | |
70 | +}; | |
71 | +``` | |
72 | + | |
73 | +### 隐私保护环状 | |
74 | + | |
75 | +```tsx | |
76 | +import React, { useState } from 'react'; | |
77 | +import { QxProgress } from '@qx/common'; | |
78 | + | |
79 | +export default () => { | |
80 | + const [privacy, setPrivacy] = useState(true); | |
81 | + return ( | |
82 | + <QxProgress | |
83 | + value={34} | |
84 | + formatValue={'34%'} | |
85 | + type={'circle'} | |
86 | + controlPrivacy={true} | |
87 | + privacy={true} | |
88 | + size={'default'} | |
89 | + /> | |
90 | + ); | |
91 | +}; | |
92 | +``` | |
93 | + | |
94 | +<API></API> | ... | ... |
src/qx-progress/index.tsx
0 → 100644
1 | +import React, { useState } from 'react'; | |
2 | +import { Progress, Tooltip } from 'antd'; | |
3 | +import './index.less'; | |
4 | +import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; | |
5 | + | |
6 | +interface QxProgressProps { | |
7 | + /** | |
8 | + * @description 值 | |
9 | + */ | |
10 | + value: number; | |
11 | + /** | |
12 | + * @description 进度条类型 | |
13 | + * @default "circle" | |
14 | + */ | |
15 | + type?: 'bar' | 'circle'; | |
16 | + /** | |
17 | + * @description 环状进度条规格 | |
18 | + * @default "small" | |
19 | + */ | |
20 | + size?: 'small' | 'middle' | 'default' | string | undefined; | |
21 | + /** | |
22 | + * @description 是否开启隐私保护 | |
23 | + * @default "false" | |
24 | + */ | |
25 | + privacy?: boolean; | |
26 | + /** | |
27 | + * @description 自定义显示的内容 | |
28 | + */ | |
29 | + formatValue?: string | number; | |
30 | + /** | |
31 | + * @description 是否需要控制隐私保护的按钮 | |
32 | + * @default "false" | |
33 | + */ | |
34 | + controlPrivacy?: boolean; | |
35 | + /** | |
36 | + * @description 位置 | |
37 | + * @default "left" | |
38 | + */ | |
39 | + align?: string; | |
40 | +} | |
41 | + | |
42 | +export const QxProgress: React.FC<QxProgressProps> = (props) => { | |
43 | + const { value, type, size, privacy, formatValue, controlPrivacy, align } = props; | |
44 | + const [privacyLocal, setPrivacyLocal] = useState<boolean>(privacy); | |
45 | + | |
46 | + return ( | |
47 | + <> | |
48 | + {value || typeof value === 'number' ? ( | |
49 | + <> | |
50 | + {type === 'bar' ? ( | |
51 | + <div | |
52 | + className={`qx-progress__horizontal ${ | |
53 | + controlPrivacy && privacy ? 'qx-progress__horizontal--look' : '' | |
54 | + }`} | |
55 | + style={{ justifyContent: align || 'left' }} | |
56 | + > | |
57 | + <Progress | |
58 | + percent={ | |
59 | + (!controlPrivacy && privacy) || (controlPrivacy && privacyLocal) ? 0 : value | |
60 | + } | |
61 | + format={() => ( | |
62 | + <Tooltip | |
63 | + overlayClassName={'qx-ph-tooltip'} | |
64 | + title={ | |
65 | + (!controlPrivacy && privacy) || (controlPrivacy && privacyLocal) | |
66 | + ? '***%' | |
67 | + : formatValue || `${value}%` | |
68 | + } | |
69 | + color={'#fff'} | |
70 | + > | |
71 | + {(!controlPrivacy && privacy) || (controlPrivacy && privacyLocal) | |
72 | + ? '***%' | |
73 | + : formatValue || `${value}%`} | |
74 | + </Tooltip> | |
75 | + )} | |
76 | + /> | |
77 | + {controlPrivacy && privacy ? ( | |
78 | + <span | |
79 | + className={'qx-progress__look'} | |
80 | + onClick={() => { | |
81 | + setPrivacyLocal(!privacyLocal); | |
82 | + }} | |
83 | + > | |
84 | + {privacyLocal ? <EyeInvisibleOutlined /> : <EyeOutlined />} | |
85 | + </span> | |
86 | + ) : null} | |
87 | + </div> | |
88 | + ) : ( | |
89 | + <div className={'qx-progress__annular'} style={{ justifyContent: align || 'left' }}> | |
90 | + <Progress | |
91 | + strokeWidth={10} | |
92 | + type="circle" | |
93 | + format={() => ( | |
94 | + <Tooltip | |
95 | + overlayClassName={'qx-pa-tooltip'} | |
96 | + title={ | |
97 | + (!controlPrivacy && privacy) || (controlPrivacy && privacyLocal) | |
98 | + ? '***%' | |
99 | + : formatValue || `${value}%` | |
100 | + } | |
101 | + color={'#fff'} | |
102 | + > | |
103 | + {(!controlPrivacy && privacy) || (controlPrivacy && privacyLocal) | |
104 | + ? '***%' | |
105 | + : formatValue || `${value}%`} | |
106 | + </Tooltip> | |
107 | + )} | |
108 | + percent={ | |
109 | + (!controlPrivacy && privacy) || (controlPrivacy && privacyLocal) ? 0 : value | |
110 | + } | |
111 | + className={`qx-pa--main ${ | |
112 | + size === 'default' | |
113 | + ? 'qx-pa--default' | |
114 | + : size === 'middle' | |
115 | + ? 'qx-pa--middle' | |
116 | + : 'qx-pa--small' | |
117 | + }`} | |
118 | + width={size === 'default' ? 40 : size === 'middle' ? 32 : 24} | |
119 | + /> | |
120 | + {controlPrivacy && privacy ? ( | |
121 | + <span | |
122 | + className={'qx-progress__look'} | |
123 | + onClick={() => { | |
124 | + setPrivacyLocal(!privacyLocal); | |
125 | + }} | |
126 | + > | |
127 | + {privacyLocal ? <EyeInvisibleOutlined /> : <EyeOutlined />} | |
128 | + </span> | |
129 | + ) : null} | |
130 | + </div> | |
131 | + )} | |
132 | + </> | |
133 | + ) : null} | |
134 | + </> | |
135 | + ); | |
136 | +}; | ... | ... |
src/qx-search-input/index.less
0 → 100644
src/qx-search-input/index.md
0 → 100644
1 | +--- | |
2 | +nav: | |
3 | + path: /component | |
4 | + title: 组件 | |
5 | + order: 1 | |
6 | +group: | |
7 | + path: /common | |
8 | + title: 公共组件方法 | |
9 | + order: 0 | |
10 | +--- | |
11 | + | |
12 | +## QxSearchInput 实时搜索框 | |
13 | + | |
14 | +Demo: | |
15 | + | |
16 | +```tsx | |
17 | +import { QxSearchInput } from '@qx/common'; | |
18 | +import type { InputRef } from 'antd'; | |
19 | +import { Button } from 'antd'; | |
20 | +import { useRef, useState } from 'react'; | |
21 | + | |
22 | +export default () => { | |
23 | + // 输入框内容 | |
24 | +const [keyValue, setKeyValue] = useState(''); | |
25 | +const searchInputRef = useRef<InputRef>(null); | |
26 | + | |
27 | +const handleChange = (val: string) => { | |
28 | + // 输入框内容变化时的回调、按下回车的回调 | |
29 | + setKeyValue(val); | |
30 | +}; | |
31 | + return ( | |
32 | + <div> | |
33 | + <Button | |
34 | + type="primary" | |
35 | + onClick={() => { | |
36 | + // 自动聚焦 | |
37 | + searchInputRef?.current!.focus({ | |
38 | + cursor: 'end', | |
39 | + }); | |
40 | + }} | |
41 | + > | |
42 | + 聚焦 | |
43 | + </Button> | |
44 | + <QxSearchInput | |
45 | + searchInputRef={searchInputRef} | |
46 | + value={keyValue} | |
47 | + onChange={handleChange} | |
48 | + onPressEnter={handleChange} | |
49 | + // prefix={prefixIcon} | |
50 | + /> | |
51 | + </div> | |
52 | + ); | |
53 | +}; | |
54 | +``` | |
55 | + | |
56 | +<API></API> | |
57 | + | |
58 | +More skills for writing demo: https://d.umijs.org/guide/basic#write-component-demo | ... | ... |
src/qx-search-input/index.tsx
0 → 100644
1 | +import React, { CSSProperties, Ref } from 'react'; | |
2 | +import { Input } from 'antd'; | |
3 | +import type { InputProps, InputRef } from 'antd'; | |
4 | +import { SearchOutlined } from '@ant-design/icons'; | |
5 | +import './index.less'; | |
6 | + | |
7 | +const prefixCls = 'qx-search-input'; | |
8 | + | |
9 | +type QxSearchInputProps = { | |
10 | + // searchInputRef.current 获取当前input Dom 元素 自动聚焦 | |
11 | + searchInputRef?: Ref<InputRef>; | |
12 | + // 自定义样式 style | |
13 | + style?: CSSProperties; | |
14 | + // 轻提示 | |
15 | + placeholder?: string; | |
16 | + // 支持 antd-Input 自身API | |
17 | +} & InputProps; | |
18 | + | |
19 | +export const QxSearchInput: React.FC<QxSearchInputProps> = (props) => { | |
20 | + const defaultPrefixIcon: React.ReactNode = <SearchOutlined />; | |
21 | + | |
22 | + return ( | |
23 | + <Input | |
24 | + ref={props.searchInputRef} | |
25 | + className={prefixCls} | |
26 | + placeholder={props?.placeholder || '请输入搜索内容'} | |
27 | + prefix={props?.prefix || defaultPrefixIcon} | |
28 | + {...props} | |
29 | + /> | |
30 | + ); | |
31 | +}; | ... | ... |