@jiangood/open-admin 2.5.5 → 2.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/common-plugin.js +2 -2
- package/package.json +3 -2
- package/src/.umi-production/appData.json +91 -90
- package/src/.umi-production/core/route.tsx +6 -6
- package/src/.umi-production/umi.ts +1 -1
- package/src/framework/GlobalData.d.ts +8 -0
- package/src/framework/GlobalData.js +33 -0
- package/src/framework/{components/system → biz}/HasPerm.tsx +1 -1
- package/src/framework/{components/OrgTree/index.tsx → biz/OrgTree.tsx} +12 -2
- package/src/framework/{components/system/ButtonList.tsx → biz/PermActions.tsx} +6 -5
- package/src/framework/{components/RoleTree/index.tsx → biz/RoleTree.tsx} +13 -3
- package/src/framework/{components/system → biz}/index.tsx +4 -2
- package/src/framework/components/ContextMenu/index.tsx +12 -4
- package/src/framework/components/DownloadModal/index.tsx +332 -0
- package/src/framework/components/ErrorBoundary/index.tsx +18 -0
- package/src/framework/components/NamedIcon/{index.jsx → index.tsx} +8 -3
- package/src/framework/components/Page/index.tsx +2 -2
- package/src/framework/components/ProTable/index.tsx +3 -10
- package/src/framework/components/index.ts +2 -6
- package/src/framework/fields/BaseRemoteSelect/index.tsx +2 -3
- package/src/framework/fields/FieldBoolean/index.tsx +3 -4
- package/src/framework/fields/FieldDate/index.tsx +0 -21
- package/src/framework/fields/FieldEditor/index.tsx +35 -44
- package/src/framework/fields/FieldNumberRange/index.tsx +2 -2
- package/src/framework/fields/FieldRemoteTree/index.tsx +1 -1
- package/src/framework/fields/FieldTable/index.tsx +24 -27
- package/src/framework/fields/FieldUploadFile/index.tsx +10 -3
- package/src/framework/fields/index.ts +1 -3
- package/src/framework/index.ts +2 -0
- package/src/framework/utils/ArrayUtils.d.ts +6 -0
- package/src/framework/utils/ArrayUtils.js +30 -0
- package/src/framework/utils/DateUtils.d.ts +20 -0
- package/src/framework/utils/DateUtils.js +81 -0
- package/src/framework/utils/DeviceUtils.d.ts +3 -0
- package/src/framework/utils/DeviceUtils.js +10 -0
- package/src/framework/utils/EventBus.d.ts +6 -0
- package/src/framework/utils/EventBus.js +51 -0
- package/src/framework/utils/MessageUtils.d.ts +10 -0
- package/src/framework/utils/MessageUtils.js +43 -0
- package/src/framework/utils/ObjectUtils.d.ts +14 -0
- package/src/framework/utils/ObjectUtils.js +12 -0
- package/src/framework/utils/StorageUtils.d.ts +4 -0
- package/src/framework/utils/{StorageUtils.ts → StorageUtils.js} +7 -11
- package/src/framework/utils/StringUtils.d.ts +17 -0
- package/src/framework/utils/StringUtils.js +107 -0
- package/src/framework/utils/TreeUtils.d.ts +13 -0
- package/src/framework/utils/TreeUtils.js +64 -0
- package/src/framework/utils/UrlUtils.d.ts +6 -0
- package/src/framework/utils/UrlUtils.js +71 -0
- package/src/framework/utils/index.ts +1 -5
- package/src/framework/utils/system/DictUtils.d.ts +20 -0
- package/src/framework/utils/system/DictUtils.js +31 -0
- package/src/framework/utils/system/FormRegistry.d.ts +8 -0
- package/src/framework/utils/system/FormRegistry.js +31 -0
- package/src/framework/utils/system/HttpUtils.d.ts +14 -0
- package/src/framework/utils/system/HttpUtils.js +87 -0
- package/src/framework/utils/system/PageUtils.d.ts +14 -0
- package/src/framework/utils/system/PageUtils.js +59 -0
- package/src/framework/utils/system/PermUtils.d.ts +5 -0
- package/src/framework/utils/system/PermUtils.js +27 -0
- package/src/framework/utils/system/ThemeUtils.d.ts +6 -0
- package/src/framework/utils/system/{ThemeUtils.ts → ThemeUtils.js} +19 -19
- package/src/framework/utils/system/index.ts +1 -2
- package/src/framework/views/ViewFile/index.tsx +3 -3
- package/src/framework/views/ViewImage/index.tsx +3 -3
- package/src/framework/views/{ViewBooleanEnableDisable → ViewSwitch}/index.tsx +1 -1
- package/src/framework/views/index.ts +3 -4
- package/src/layouts/admin/HeaderRight.jsx +32 -47
- package/src/layouts/admin/index.jsx +5 -29
- package/src/layouts/index.jsx +29 -19
- package/src/pages/about.jsx +0 -1
- package/src/pages/{userCenter/ChangePassword.jsx → account/changePassword.jsx} +1 -2
- package/src/pages/{userCenter → account}/index.jsx +2 -6
- package/src/pages/forceUpdatePwd.jsx +89 -0
- package/src/pages/login.jsx +5 -5
- package/src/pages/system/dict/index.jsx +6 -6
- package/src/pages/system/file/index.jsx +4 -4
- package/src/pages/system/job/index.jsx +2 -2
- package/src/pages/system/org/index.jsx +2 -2
- package/src/pages/system/role/index.jsx +4 -4
- package/src/pages/system/role/{perm.jsx → rolePerm.jsx} +2 -2
- package/src/pages/system/user/index.jsx +4 -4
- package/src/framework/components/DownloadFileButton/index.tsx +0 -33
- package/src/framework/components/NamedIcon/index.d.ts +0 -7
- package/src/framework/components/ProTable/utils/index.ts +0 -62
- package/src/framework/fields/FieldRemoteSelectMultipleInline/index.tsx +0 -33
- package/src/framework/fields/FieldRemoteTreeSelectMultiple/index.tsx +0 -43
- package/src/framework/utils/ArrayUtils.ts +0 -186
- package/src/framework/utils/ColorsUtils.ts +0 -378
- package/src/framework/utils/DateUtils.ts +0 -187
- package/src/framework/utils/DeviceUtils.ts +0 -46
- package/src/framework/utils/DomUtils.ts +0 -50
- package/src/framework/utils/EventBusUtils.ts +0 -143
- package/src/framework/utils/Logger.ts +0 -40
- package/src/framework/utils/MessageUtils.tsx +0 -90
- package/src/framework/utils/ObjectUtils.ts +0 -118
- package/src/framework/utils/StringUtils.ts +0 -436
- package/src/framework/utils/TreeUtils.ts +0 -251
- package/src/framework/utils/UrlUtils.ts +0 -152
- package/src/framework/utils/UuidUtils.ts +0 -88
- package/src/framework/utils/ValidateUtils.ts +0 -28
- package/src/framework/utils/system/DictUtils.ts +0 -96
- package/src/framework/utils/system/FormRegistryUtils.ts +0 -76
- package/src/framework/utils/system/HttpUtils.ts +0 -251
- package/src/framework/utils/system/PageUtils.ts +0 -163
- package/src/framework/utils/system/PermUtils.ts +0 -79
- package/src/framework/utils/system/SysUtils.ts +0 -105
- package/src/framework/views/ViewProcessInstanceProgress/index.tsx +0 -98
- package/src/framework/views/ViewProcessInstanceProgressButton/index.tsx +0 -23
- package/src/pages/userCenter/message.jsx +0 -105
- /package/src/framework/{components → views}/ViewRange/index.tsx +0 -0
- /package/src/pages/system/user/{UserPerm.jsx → userPerm.jsx} +0 -0
|
@@ -19,14 +19,14 @@ if (process.env.NODE_ENV === 'development') {
|
|
|
19
19
|
import React from 'react';
|
|
20
20
|
|
|
21
21
|
export async function getRoutes() {
|
|
22
|
-
const routes = {"404":{"path":"*","id":"404","parentId":"@@/global-layout"},"
|
|
22
|
+
const routes = {"404":{"path":"*","id":"404","parentId":"@@/global-layout"},"account/changePassword":{"path":"account/changePassword","id":"account/changePassword","parentId":"@@/global-layout"},"system/role/rolePerm":{"path":"system/role/rolePerm","id":"system/role/rolePerm","parentId":"@@/global-layout"},"system/user/userPerm":{"path":"system/user/userPerm","id":"system/user/userPerm","parentId":"@@/global-layout"},"system/dict/index":{"path":"system/dict","id":"system/dict/index","parentId":"@@/global-layout"},"system/file/index":{"path":"system/file","id":"system/file/index","parentId":"@@/global-layout"},"system/role/index":{"path":"system/role","id":"system/role/index","parentId":"@@/global-layout"},"system/user/index":{"path":"system/user","id":"system/user/index","parentId":"@@/global-layout"},"system/job/index":{"path":"system/job","id":"system/job/index","parentId":"@@/global-layout"},"system/log/index":{"path":"system/log","id":"system/log/index","parentId":"@@/global-layout"},"system/org/index":{"path":"system/org","id":"system/org/index","parentId":"@@/global-layout"},"forceUpdatePwd":{"path":"forceUpdatePwd","id":"forceUpdatePwd","parentId":"@@/global-layout"},"account/index":{"path":"account","id":"account/index","parentId":"@@/global-layout"},"about":{"path":"about","id":"about","parentId":"@@/global-layout"},"index":{"path":"/","id":"index","parentId":"@@/global-layout"},"login":{"path":"login","id":"login","parentId":"@@/global-layout"},"@@/global-layout":{"id":"@@/global-layout","path":"/","isLayout":true}} as const;
|
|
23
23
|
return {
|
|
24
24
|
routes,
|
|
25
25
|
routeComponents: {
|
|
26
26
|
'404': React.lazy(() => import(/* webpackChunkName: "src__pages__404" */'../../../src/pages/404.jsx')),
|
|
27
|
-
'
|
|
28
|
-
'system/
|
|
29
|
-
'
|
|
27
|
+
'account/changePassword': React.lazy(() => import(/* webpackChunkName: "src__pages__account__changePassword" */'../../../src/pages/account/changePassword.jsx')),
|
|
28
|
+
'system/role/rolePerm': React.lazy(() => import(/* webpackChunkName: "src__pages__system__role__rolePerm" */'../../../src/pages/system/role/rolePerm.jsx')),
|
|
29
|
+
'system/user/userPerm': React.lazy(() => import(/* webpackChunkName: "src__pages__system__user__userPerm" */'../../../src/pages/system/user/userPerm.jsx')),
|
|
30
30
|
'system/dict/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__dict__index" */'../../../src/pages/system/dict/index.jsx')),
|
|
31
31
|
'system/file/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__file__index" */'../../../src/pages/system/file/index.jsx')),
|
|
32
32
|
'system/role/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__role__index" */'../../../src/pages/system/role/index.jsx')),
|
|
@@ -34,8 +34,8 @@ export async function getRoutes() {
|
|
|
34
34
|
'system/job/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__job__index" */'../../../src/pages/system/job/index.jsx')),
|
|
35
35
|
'system/log/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__log__index" */'../../../src/pages/system/log/index.jsx')),
|
|
36
36
|
'system/org/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__org__index" */'../../../src/pages/system/org/index.jsx')),
|
|
37
|
-
'
|
|
38
|
-
'
|
|
37
|
+
'forceUpdatePwd': React.lazy(() => import(/* webpackChunkName: "src__pages__forceUpdatePwd" */'../../../src/pages/forceUpdatePwd.jsx')),
|
|
38
|
+
'account/index': React.lazy(() => import(/* webpackChunkName: "src__pages__account__index" */'../../../src/pages/account/index.jsx')),
|
|
39
39
|
'about': React.lazy(() => import(/* webpackChunkName: "src__pages__about" */'../../../src/pages/about.jsx')),
|
|
40
40
|
'index': React.lazy(() => import(/* webpackChunkName: "src__pages__index" */'../../../src/pages/index.jsx')),
|
|
41
41
|
'login': React.lazy(() => import(/* webpackChunkName: "src__pages__login" */'../../../src/pages/login.jsx')),
|
|
@@ -9,7 +9,7 @@ import { createPluginManager } from './core/plugin';
|
|
|
9
9
|
import { createHistory } from './core/history';
|
|
10
10
|
import Loading from '/home/runner/work/open-admin/open-admin/web/src/loading.jsx';
|
|
11
11
|
import { ApplyPluginsType } from 'umi';
|
|
12
|
-
import {
|
|
12
|
+
import {FormRegistry} from '/home/runner/work/open-admin/open-admin/web/src/framework';
|
|
13
13
|
|
|
14
14
|
const publicPath = "./";
|
|
15
15
|
const runtimePublicPath = false;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export class GlobalData {
|
|
2
|
+
static setSiteInfo(data: object): void;
|
|
3
|
+
static getSiteInfo(): object;
|
|
4
|
+
static setLoginInfo(data: object): void;
|
|
5
|
+
static getLoginInfo(): object;
|
|
6
|
+
static setDictInfo(data: object | null): void;
|
|
7
|
+
static getDictInfo(): object | null;
|
|
8
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {StorageUtils} from './utils/StorageUtils';
|
|
2
|
+
|
|
3
|
+
const SITE_INFO_KEY = "siteInfo";
|
|
4
|
+
const LOGIN_INFO_KEY = "loginInfo";
|
|
5
|
+
const DICT_INFO_KEY = "dictInfo";
|
|
6
|
+
|
|
7
|
+
export class GlobalData {
|
|
8
|
+
static setSiteInfo(data) {
|
|
9
|
+
StorageUtils.set(SITE_INFO_KEY, data);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static getSiteInfo() {
|
|
13
|
+
return StorageUtils.get(SITE_INFO_KEY) || {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static setLoginInfo(data) {
|
|
17
|
+
StorageUtils.set(LOGIN_INFO_KEY, data);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getLoginInfo() {
|
|
21
|
+
return StorageUtils.get(LOGIN_INFO_KEY) || {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static setDictInfo(data) {
|
|
25
|
+
StorageUtils.set(DICT_INFO_KEY, data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static getDictInfo() {
|
|
29
|
+
return StorageUtils.get(DICT_INFO_KEY);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { SITE_INFO_KEY, LOGIN_INFO_KEY, DICT_INFO_KEY };
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import {Alert, Skeleton, Tree} from 'antd';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import * as Icons from '@ant-design/icons';
|
|
4
|
-
import {HttpUtils} from "
|
|
4
|
+
import {HttpUtils} from "../utils";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface OrgTreeProps {
|
|
7
|
+
onChange?: (orgId: string | null) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface OrgTreeState {
|
|
11
|
+
treeDataLoading: boolean;
|
|
12
|
+
treeData: any[];
|
|
13
|
+
currentOrgId: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class OrgTree extends React.Component<OrgTreeProps, OrgTreeState> {
|
|
7
17
|
|
|
8
18
|
state = {
|
|
9
19
|
treeDataLoading: true,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Space } from 'antd';
|
|
3
|
-
import { PermUtils } from '
|
|
3
|
+
import { PermUtils } from '../utils';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @param
|
|
6
|
+
* 带权限的操作区。根据子元素的 `perm` 属性控制显隐,默认用 Space 包裹。
|
|
7
|
+
* @param gap - 是否在操作项之间增加间距,默认 true
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
10
|
-
const { children } = props;
|
|
9
|
+
export function PermActions(props) {
|
|
10
|
+
const { children, gap = true } = props;
|
|
11
11
|
|
|
12
12
|
const checkPerm = (element) => {
|
|
13
13
|
const _props = element?.props;
|
|
@@ -18,5 +18,6 @@ export function ButtonList(props) {
|
|
|
18
18
|
(child) => child != null && checkPerm(child)
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
+
if (!gap) return <>{nodes}</>;
|
|
21
22
|
return <Space>{nodes}</Space>;
|
|
22
23
|
}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import {Skeleton, Tree} from 'antd';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import {SolutionOutlined} from '@ant-design/icons';
|
|
4
|
-
import {HttpUtils} from "
|
|
4
|
+
import {HttpUtils} from "../utils";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface RoleTreeProps {
|
|
7
|
+
onSelect?: (orgId: string | null) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RoleTreeState {
|
|
11
|
+
treeDataLoading: boolean;
|
|
12
|
+
treeData: any[];
|
|
13
|
+
currentOrgId: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class RoleTree extends React.Component<RoleTreeProps, RoleTreeState> {
|
|
7
17
|
|
|
8
18
|
state = {
|
|
9
19
|
treeDataLoading: true,
|
|
@@ -25,7 +35,7 @@ export class RoleTree extends React.Component<any, any> {
|
|
|
25
35
|
render() {
|
|
26
36
|
const {treeData, treeDataLoading} = this.state
|
|
27
37
|
if (treeDataLoading) {
|
|
28
|
-
return <Skeleton title='
|
|
38
|
+
return <Skeleton active title={{ width: '60%' }} paragraph={{ rows: 6 }} />;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
return <Tree
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {FieldRemoteSelect, FieldRemoteSelectMultiple, FieldRemoteTreeSelect} from "
|
|
1
|
+
import {FieldRemoteSelect, FieldRemoteSelectMultiple, FieldRemoteTreeSelect} from "../fields";
|
|
2
2
|
import React from "react";
|
|
3
3
|
|
|
4
|
-
export * from './
|
|
4
|
+
export * from './PermActions'
|
|
5
5
|
export * from './HasPerm'
|
|
6
|
+
export * from './OrgTree'
|
|
7
|
+
export * from './RoleTree'
|
|
6
8
|
|
|
7
9
|
export function FieldUserSelect(props) {
|
|
8
10
|
return <FieldRemoteSelect url="admin/sysUser/options" {...props} />;
|
|
@@ -21,17 +21,25 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, items, onClick,
|
|
|
21
21
|
const ref = useRef<HTMLDivElement>(null);
|
|
22
22
|
|
|
23
23
|
useEffect(() => {
|
|
24
|
-
const
|
|
24
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
25
25
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
26
26
|
onClose();
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
+
const handleScroll = () => onClose();
|
|
30
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
31
|
+
if (e.key === 'Escape') onClose();
|
|
32
|
+
};
|
|
29
33
|
const timer = setTimeout(() => {
|
|
30
|
-
document.addEventListener('mousedown',
|
|
34
|
+
document.addEventListener('mousedown', handleMouseDown);
|
|
35
|
+
document.addEventListener('scroll', handleScroll, true);
|
|
36
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
31
37
|
}, 0);
|
|
32
38
|
return () => {
|
|
33
39
|
clearTimeout(timer);
|
|
34
|
-
document.removeEventListener('mousedown',
|
|
40
|
+
document.removeEventListener('mousedown', handleMouseDown);
|
|
41
|
+
document.removeEventListener('scroll', handleScroll, true);
|
|
42
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
35
43
|
};
|
|
36
44
|
}, [onClose]);
|
|
37
45
|
|
|
@@ -66,7 +74,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, items, onClick,
|
|
|
66
74
|
padding: '5px 12px',
|
|
67
75
|
fontSize: 13,
|
|
68
76
|
cursor: item.disabled ? 'not-allowed' : 'pointer',
|
|
69
|
-
color: item.danger ? '#ff4d4f' : item.disabled ? '
|
|
77
|
+
color: item.danger ? '#ff4d4f' : item.disabled ? 'rgba(0,0,0,0.25)' : '#333',
|
|
70
78
|
display: 'flex',
|
|
71
79
|
alignItems: 'center',
|
|
72
80
|
gap: 8,
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {Button, Modal, Progress, Tag} from "antd";
|
|
3
|
+
import {
|
|
4
|
+
DownloadOutlined,
|
|
5
|
+
CheckCircleOutlined,
|
|
6
|
+
CloseCircleOutlined,
|
|
7
|
+
} from "@ant-design/icons";
|
|
8
|
+
import axios from "axios";
|
|
9
|
+
import qs from 'qs';
|
|
10
|
+
|
|
11
|
+
const axiosInstance = axios.create({
|
|
12
|
+
baseURL: (typeof SERVLET_CONTEXT !== 'undefined' && SERVLET_CONTEXT) || '',
|
|
13
|
+
withCredentials: true,
|
|
14
|
+
headers: {'Content-Type': 'application/json'},
|
|
15
|
+
paramsSerializer: (params) => qs.stringify(params, {indices: false})
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
interface DownloadOptions {
|
|
19
|
+
url: string;
|
|
20
|
+
params?: Record<string, any>;
|
|
21
|
+
data?: Record<string, any>;
|
|
22
|
+
method?: 'GET' | 'POST';
|
|
23
|
+
fileName?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ModalState {
|
|
27
|
+
open: boolean;
|
|
28
|
+
status: '' | 'downloading' | 'completed' | 'failed';
|
|
29
|
+
fileName: string;
|
|
30
|
+
progress: number;
|
|
31
|
+
loaded: number;
|
|
32
|
+
total: number;
|
|
33
|
+
speed: string;
|
|
34
|
+
errorMessage: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class DownloadModal extends React.Component<{}, ModalState> {
|
|
38
|
+
static instance: DownloadModal | null = null;
|
|
39
|
+
private abortController: AbortController | null = null;
|
|
40
|
+
private lastOptions: DownloadOptions | null = null;
|
|
41
|
+
private lastTime: number = 0;
|
|
42
|
+
private lastLoaded: number = 0;
|
|
43
|
+
private speedTimer: ReturnType<typeof setInterval> | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(props: {}) {
|
|
46
|
+
super(props);
|
|
47
|
+
this.state = {
|
|
48
|
+
open: false,
|
|
49
|
+
status: '',
|
|
50
|
+
fileName: '',
|
|
51
|
+
progress: 0,
|
|
52
|
+
loaded: 0,
|
|
53
|
+
total: 0,
|
|
54
|
+
speed: '',
|
|
55
|
+
errorMessage: '',
|
|
56
|
+
};
|
|
57
|
+
DownloadModal.instance = this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static download(options: DownloadOptions) {
|
|
61
|
+
const instance = DownloadModal.instance;
|
|
62
|
+
if (instance) {
|
|
63
|
+
instance.startDownload(options);
|
|
64
|
+
} else {
|
|
65
|
+
console.warn('[DownloadModal] 组件未挂载,download() 调用被忽略');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
componentWillUnmount() {
|
|
70
|
+
this.clearSpeedTimer();
|
|
71
|
+
DownloadModal.instance = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private clearSpeedTimer() {
|
|
75
|
+
if (this.speedTimer) {
|
|
76
|
+
clearInterval(this.speedTimer);
|
|
77
|
+
this.speedTimer = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private formatSize(bytes: number): string {
|
|
82
|
+
if (bytes === 0) return '0 B';
|
|
83
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
84
|
+
const k = 1024;
|
|
85
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
86
|
+
const size = (bytes / Math.pow(k, i)).toFixed(i > 0 ? 1 : 0);
|
|
87
|
+
return `${size} ${units[i]}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private formatSpeed(bytesPerSecond: number): string {
|
|
91
|
+
return this.formatSize(bytesPerSecond) + '/s';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private handleCancel = () => {
|
|
95
|
+
if (this.abortController) {
|
|
96
|
+
this.abortController.abort();
|
|
97
|
+
this.abortController = null;
|
|
98
|
+
}
|
|
99
|
+
this.clearSpeedTimer();
|
|
100
|
+
this.setState({open: false, status: ''});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
private handleClose = () => {
|
|
104
|
+
this.setState({open: false, status: ''});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
private handleRetry = () => {
|
|
108
|
+
if (this.lastOptions) {
|
|
109
|
+
this.startDownload(this.lastOptions);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
private saveBlob(response: any) {
|
|
114
|
+
return new Promise<void>((resolve, reject) => {
|
|
115
|
+
const {data: blob, headers} = response;
|
|
116
|
+
|
|
117
|
+
// 检查是否为 JSON 错误响应
|
|
118
|
+
if (blob.type === 'application/json') {
|
|
119
|
+
const reader = new FileReader();
|
|
120
|
+
reader.readAsText(blob, 'utf-8');
|
|
121
|
+
reader.onerror = function () {
|
|
122
|
+
reject(new Error('读取下载数据失败'));
|
|
123
|
+
};
|
|
124
|
+
reader.onload = function () {
|
|
125
|
+
try {
|
|
126
|
+
const rs = JSON.parse(reader.result as string);
|
|
127
|
+
reject(new Error(rs.message || '下载失败'));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
reject(new Error('解析错误响应失败'));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 从 Content-Disposition 解析文件名
|
|
136
|
+
const contentDisposition = headers['content-disposition'] || headers['Content-Disposition'];
|
|
137
|
+
let filename = this.state.fileName;
|
|
138
|
+
if (!filename && contentDisposition) {
|
|
139
|
+
const match = /filename\*?=(?:['"]?)(?:UTF-8''|)(.+?)(?:['"]?$|;)/i.exec(contentDisposition);
|
|
140
|
+
let parsedName = match && match[1] ? match[1].trim() : 'download.file';
|
|
141
|
+
try {
|
|
142
|
+
parsedName = decodeURIComponent(parsedName.replace(/"/g, ''));
|
|
143
|
+
} catch (e) {
|
|
144
|
+
parsedName = parsedName.replace(/"/g, '');
|
|
145
|
+
}
|
|
146
|
+
filename = parsedName;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const url = window.URL.createObjectURL(new Blob([blob]));
|
|
150
|
+
const link = document.createElement('a');
|
|
151
|
+
link.style.display = 'none';
|
|
152
|
+
link.href = url;
|
|
153
|
+
link.download = filename || 'download.file';
|
|
154
|
+
document.body.appendChild(link);
|
|
155
|
+
link.click();
|
|
156
|
+
document.body.removeChild(link);
|
|
157
|
+
window.URL.revokeObjectURL(url);
|
|
158
|
+
resolve();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
startDownload(options: DownloadOptions) {
|
|
163
|
+
// 取消之前的下载
|
|
164
|
+
if (this.abortController) {
|
|
165
|
+
this.abortController.abort();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.lastOptions = options;
|
|
169
|
+
this.abortController = new AbortController();
|
|
170
|
+
this.lastTime = Date.now();
|
|
171
|
+
this.lastLoaded = 0;
|
|
172
|
+
|
|
173
|
+
this.setState({
|
|
174
|
+
open: true,
|
|
175
|
+
status: 'downloading',
|
|
176
|
+
fileName: options.fileName || '',
|
|
177
|
+
progress: 0,
|
|
178
|
+
loaded: 0,
|
|
179
|
+
total: 0,
|
|
180
|
+
speed: '',
|
|
181
|
+
errorMessage: '',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 定时更新速度(当 total 未知时也能显示)
|
|
185
|
+
this.speedTimer = setInterval(() => {
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const elapsed = (now - this.lastTime) / 1000;
|
|
188
|
+
if (elapsed > 1 && this.lastLoaded > 0) {
|
|
189
|
+
const speed = this.lastLoaded / elapsed;
|
|
190
|
+
this.setState({speed: this.formatSpeed(speed)});
|
|
191
|
+
this.lastTime = now;
|
|
192
|
+
this.lastLoaded = 0;
|
|
193
|
+
}
|
|
194
|
+
}, 2000);
|
|
195
|
+
|
|
196
|
+
const config: any = {
|
|
197
|
+
url: options.url,
|
|
198
|
+
method: options.method || 'GET',
|
|
199
|
+
responseType: 'blob',
|
|
200
|
+
signal: this.abortController.signal,
|
|
201
|
+
onDownloadProgress: (progressEvent: any) => {
|
|
202
|
+
const {loaded, total} = progressEvent;
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
// 计算速度
|
|
205
|
+
const elapsed = (now - this.lastTime) / 1000;
|
|
206
|
+
let speed = 0;
|
|
207
|
+
if (elapsed > 1) {
|
|
208
|
+
speed = (loaded - this.lastLoaded) / elapsed;
|
|
209
|
+
this.lastTime = now;
|
|
210
|
+
this.lastLoaded = loaded;
|
|
211
|
+
}
|
|
212
|
+
this.setState({
|
|
213
|
+
progress: total ? Math.round((loaded / total) * 100) : 0,
|
|
214
|
+
loaded,
|
|
215
|
+
total,
|
|
216
|
+
speed: speed > 0 ? this.formatSpeed(speed) : this.state.speed,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (options.params) {
|
|
222
|
+
config.params = options.params;
|
|
223
|
+
}
|
|
224
|
+
if (options.data) {
|
|
225
|
+
config.data = options.data;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
axiosInstance(config).then((response: any) => {
|
|
229
|
+
this.clearSpeedTimer();
|
|
230
|
+
return this.saveBlob(response);
|
|
231
|
+
}).then(() => {
|
|
232
|
+
const total = this.state.total;
|
|
233
|
+
this.setState({
|
|
234
|
+
status: 'completed',
|
|
235
|
+
progress: 100,
|
|
236
|
+
speed: '',
|
|
237
|
+
loaded: total || this.state.loaded,
|
|
238
|
+
});
|
|
239
|
+
}).catch((error: any) => {
|
|
240
|
+
this.clearSpeedTimer();
|
|
241
|
+
if (axios.isCancel(error)) {
|
|
242
|
+
// 用户取消,不显示错误
|
|
243
|
+
this.setState({open: false, status: ''});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let msg = '下载失败';
|
|
247
|
+
if (error.message) {
|
|
248
|
+
msg = error.message;
|
|
249
|
+
}
|
|
250
|
+
console.error('[DownloadModal] 下载失败:', msg);
|
|
251
|
+
this.setState({
|
|
252
|
+
status: 'failed',
|
|
253
|
+
errorMessage: msg,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
render() {
|
|
259
|
+
const {open, status, fileName, progress, loaded, total, speed, errorMessage} = this.state;
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<Modal
|
|
263
|
+
title={<span><DownloadOutlined style={{marginRight: 8}}/>文件下载</span>}
|
|
264
|
+
open={open}
|
|
265
|
+
maskClosable={false}
|
|
266
|
+
closable={status !== 'downloading'}
|
|
267
|
+
onCancel={this.handleClose}
|
|
268
|
+
footer={
|
|
269
|
+
status === 'downloading' ? (
|
|
270
|
+
<Button danger onClick={this.handleCancel}>取消下载</Button>
|
|
271
|
+
) : status === 'failed' ? (
|
|
272
|
+
<>
|
|
273
|
+
<Button onClick={this.handleClose}>关闭</Button>
|
|
274
|
+
<Button type="primary" onClick={this.handleRetry}>重试</Button>
|
|
275
|
+
</>
|
|
276
|
+
) : null
|
|
277
|
+
}
|
|
278
|
+
destroyOnClose
|
|
279
|
+
>
|
|
280
|
+
<div style={{padding: '20px 0'}}>
|
|
281
|
+
{/* 文件名 */}
|
|
282
|
+
<div style={{marginBottom: 16, fontSize: 15, fontWeight: 500, color: '#333'}}>
|
|
283
|
+
<DownloadOutlined style={{marginRight: 8}}/>
|
|
284
|
+
{fileName || '未知文件'}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* 下载中 */}
|
|
288
|
+
{status === 'downloading' && (
|
|
289
|
+
<>
|
|
290
|
+
<Progress percent={progress} status="active" strokeColor="var(--ant-color-primary)"/>
|
|
291
|
+
<div style={{marginTop: 8, fontSize: 13, color: '#999'}}>
|
|
292
|
+
<span>{this.formatSize(loaded)}</span>
|
|
293
|
+
{total > 0 && <span> / {this.formatSize(total)}</span>}
|
|
294
|
+
{speed && <span style={{marginLeft: 12}}>{speed}</span>}
|
|
295
|
+
</div>
|
|
296
|
+
<div style={{marginTop: 8}}>
|
|
297
|
+
<Tag color="processing">下载中...</Tag>
|
|
298
|
+
</div>
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* 已完成 */}
|
|
303
|
+
{status === 'completed' && (
|
|
304
|
+
<>
|
|
305
|
+
<Progress percent={100} status="success"/>
|
|
306
|
+
<div style={{marginTop: 8, fontSize: 13, color: '#999'}}>
|
|
307
|
+
{loaded > 0 && <span>文件大小:{this.formatSize(loaded)}</span>}
|
|
308
|
+
</div>
|
|
309
|
+
<div style={{marginTop: 8}}>
|
|
310
|
+
<Tag icon={<CheckCircleOutlined/>} color="success">下载完成</Tag>
|
|
311
|
+
</div>
|
|
312
|
+
</>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{/* 失败 */}
|
|
316
|
+
{status === 'failed' && (
|
|
317
|
+
<>
|
|
318
|
+
<Progress percent={progress} status="exception"/>
|
|
319
|
+
<div style={{marginTop: 8, fontSize: 13, color: '#ff4d4f'}}>
|
|
320
|
+
<CloseCircleOutlined style={{marginRight: 4}}/>
|
|
321
|
+
{errorMessage}
|
|
322
|
+
</div>
|
|
323
|
+
<div style={{marginTop: 8}}>
|
|
324
|
+
<Tag icon={<CloseCircleOutlined/>} color="error">下载失败</Tag>
|
|
325
|
+
</div>
|
|
326
|
+
</>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
</Modal>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -6,6 +6,8 @@ interface Props {
|
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
/** 自定义降级 UI */
|
|
8
8
|
fallback?: React.ReactNode;
|
|
9
|
+
/** 最小化模式(仅显示标题和刷新按钮,不显示错误详情) */
|
|
10
|
+
minimal?: boolean;
|
|
9
11
|
/** 错误回调,可用于上报 */
|
|
10
12
|
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
11
13
|
}
|
|
@@ -46,6 +48,22 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
|
|
46
48
|
return this.props.fallback;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// minimal 模式:简洁降级,只引导刷新
|
|
52
|
+
if (this.props.minimal) {
|
|
53
|
+
return (
|
|
54
|
+
<Result
|
|
55
|
+
status="error"
|
|
56
|
+
title="应用出现异常"
|
|
57
|
+
subTitle="页面渲染时发生错误,请尝试刷新页面。"
|
|
58
|
+
extra={[
|
|
59
|
+
<Button key="refresh" onClick={() => window.location.reload()}>
|
|
60
|
+
刷新页面
|
|
61
|
+
</Button>,
|
|
62
|
+
]}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
const detailStyle: React.CSSProperties = {
|
|
50
68
|
maxHeight: 300, overflow: 'auto',
|
|
51
69
|
fontSize: 12, fontFamily: 'monospace',
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import * as Icons from '@ant-design/icons';
|
|
2
2
|
import { WarningOutlined } from '@ant-design/icons';
|
|
3
|
+
import React from 'react';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
type NamedIconProps = {
|
|
6
|
+
name: string;
|
|
7
|
+
} & React.ComponentProps<typeof WarningOutlined>;
|
|
8
|
+
|
|
9
|
+
export function NamedIcon(props: NamedIconProps): React.ReactElement {
|
|
5
10
|
const {name, ...rest} = props;
|
|
6
|
-
const IconType = Icons[name];
|
|
11
|
+
const IconType = Icons[name as keyof typeof Icons];
|
|
7
12
|
|
|
8
13
|
if (IconType) {
|
|
9
14
|
return <IconType {...rest}></IconType>;
|
|
@@ -11,4 +16,4 @@ export function NamedIcon(props) {
|
|
|
11
16
|
|
|
12
17
|
console.warn(`NamedIcon: icon "${name}" not found, using fallback`);
|
|
13
18
|
return <WarningOutlined {...rest} />;
|
|
14
|
-
}
|
|
19
|
+
}
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { Typography } from "antd";
|
|
3
3
|
import './index.less'
|
|
4
4
|
import {ThemeUtils} from "../../utils";
|
|
5
|
-
import {
|
|
5
|
+
import { PermActions } from "../../biz/PermActions";
|
|
6
6
|
|
|
7
7
|
interface PageProps {
|
|
8
8
|
padding?: boolean;
|
|
@@ -38,7 +38,7 @@ export const Page: React.FC<PageProps> = ({ padding = true, backgroundGray = fal
|
|
|
38
38
|
{description && <div><Typography.Text type="secondary">{description}</Typography.Text></div>}
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
41
|
-
{actions && <
|
|
41
|
+
{actions && <PermActions>{actions}</PermActions>}
|
|
42
42
|
</div>
|
|
43
43
|
</div>
|
|
44
44
|
)}
|
|
@@ -98,17 +98,10 @@ export class ProTable extends React.Component {
|
|
|
98
98
|
updateSelectedRows = list => {
|
|
99
99
|
const {rowKey = "id"} = this.props
|
|
100
100
|
const {selectedRows} = this.state
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const oldItem = selectedRows[i];
|
|
104
|
-
if (oldItem[rowKey] === newItem[rowKey]) {
|
|
105
|
-
selectedRows[i] = newItem;
|
|
106
|
-
break
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
101
|
+
const rowMap = new Map(list.map(item => [item[rowKey], item]))
|
|
102
|
+
const updated = selectedRows.map(old => rowMap.get(old[rowKey]) || old)
|
|
110
103
|
|
|
111
|
-
this.setState({selectedRows:
|
|
104
|
+
this.setState({selectedRows: updated})
|
|
112
105
|
};
|
|
113
106
|
|
|
114
107
|
|