Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-frontend-base into dev

This commit is contained in:
Xu Chang 2025-03-15 13:22:21 +08:00
commit 6283c03f63
38 changed files with 684 additions and 164 deletions

View File

@ -7,5 +7,6 @@ declare const _default: <ED2 extends ED, T2 extends keyof ED2>(props: ReactCompo
desc?: string | undefined;
children?: React.ReactNode;
icon?: React.ReactNode;
content?: React.ReactNode;
}>) => React.ReactElement;
export default _default;

View File

@ -30,6 +30,21 @@ const DefaultErrorInfo = {
desc: '抱歉,您正在使用的浏览器版本过低,无法打开当前网页。',
imagePath: './assets/svg/assets-result-browser-incompatible.svg',
},
[ECode.pageCrash]: {
title: '抱歉,网页崩溃了',
desc: '网页似乎遇到了问题,我们将尽快解决,请稍后重新加载。',
imagePath: './assets/svg/assets-result-404.svg',
},
[ECode.pageUpdate]: {
title: '网页有更新',
desc: '检查到网页有更新,请刷新以查看最新内容。',
imagePath: './assets/svg/assets-result-404.svg',
},
[ECode.pageDataCacheFailure]: {
title: '数据缓存失效',
desc: '非常抱歉,数据缓存已失效。请点击【清除缓存】以更新信息。',
imagePath: './assets/svg/assets-result-404.svg',
},
};
export default OakComponent({
isList: false,
@ -65,5 +80,8 @@ export default OakComponent({
goBack(delta) {
this.navigateBack(delta);
},
async clearData() {
await this.features.localStorage.clear();
}
},
});

View File

@ -9,8 +9,10 @@ interface IErrorPageProps {
desc?: string;
children?: React.ReactNode;
icon?: React.ReactNode;
content?: React.ReactNode;
}
export default function Render(props: WebComponentProps<ED, keyof ED, false, IErrorPageProps, {
goBack: (delta?: number) => void;
clearData: () => Promise<void>;
}>): React.JSX.Element;
export {};

View File

@ -39,12 +39,28 @@ const errorInfo = {
desc: '系统维护中,请稍后再试。',
icon: <LightMaintenanceIcon />,
},
[ECode.pageCrash]: {
title: '抱歉,网页崩溃了',
desc: '网页似乎遇到了问题,我们将尽快解决,请稍后重新加载。',
icon: <Light404Icon />,
},
[ECode.pageUpdate]: {
title: '网页有更新',
desc: '检查到网页有更新,请刷新以查看最新内容。',
icon: <Light404Icon />,
},
[ECode.pageDataCacheFailure]: {
title: '数据缓存失效',
desc: '非常抱歉,数据缓存已失效。请点击【清除缓存】以更新信息。',
icon: <Light404Icon />,
},
};
export default function Render(props) {
const { code, icon, title, desc, children } = props.data;
const { t, goBack } = props.methods;
const { code, icon, title, desc, children, content } = props.data;
const { t, goBack, clearData } = props.methods;
const info = errorInfo[code];
const prefixCls = 'oak';
const backed = code === ECode.notFound || code === ECode.forbidden;
return (<div className={`${prefixCls}-errorPage`}>
{icon || info?.icon}
<div className={`${prefixCls}-errorPage__title`}>
@ -53,10 +69,37 @@ export default function Render(props) {
<div className={`${prefixCls}-errorPage__description`}>
{desc || info?.desc}
</div>
{content}
{children || (<Button type="primary" onClick={() => {
goBack();
switch (code) {
case ECode.forbidden:
case ECode.notFound: {
goBack();
break;
}
case ECode.pageDataCacheFailure: {
clearData();
window.location.reload();
break;
}
case ECode.networkError:
case ECode.browserIncompatible:
case ECode.maintenance:
case ECode.pageCrash:
case ECode.pageUpdate:
default: {
window.location.reload();
break;
}
}
}}>
返回
{backed
? t('common::back', {
'#oakModule': 'oak-frontend-base',
})
: t('common::refresh', {
'#oakModule': 'oak-frontend-base',
})}
</Button>)}
</div>);
}

View File

@ -66,8 +66,6 @@ export default function Render(props) {
case 'DatePicker': {
const { dateProps } = column;
const { showTime = false } = dateProps || {};
//assert(op, '选择时间,算子必须传入');
const unitOfTime = 'day';
V = (<DatePicker placeholder={placeholder || t('placeholder.select')} style={{ width: '100%' }} format="YYYY-MM-DD" showTime={showTime} onChange={(date, dateString) => {
setFilterAndResetFilter(viewType, date);
}}/>);
@ -98,6 +96,3 @@ export default function Render(props) {
<>{V}</>
</Form.Item>);
}
function assertMessage(attr, attrType, op, ops) {
return `attr为【${attr}】, 传入的算子【${op}】不支持,类型【${attrType}】只支持【${JSON.stringify(ops)}`;
}

View File

@ -21,16 +21,17 @@ export default function render(props) {
}} options={data.map((ele) => ({
value: ele.id,
label: ele.title,
key: ele.id,
}))} multiple={multiple}></Selector>);
}
case 'radio': {
if (multiple) {
return (<Checkbox.Group value={entityIds} onChange={(value) => onChange(value)}>
{data.map((ele) => (<Checkbox value={ele.id}>{ele.title}</Checkbox>))}
{data.map((ele) => (<Checkbox key={ele.id} value={ele.id}>{ele.title}</Checkbox>))}
</Checkbox.Group>);
}
return (<Radio.Group onChange={(value) => onChange([value])} value={entityId}>
{data.map((ele) => (<Radio value={ele.id}>{ele.title}</Radio>))}
{data.map((ele) => (<Radio key={ele.id} value={ele.id}>{ele.title}</Radio>))}
</Radio.Group>);
}
case 'list': {

View File

@ -38,11 +38,13 @@ export default function render(props) {
return (<Checkbox.Group options={data.map((ele) => ({
value: ele.id,
label: ele.title,
key: ele.id,
}))} value={entityIds} onChange={(value) => onChange(value)}/>);
}
return (<Radio.Group onChange={({ target }) => onChange(target.value)} value={entityId} options={data.map((ele) => ({
value: ele.id,
label: ele.title,
key: ele.id,
}))}></Radio.Group>);
}
case 'list': {

View File

@ -765,7 +765,10 @@ class ListNode extends EntityNode {
const { data, sorter, filter } = this.constructSelection(true, false, true);
const ids2 = ids.concat(createIds);
/**
* 在非modi状态下所取数据是在ids2中满足filter的部分自身对象和其它对象的更新可能会影响这些行不再满足条件
* 在非modi状态下当前的逻辑是
* 若本结点非脏其子结点也非脏直接用ids作为查询条件(这里也隐喻着父结点不会影响这里的数据)
* 若本结点为脏则要考虑当前这些ids是否还满足filters若一个结点出现这种情况要考虑则要自己保证filter中的关联关系数据都在cache当中不然会出现数据为空
* by Xc 20250308
*/
const filter2 = inModiNextBranch ? filter : (ids2.length > 0 ? (filter && this.isDirty() ? combineFilters(this.entity, this.cache.getSchema(), [filter, {
id: {

View File

@ -18,5 +18,21 @@
"unknown": {
"title": "未知异常",
"content": "抱歉,出现未知异常,可能是数据缓存失效,需要清除缓存,请点击【清除缓存】再尝试"
},
"timeout": {
"title": "请求超时",
"content": "非常抱歉,请求超时。请稍后再试"
},
"clockDrift": {
"title": "网络请求错误",
"content": "非常抱歉,网络请求出现了问题。请稍后再试,我们正在努力修复"
},
"pageCrash": {
"title": "抱歉,网页崩溃了",
"content": "网页似乎遇到了问题,我们将尽快解决,请稍后重新加载"
},
"pageUpdate": {
"title": "网页有更新",
"content": "检查到网页有更新,请刷新以查看最新内容"
}
}

View File

@ -1,13 +1,18 @@
import React, { lazy } from 'react';
const Message = lazy(() => import('../../../components/message'));
const DebugPanel = lazy(() => import('../../../components/func/debugPanel'));
const ErrorBoundary = lazy(() => import('./ErrorBoundary'));
const AppContainer = (props) => {
const { children } = props;
return (<React.Fragment>
<React.Suspense fallback={<></>}>
<Message />
</React.Suspense>
{children}
<React.Suspense fallback={<></>}>
<ErrorBoundary>
{children}
</ErrorBoundary>
</React.Suspense>
<React.Suspense fallback={<></>}>
{process.env.NODE_ENV === 'development' && <DebugPanel />}
</React.Suspense>

View File

@ -1,6 +1,7 @@
import React, { lazy } from 'react';
import { Button } from 'antd';
import { OakException, OakNetworkException, OakServerProxyException, } from 'oak-domain/lib/types/Exception';
import { OakException, OakNetworkException, OakServerProxyException, OakClockDriftException, OakRequestTimeoutException, } from 'oak-domain/lib/types/Exception';
import ErrorMessage from './ErrorMessage';
import { ECode } from '../../../types/ErrorPage';
const ErrorPage = lazy(() => import('../../../components/errorPage'));
function Error(props) {
@ -13,8 +14,7 @@ function Error(props) {
'#oakModule': "oak-frontend-base"
})} desc={locales.t('error::network.content', {
'#oakModule': "oak-frontend-base"
})}>
<ErrorMessage error={error}/>
})} content={<ErrorMessage error={error}/>}>
<Button type="primary" onClick={async () => {
window.location.reload();
}}>
@ -30,8 +30,41 @@ function Error(props) {
'#oakModule': "oak-frontend-base"
})} desc={locales.t('error::proxy.content', {
'#oakModule': "oak-frontend-base"
})}>
<ErrorMessage error={error}/>
})} content={<ErrorMessage error={error}/>}>
<Button type="primary" onClick={async () => {
window.location.reload();
}}>
{locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
})}
</Button>
</ErrorPage>);
}
if (error instanceof OakClockDriftException) {
// 服务器加密报错
return (<React.Suspense fallback={null}>
<ErrorPage code={ECode.error} title={locales.t('error::clockDrift.title', {
'#oakModule': "oak-frontend-base"
})} desc={locales.t('error::clockDrift.content', {
'#oakModule': "oak-frontend-base"
})} content={<ErrorMessage error={error}/>}>
<Button type="primary" onClick={async () => {
window.location.reload();
}}>
{locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
})}
</Button>
</ErrorPage>
</React.Suspense>);
}
if (error instanceof OakRequestTimeoutException) {
// 服务器超时报错
return (<ErrorPage code={ECode.error} title={locales.t('error::timeout.title', {
'#oakModule': "oak-frontend-base"
})} desc={locales.t('error::timeout.content', {
'#oakModule': "oak-frontend-base"
})} content={<ErrorMessage error={error}/>}>
<Button type="primary" onClick={async () => {
window.location.reload();
}}>
@ -46,8 +79,7 @@ function Error(props) {
'#oakModule': "oak-frontend-base"
})} desc={_module ? locales.t(message, {
'#oakModule': _module
}) : message}>
<ErrorMessage error={error}/>
}) : message} content={<ErrorMessage error={error}/>}>
<Button type="primary" onClick={async () => {
window.location.reload();
}}>
@ -57,7 +89,24 @@ function Error(props) {
</Button>
</ErrorPage>);
}
return (<ErrorPage code={ECode.error} title={locales.t('error::error.title')} desc={locales.t('error::error.content')}>
if (error?.message?.includes("Failed to fetch")) {
return (<React.Suspense fallback={null}>
<ErrorPage code={ECode.error} title={locales.t('error::network.title', {
'#oakModule': "oak-frontend-base"
})} desc={locales.t('error::network.content', {
'#oakModule': "oak-frontend-base"
})} content={<ErrorMessage error={error}/>}>
<Button type="primary" onClick={async () => {
window.location.reload();
}}>
{locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
})}
</Button>
</ErrorPage>
</React.Suspense>);
}
return (<ErrorPage code={ECode.pageDataCacheFailure} title={locales.t('error::unknown.title')} desc={locales.t('error::unknown.content')} content={<ErrorMessage error={error}/>}>
<Button type="primary" onClick={async () => {
await features.localStorage.clear();
window.location.reload();
@ -66,20 +115,4 @@ function Error(props) {
</Button>
</ErrorPage>);
}
function ErrorMessage(props) {
const { error } = props;
if (process.env.NODE_ENV === 'development') {
const {} = error;
return (<span style={{
marginBottom: 24,
color: 'red',
fontSize: 14,
marginLeft: 24,
marginRight: 24,
}}>
{typeof error === 'object' ? error.message : error}
</span>);
}
return null;
}
export default Error;

View File

@ -0,0 +1,17 @@
import React from 'react';
declare class ErrorBoundary extends React.Component<{
disabled?: boolean;
children: React.ReactNode;
}> {
state: {
hasError: boolean;
error: undefined;
info: undefined;
};
static getDerivedStateFromError(error: any): {
hasError: boolean;
};
componentDidCatch(error: any, info: any): void;
render(): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined;
}
export default ErrorBoundary;

View File

@ -0,0 +1,40 @@
import React, { lazy } from 'react';
import ErrorMessage from './ErrorMessage';
import { ECode } from '../../../types/ErrorPage';
const ErrorPage = lazy(() => import('../../../components/errorPage'));
class ErrorBoundary extends React.Component {
state = {
hasError: false,
error: undefined,
info: undefined,
};
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error('ErrorBoundary caught an error:', error, info);
// 在此处可以记录错误
if (this.props.disabled) {
return;
}
this.setState({
error,
info,
});
}
render() {
if (this.props.disabled) {
return this.props.children;
}
if (this.state.hasError) {
const { error } = this.state;
// 渲染备用 UI
const message = typeof error === 'object' ? JSON.stringify(error) : error;
const isChunkLoadError = message?.includes('ChunkLoadError');
return (<ErrorPage code={isChunkLoadError ? ECode.pageUpdate : ECode.pageCrash} content={<ErrorMessage error={error}/>}>
</ErrorPage>);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1,6 @@
import React from 'react';
type IErrorMessageProps = {
error: any;
};
declare function ErrorMessage(props: IErrorMessageProps): React.JSX.Element | null;
export default ErrorMessage;

View File

@ -0,0 +1,17 @@
import React from 'react';
function ErrorMessage(props) {
const { error } = props;
if (process.env.NODE_ENV === 'development') {
return (<span style={{
marginBottom: 24,
color: 'red',
fontSize: 14,
marginLeft: 24,
marginRight: 24,
}}>
{typeof error === 'object' ? error.message : error}
</span>);
}
return null;
}
export default ErrorMessage;

View File

@ -4,5 +4,8 @@ export declare enum ECode {
error = "500",
networkError = "network-error",
browserIncompatible = "browser-incompatible",
maintenance = "maintenance"
maintenance = "maintenance",
pageCrash = "pageCrash",// 页面崩溃
pageUpdate = "pageUpdate",// 页面更新
pageDataCacheFailure = "pageDataCacheFailure"
}

View File

@ -6,4 +6,7 @@ export var ECode;
ECode["networkError"] = "network-error";
ECode["browserIncompatible"] = "browser-incompatible";
ECode["maintenance"] = "maintenance";
ECode["pageCrash"] = "pageCrash";
ECode["pageUpdate"] = "pageUpdate";
ECode["pageDataCacheFailure"] = "pageDataCacheFailure";
})(ECode || (ECode = {}));

View File

@ -768,9 +768,12 @@ class ListNode extends EntityNode {
const { data, sorter, filter } = this.constructSelection(true, false, true);
const ids2 = ids.concat(createIds);
/**
* 在非modi状态下所取数据是在ids2中满足filter的部分自身对象和其它对象的更新可能会影响这些行不再满足条件
* 在非modi状态下当前的逻辑是
* 若本结点非脏其子结点也非脏直接用ids作为查询条件(这里也隐喻着父结点不会影响这里的数据)
* 若本结点为脏则要考虑当前这些ids是否还满足filters若一个结点出现这种情况要考虑则要自己保证filter中的关联关系数据都在cache当中不然会出现数据为空
* by Xc 20250308
*/
const filter2 = inModiNextBranch ? filter : (ids2.length > 0 ? (filter ? (0, filter_1.combineFilters)(this.entity, this.cache.getSchema(), [filter, {
const filter2 = inModiNextBranch ? filter : (ids2.length > 0 ? (filter && this.isDirty() ? (0, filter_1.combineFilters)(this.entity, this.cache.getSchema(), [filter, {
id: {
$in: ids2,
}

View File

@ -5,8 +5,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = tslib_1.__importStar(require("react"));
const Message = (0, react_1.lazy)(() => Promise.resolve().then(() => tslib_1.__importStar(require('../../../components/message'))));
const DebugPanel = (0, react_1.lazy)(() => Promise.resolve().then(() => tslib_1.__importStar(require('../../../components/func/debugPanel'))));
const ErrorBoundary = (0, react_1.lazy)(() => Promise.resolve().then(() => tslib_1.__importStar(require('./ErrorBoundary'))));
const AppContainer = (props) => {
const { children } = props;
return ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}), children: (0, jsx_runtime_1.jsx)(Message, {}) }), children, (0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}), children: process.env.NODE_ENV === 'development' && (0, jsx_runtime_1.jsx)(DebugPanel, {}) })] }));
return ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}), children: (0, jsx_runtime_1.jsx)(Message, {}) }), (0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}), children: (0, jsx_runtime_1.jsx)(ErrorBoundary, { children: children }) }), (0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}), children: process.env.NODE_ENV === 'development' && (0, jsx_runtime_1.jsx)(DebugPanel, {}) })] }));
};
exports.default = AppContainer;

View File

@ -1,90 +1,90 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const react_1 = tslib_1.__importStar(require("react"));
const antd_1 = require("antd");
const Exception_1 = require("oak-domain/lib/types/Exception");
const ErrorMessage_1 = tslib_1.__importDefault(require("./ErrorMessage"));
const ErrorPage_1 = require("../../../types/ErrorPage");
const ErrorPage = (0, react_1.lazy)(() => Promise.resolve().then(() => __importStar(require('../../../components/errorPage'))));
const ErrorPage = (0, react_1.lazy)(() => Promise.resolve().then(() => tslib_1.__importStar(require('../../../components/errorPage'))));
function Error(props) {
const { error, features } = props;
const { locales } = features;
if (error instanceof Exception_1.OakException) {
if (error instanceof Exception_1.OakNetworkException) {
// 网络中断出现的异常
return ((0, jsx_runtime_1.jsxs)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::network.title', {
return ((0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::network.title', {
'#oakModule': "oak-frontend-base"
}), desc: locales.t('error::network.content', {
'#oakModule': "oak-frontend-base"
}), children: [(0, jsx_runtime_1.jsx)(ErrorMessage, { error: error }), (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) })] }));
}
if (error instanceof Exception_1.OakServerProxyException) {
// 服务器代理异常
return ((0, jsx_runtime_1.jsxs)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::proxy.title', {
'#oakModule': "oak-frontend-base"
}), desc: locales.t('error::proxy.content', {
'#oakModule': "oak-frontend-base"
}), children: [(0, jsx_runtime_1.jsx)(ErrorMessage, { error: error }), (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) })] }));
}
const { _module, message } = error;
return ((0, jsx_runtime_1.jsxs)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::error.title', {
'#oakModule': "oak-frontend-base"
}), desc: _module ? locales.t(message, {
'#oakModule': _module
}) : message, children: [(0, jsx_runtime_1.jsx)(ErrorMessage, { error: error }), (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
}), content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) })] }));
}) }) }));
}
if (error instanceof Exception_1.OakServerProxyException) {
// 服务器代理异常
return ((0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::proxy.title', {
'#oakModule': "oak-frontend-base"
}), desc: locales.t('error::proxy.content', {
'#oakModule': "oak-frontend-base"
}), content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) }) }));
}
if (error instanceof Exception_1.OakClockDriftException) {
// 服务器加密报错
return ((0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: null, children: (0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::clockDrift.title', {
'#oakModule': "oak-frontend-base"
}), desc: locales.t('error::clockDrift.content', {
'#oakModule': "oak-frontend-base"
}), content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) }) }) }));
}
if (error instanceof Exception_1.OakRequestTimeoutException) {
// 服务器超时报错
return ((0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::timeout.title', {
'#oakModule': "oak-frontend-base"
}), desc: locales.t('error::timeout.content', {
'#oakModule': "oak-frontend-base"
}), content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) }) }));
}
const { _module, message } = error;
return ((0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::error.title', {
'#oakModule': "oak-frontend-base"
}), desc: _module ? locales.t(message, {
'#oakModule': _module
}) : message, content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) }) }));
}
return ((0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::error.title'), desc: locales.t('error::error.content'), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
if (error?.message?.includes("Failed to fetch")) {
return ((0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: null, children: (0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.error, title: locales.t('error::network.title', {
'#oakModule': "oak-frontend-base"
}), desc: locales.t('error::network.content', {
'#oakModule': "oak-frontend-base"
}), content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
window.location.reload();
}, children: locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
}) }) }) }));
}
return ((0, jsx_runtime_1.jsx)(ErrorPage, { code: ErrorPage_1.ECode.pageDataCacheFailure, title: locales.t('error::unknown.title'), desc: locales.t('error::unknown.content'), content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }), children: (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", onClick: async () => {
await features.localStorage.clear();
window.location.reload();
}, children: locales.t('clearCache') }) }));
}
function ErrorMessage(props) {
const { error } = props;
if (process.env.NODE_ENV === 'development') {
const {} = error;
return ((0, jsx_runtime_1.jsx)("span", { style: {
marginBottom: 24,
color: 'red',
fontSize: 14,
marginLeft: 24,
marginRight: 24,
}, children: typeof error === 'object' ? error.message : error }));
}
return null;
}
exports.default = Error;

View File

@ -0,0 +1,17 @@
import React from 'react';
declare class ErrorBoundary extends React.Component<{
disabled?: boolean;
children: React.ReactNode;
}> {
state: {
hasError: boolean;
error: undefined;
info: undefined;
};
static getDerivedStateFromError(error: any): {
hasError: boolean;
};
componentDidCatch(error: any, info: any): void;
render(): string | number | boolean | Iterable<React.ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
}
export default ErrorBoundary;

View File

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = tslib_1.__importStar(require("react"));
const ErrorMessage_1 = tslib_1.__importDefault(require("./ErrorMessage"));
const ErrorPage_1 = require("../../../types/ErrorPage");
const ErrorPage = (0, react_1.lazy)(() => Promise.resolve().then(() => tslib_1.__importStar(require('../../../components/errorPage'))));
class ErrorBoundary extends react_1.default.Component {
state = {
hasError: false,
error: undefined,
info: undefined,
};
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error('ErrorBoundary caught an error:', error, info);
// 在此处可以记录错误
if (this.props.disabled) {
return;
}
this.setState({
error,
info,
});
}
render() {
if (this.props.disabled) {
return this.props.children;
}
if (this.state.hasError) {
const { error } = this.state;
// 渲染备用 UI
const message = typeof error === 'object' ? JSON.stringify(error) : error;
const isChunkLoadError = message?.includes('ChunkLoadError');
return ((0, jsx_runtime_1.jsx)(ErrorPage, { code: isChunkLoadError ? ErrorPage_1.ECode.pageUpdate : ErrorPage_1.ECode.pageCrash, content: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error }) }));
}
return this.props.children;
}
}
exports.default = ErrorBoundary;

View File

@ -0,0 +1,5 @@
type IErrorMessageProps = {
error: any;
};
declare function ErrorMessage(props: IErrorMessageProps): import("react/jsx-runtime").JSX.Element | null;
export default ErrorMessage;

View File

@ -0,0 +1,17 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_runtime_1 = require("react/jsx-runtime");
function ErrorMessage(props) {
const { error } = props;
if (process.env.NODE_ENV === 'development') {
return ((0, jsx_runtime_1.jsx)("span", { style: {
marginBottom: 24,
color: 'red',
fontSize: 14,
marginLeft: 24,
marginRight: 24,
}, children: typeof error === 'object' ? error.message : error }));
}
return null;
}
exports.default = ErrorMessage;

View File

@ -4,5 +4,8 @@ export declare enum ECode {
error = "500",
networkError = "network-error",
browserIncompatible = "browser-incompatible",
maintenance = "maintenance"
maintenance = "maintenance",
pageCrash = "pageCrash",// 页面崩溃
pageUpdate = "pageUpdate",// 页面更新
pageDataCacheFailure = "pageDataCacheFailure"
}

View File

@ -9,4 +9,7 @@ var ECode;
ECode["networkError"] = "network-error";
ECode["browserIncompatible"] = "browser-incompatible";
ECode["maintenance"] = "maintenance";
ECode["pageCrash"] = "pageCrash";
ECode["pageUpdate"] = "pageUpdate";
ECode["pageDataCacheFailure"] = "pageDataCacheFailure";
})(ECode || (exports.ECode = ECode = {}));

View File

@ -35,6 +35,21 @@ const DefaultErrorInfo = {
desc: '抱歉,您正在使用的浏览器版本过低,无法打开当前网页。',
imagePath: './assets/svg/assets-result-browser-incompatible.svg',
},
[ECode.pageCrash]: {
title: '抱歉,网页崩溃了',
desc: '网页似乎遇到了问题,我们将尽快解决,请稍后重新加载。',
imagePath: './assets/svg/assets-result-404.svg',
},
[ECode.pageUpdate]: {
title: '网页有更新',
desc: '检查到网页有更新,请刷新以查看最新内容。',
imagePath: './assets/svg/assets-result-404.svg',
},
[ECode.pageDataCacheFailure]: {
title: '数据缓存失效',
desc: '非常抱歉,数据缓存已失效。请点击【清除缓存】以更新信息。',
imagePath: './assets/svg/assets-result-404.svg',
},
};
export default OakComponent({
@ -72,6 +87,9 @@ export default OakComponent({
goBack(delta?: number) {
this.navigateBack(delta);
},
async clearData() {
await this.features.localStorage.clear();
}
},
}) as <ED2 extends ED, T2 extends keyof ED2>(
props: ReactComponentProps<
@ -84,6 +102,7 @@ export default OakComponent({
desc?: string;
children?: React.ReactNode;
icon?: React.ReactNode;
content?: React.ReactNode;
}
>
) => React.ReactElement;

View File

@ -19,6 +19,7 @@ interface IErrorPageProps {
desc?: string;
children?: React.ReactNode;
icon?: React.ReactNode;
content?: React.ReactNode;
}
const errorInfo = {
@ -52,6 +53,21 @@ const errorInfo = {
desc: '系统维护中,请稍后再试。',
icon: <LightMaintenanceIcon />,
},
[ECode.pageCrash]: {
title: '抱歉,网页崩溃了',
desc: '网页似乎遇到了问题,我们将尽快解决,请稍后重新加载。',
icon: <Light404Icon />,
},
[ECode.pageUpdate]: {
title: '网页有更新',
desc: '检查到网页有更新,请刷新以查看最新内容。',
icon: <Light404Icon />,
},
[ECode.pageDataCacheFailure]: {
title: '数据缓存失效',
desc: '非常抱歉,数据缓存已失效。请点击【清除缓存】以更新信息。',
icon: <Light404Icon />,
},
};
export default function Render(
@ -62,13 +78,16 @@ export default function Render(
IErrorPageProps,
{
goBack: (delta?: number) => void;
clearData: () => Promise<void>;
}
>
) {
const { code, icon, title, desc, children } = props.data;
const { t, goBack } = props.methods;
const { code, icon, title, desc, children, content } = props.data;
const { t, goBack, clearData } = props.methods;
const info = errorInfo[code];
const prefixCls = 'oak';
const backed = code === ECode.notFound || code === ECode.forbidden;
return (
<div className={`${prefixCls}-errorPage`}>
@ -79,14 +98,41 @@ export default function Render(
<div className={`${prefixCls}-errorPage__description`}>
{desc || info?.desc}
</div>
{content}
{children || (
<Button
type="primary"
onClick={() => {
goBack();
switch (code) {
case ECode.forbidden:
case ECode.notFound: {
goBack();
break;
}
case ECode.pageDataCacheFailure: {
clearData();
window.location.reload();
break;
}
case ECode.networkError:
case ECode.browserIncompatible:
case ECode.maintenance:
case ECode.pageCrash:
case ECode.pageUpdate:
default: {
window.location.reload();
break;
}
}
}}
>
{backed
? t('common::back', {
'#oakModule': 'oak-frontend-base',
})
: t('common::refresh', {
'#oakModule': 'oak-frontend-base',
})}
</Button>
)}
</div>

View File

@ -107,11 +107,7 @@ export default function Render<ED2 extends ED>(
label:
typeof ele.value === 'boolean'
? t(`${ele.value ? 'tip.yes' : 'tip.no'}`)
: t(
`${entityI18n as string}:v.${attrI18n}.${
ele.value
}`
),
: t(`${entityI18n as string}:v.${attrI18n}.${ele.value}`),
value: ele.value,
}));
const multiple = ['$in', '$nin'].includes(op || '');
@ -139,8 +135,6 @@ export default function Render<ED2 extends ED>(
case 'DatePicker': {
const { dateProps } = column;
const { showTime = false } = dateProps || {};
//assert(op, '选择时间,算子必须传入');
const unitOfTime = 'day';
V = (
<DatePicker
placeholder={placeholder || t('placeholder.select')}
@ -200,9 +194,3 @@ export default function Render<ED2 extends ED>(
</Form.Item>
);
}
function assertMessage(attr: string, attrType: string, op: Ops, ops: Ops[]) {
return `attr为【${attr}】, 传入的算子【${op}】不支持,类型【${attrType}】只支持【${JSON.stringify(
ops
)}`;
}

View File

@ -17,8 +17,7 @@ export default OakComponent({
},
formData() {
const { multiple, entityIds, pickerRender } = this.props;
const { entity, projection, title } =
pickerRender as OakAbsRefAttrPickerRender<ED, keyof ED>;
const { entity, projection, title } = pickerRender as OakAbsRefAttrPickerRender<ED, keyof ED>;
const rows =
entityIds &&
entityIds.length &&
@ -33,8 +32,7 @@ export default OakComponent({
},
},
});
const renderValue =
rows && rows.length ? rows.map((row) => title(row)).join(',') : '';
const renderValue = rows && rows.length ? rows.map((row) => title(row)).join(',') : '';
const schema = this.features.cache.getSchema();
return {
renderValue,
@ -60,8 +58,7 @@ export default OakComponent({
methods: {
async refreshData() {
const { pickerRender, multiple } = this.props;
const { mode, entity, projection, filter, count, title } =
pickerRender as OakAbsRefAttrPickerDef<ED, keyof ED>;
const { mode, entity, projection, filter, count, title } = pickerRender as OakAbsRefAttrPickerDef<ED, keyof ED>;
if (mode === 'radio') {
// radio的要先取数据出来
assert(
@ -78,8 +75,7 @@ export default OakComponent({
return;
}
const proj =
typeof projection === 'function' ? projection() : projection;
const proj = typeof projection === 'function' ? projection() : projection;
const filter2 = typeof filter === 'function' ? filter() : filter;
const { data } = await this.features.cache.refresh(entity, {
data: proj,

View File

@ -89,6 +89,7 @@ export default function render(
options={data!.map((ele) => ({
value: ele.id,
label: ele.title,
key: ele.id,
}))}
value={entityIds}
onChange={(value) => onChange(value as string[])}
@ -102,6 +103,7 @@ export default function render(
options={data!.map((ele) => ({
value: ele.id,
label: ele.title,
key: ele.id,
}))}
></Radio.Group>
);

View File

@ -67,6 +67,7 @@ export default function render(
options={data!.map((ele) => ({
value: ele.id,
label: ele.title,
key: ele.id,
}))}
multiple={multiple}
></Selector>
@ -80,7 +81,7 @@ export default function render(
onChange={(value) => onChange(value as string[])}
>
{data!.map((ele) => (
<Checkbox value={ele.id}>{ele.title}</Checkbox>
<Checkbox key={ele.id} value={ele.id}>{ele.title}</Checkbox>
))}
</Checkbox.Group>
);
@ -91,7 +92,7 @@ export default function render(
value={entityId}
>
{data!.map((ele) => (
<Radio value={ele.id}>{ele.title}</Radio>
<Radio key={ele.id} value={ele.id}>{ele.title}</Radio>
))}
</Radio.Group>
);

View File

@ -18,5 +18,21 @@
"unknown": {
"title": "未知异常",
"content": "抱歉,出现未知异常,可能是数据缓存失效,需要清除缓存,请点击【清除缓存】再尝试"
},
"timeout": {
"title": "请求超时",
"content": "非常抱歉,请求超时。请稍后再试"
},
"clockDrift": {
"title": "网络请求错误",
"content": "非常抱歉,网络请求出现了问题。请稍后再试,我们正在努力修复"
},
"pageCrash": {
"title": "抱歉,网页崩溃了",
"content": "网页似乎遇到了问题,我们将尽快解决,请稍后重新加载"
},
"pageUpdate": {
"title": "网页有更新",
"content": "检查到网页有更新,请刷新以查看最新内容"
}
}

View File

@ -1,6 +1,7 @@
import React, { lazy } from 'react';
const Message = lazy(() => import('../../../components/message'));
const DebugPanel = lazy(() => import('../../../components/func/debugPanel'));
const ErrorBoundary = lazy(() => import('./ErrorBoundary'));
type AppContainerProps = {
children?: React.ReactNode
@ -13,7 +14,11 @@ const AppContainer = (props: AppContainerProps) => {
<React.Suspense fallback={<></>}>
<Message />
</React.Suspense>
{children}
<React.Suspense fallback={<></>}>
<ErrorBoundary>
{children}
</ErrorBoundary>
</React.Suspense>
<React.Suspense fallback={<></>}>
{process.env.NODE_ENV === 'development' && <DebugPanel />}
</React.Suspense>

View File

@ -5,10 +5,12 @@ import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import {
OakException,
OakExternalException,
OakNetworkException,
OakServerProxyException,
OakClockDriftException,
OakRequestTimeoutException,
} from 'oak-domain/lib/types/Exception';
import ErrorMessage from './ErrorMessage';
import { ECode } from '../../../types/ErrorPage';
import { BasicFeatures } from '../../../features';
const ErrorPage = lazy(() => import('../../../components/errorPage'));
@ -34,8 +36,8 @@ function Error<ED extends EntityDict & BaseEntityDict>(props: ErrorProps<ED>) {
desc={locales.t('error::network.content', {
'#oakModule': "oak-frontend-base"
})}
content={<ErrorMessage error={error} />}
>
<ErrorMessage error={error} />
<Button
type="primary"
onClick={async () => {
@ -60,8 +62,64 @@ function Error<ED extends EntityDict & BaseEntityDict>(props: ErrorProps<ED>) {
desc={locales.t('error::proxy.content', {
'#oakModule': "oak-frontend-base"
})}
content={<ErrorMessage error={error} />}
>
<Button
type="primary"
onClick={async () => {
window.location.reload();
}}
>
{locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
})}
</Button>
</ErrorPage>
);
}
if (error instanceof OakClockDriftException) {
// 服务器加密报错
return (
<React.Suspense fallback={null}>
<ErrorPage
code={ECode.error}
title={locales.t('error::clockDrift.title', {
'#oakModule': "oak-frontend-base"
})}
desc={locales.t('error::clockDrift.content', {
'#oakModule': "oak-frontend-base"
})}
content={<ErrorMessage error={error} />}
>
<Button
type="primary"
onClick={async () => {
window.location.reload();
}}
>
{locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
})}
</Button>
</ErrorPage>
</React.Suspense>
);
}
if (error instanceof OakRequestTimeoutException) {
// 服务器超时报错
return (
<ErrorPage
code={ECode.error}
title={locales.t('error::timeout.title', {
'#oakModule': "oak-frontend-base"
})}
desc={locales.t('error::timeout.content', {
'#oakModule': "oak-frontend-base"
})}
content={<ErrorMessage error={error} />}
>
<ErrorMessage error={error} />
<Button
type="primary"
onClick={async () => {
@ -86,8 +144,8 @@ function Error<ED extends EntityDict & BaseEntityDict>(props: ErrorProps<ED>) {
desc={_module ? locales.t(message, {
'#oakModule': _module
}) : message}
content={<ErrorMessage error={error} />}
>
<ErrorMessage error={error} />
<Button
type="primary"
onClick={async () => {
@ -102,11 +160,40 @@ function Error<ED extends EntityDict & BaseEntityDict>(props: ErrorProps<ED>) {
);
}
if (error?.message?.includes("Failed to fetch")) {
return (
<React.Suspense fallback={null}>
<ErrorPage
code={ECode.error}
title={locales.t('error::network.title', {
'#oakModule': "oak-frontend-base"
})}
desc={locales.t('error::network.content', {
'#oakModule': "oak-frontend-base"
})}
content={<ErrorMessage error={error} />}
>
<Button
type="primary"
onClick={async () => {
window.location.reload();
}}
>
{locales.t('common::refresh', {
'#oakModule': "oak-frontend-base"
})}
</Button>
</ErrorPage>
</React.Suspense>
);
}
return (
<ErrorPage
code={ECode.error}
title={locales.t('error::error.title')}
desc={locales.t('error::error.content')}
code={ECode.pageDataCacheFailure}
title={locales.t('error::unknown.title')}
desc={locales.t('error::unknown.content')}
content={<ErrorMessage error={error} />}
>
<Button
type="primary"
@ -121,25 +208,4 @@ function Error<ED extends EntityDict & BaseEntityDict>(props: ErrorProps<ED>) {
);
}
function ErrorMessage(props: { error: any }) {
const { error } = props;
if (process.env.NODE_ENV === 'development') {
const {} = error;
return (
<span
style={{
marginBottom: 24,
color: 'red',
fontSize: 14,
marginLeft: 24,
marginRight: 24,
}}
>
{typeof error === 'object' ? error.message : error}
</span>
);
}
return null;
}
export default Error;

View File

@ -0,0 +1,52 @@
import React, { lazy } from 'react';
import ErrorMessage from './ErrorMessage';
import { ECode } from '../../../types/ErrorPage';
const ErrorPage = lazy(() => import('../../../components/errorPage'));
class ErrorBoundary extends React.Component<{ disabled?: boolean; children: React.ReactNode }> {
state = {
hasError: false,
error: undefined,
info: undefined,
};
static getDerivedStateFromError(error: any) {
return { hasError: true };
}
componentDidCatch(error: any, info: any) {
console.error('ErrorBoundary caught an error:', error, info);
// 在此处可以记录错误
if (this.props.disabled) {
return;
}
this.setState({
error,
info,
});
}
render() {
if (this.props.disabled) {
return this.props.children;
}
if (this.state.hasError) {
const { error } = this.state;
// 渲染备用 UI
const message = typeof error === 'object' ? JSON.stringify(error) : error;
const isChunkLoadError = (message as string)?.includes('ChunkLoadError');
return (
<ErrorPage
code={isChunkLoadError ? ECode.pageUpdate : ECode.pageCrash}
content={<ErrorMessage error={error} />}
>
</ErrorPage>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1,28 @@
import React from 'react';
type IErrorMessageProps = {
error: any;
}
function ErrorMessage(props: IErrorMessageProps) {
const { error } = props;
if (process.env.NODE_ENV === 'development') {
return (
<span
style={{
marginBottom: 24,
color: 'red',
fontSize: 14,
marginLeft: 24,
marginRight: 24,
}}
>
{typeof error === 'object' ? error.message : error}
</span>
);
}
return null;
}
export default ErrorMessage;

View File

@ -8,4 +8,8 @@ export enum ECode {
networkError = 'network-error',
browserIncompatible = 'browser-incompatible',
maintenance = 'maintenance',
pageCrash = 'pageCrash', // 页面崩溃
pageUpdate = 'pageUpdate', // 页面更新
pageDataCacheFailure = 'pageDataCacheFailure', // 页面数据缓存失效
}