mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 15:41:32 +08:00
feat: add error handle
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
const { on } = useLockPage();
|
||||
lockOn = on;
|
||||
}
|
||||
|
||||
return {
|
||||
transformCellText,
|
||||
zhCN,
|
||||
|
16
src/api/demo/error.ts
Normal file
16
src/api/demo/error.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
// 该地址不存在
|
||||
Error = '/error',
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 触发ajax错误
|
||||
*/
|
||||
export function fireErrorApi() {
|
||||
return defHttp.request({
|
||||
url: Api.Error,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
@@ -146,7 +146,7 @@
|
||||
}
|
||||
if (showSummary) {
|
||||
propsData.footer = renderFooter.bind(null, {
|
||||
scroll,
|
||||
scroll: scroll as any,
|
||||
columnsRef: getColumnsRef,
|
||||
summaryFunc: unref(getMergeProps).summaryFunc,
|
||||
dataSourceRef: getDataSourceRef,
|
||||
|
@@ -29,7 +29,6 @@ export default defineComponent({
|
||||
const {
|
||||
disabled = false,
|
||||
label,
|
||||
props,
|
||||
icon,
|
||||
color = '',
|
||||
type = 'link',
|
||||
@@ -41,7 +40,7 @@ export default defineComponent({
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
color={color}
|
||||
{...props}
|
||||
{...action}
|
||||
key={index}
|
||||
>
|
||||
{() => (
|
||||
@@ -101,7 +100,6 @@ export default defineComponent({
|
||||
const {
|
||||
disabled = false,
|
||||
label,
|
||||
props,
|
||||
icon,
|
||||
color = '',
|
||||
type = 'link',
|
||||
@@ -112,7 +110,7 @@ export default defineComponent({
|
||||
<Button
|
||||
type={type}
|
||||
size="small"
|
||||
{...props}
|
||||
{...action}
|
||||
disabled={disabled}
|
||||
color={color}
|
||||
>
|
||||
|
@@ -23,3 +23,11 @@ export enum ExceptionEnum {
|
||||
// No data on the page. In fact, it is not an exception page
|
||||
PAGE_NOT_DATA = 10400,
|
||||
}
|
||||
|
||||
export enum ErrorTypeEnum {
|
||||
VUE = 'vue',
|
||||
SCRIPT = 'script',
|
||||
RESOURCE = 'resource',
|
||||
AJAX = 'ajax',
|
||||
PROMISE = 'promise',
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ function handleError(e: Error) {
|
||||
// page switch
|
||||
export function useGo() {
|
||||
const { push, replace } = useRouter();
|
||||
function go(opt: PageEnum | RouteLocationRawEx = PageEnum.BASE_HOME, isReplace = false) {
|
||||
function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) {
|
||||
if (isString(opt)) {
|
||||
isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError);
|
||||
} else {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { defineComponent, unref, computed } from 'vue';
|
||||
import { Layout, Tooltip } from 'ant-design-vue';
|
||||
import { Layout, Tooltip, Badge } from 'ant-design-vue';
|
||||
import Logo from '/@/layouts/Logo.vue';
|
||||
import UserDropdown from './UserDropdown';
|
||||
import LayoutMenu from './LayoutMenu';
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
FullscreenOutlined,
|
||||
GithubFilled,
|
||||
LockOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useFullscreen } from '/@/hooks/web/useFullScreen';
|
||||
import { useTabs } from '/@/hooks/web/useTabs';
|
||||
import { GITHUB_URL } from '/@/settings/siteSetting';
|
||||
import LockAction from './actions/LockActionItem';
|
||||
import { useModal } from '/@/components/Modal/index';
|
||||
import { errorStore } from '/@/store/modules/error';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DefaultLayoutHeader',
|
||||
@@ -25,6 +28,7 @@ export default defineComponent({
|
||||
const { refreshPage } = useTabs();
|
||||
const [register, { openModal }] = useModal();
|
||||
const { toggleFullscreen, isFullscreenRef } = useFullscreen();
|
||||
const go = useGo();
|
||||
const getProjectConfigRef = computed(() => {
|
||||
return appStore.getProjectConfig;
|
||||
});
|
||||
@@ -37,6 +41,12 @@ export default defineComponent({
|
||||
const theme = unref(getProjectConfigRef).headerSetting.theme;
|
||||
return theme ? `layout-header__header--${theme}` : '';
|
||||
});
|
||||
|
||||
function handleToErrorList() {
|
||||
errorStore.commitErrorListCountState(0);
|
||||
go('/exception/error-log');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 锁定屏幕
|
||||
*/
|
||||
@@ -46,9 +56,9 @@ export default defineComponent({
|
||||
return () => {
|
||||
const getProjectConfig = unref(getProjectConfigRef);
|
||||
const {
|
||||
// useErrorHandle,
|
||||
useErrorHandle,
|
||||
showLogo,
|
||||
headerSetting: { theme: headerTheme, showRedo, showGithub, showFullScreen },
|
||||
headerSetting: { theme: headerTheme, useLockPage, showRedo, showGithub, showFullScreen },
|
||||
menuSetting: { mode, type: menuType, split: splitMenu, topMenuAlign },
|
||||
showBreadCrumb,
|
||||
} = getProjectConfig;
|
||||
@@ -77,8 +87,28 @@ export default defineComponent({
|
||||
</div>
|
||||
|
||||
<div class={`layout-header__action`}>
|
||||
{useErrorHandle && (
|
||||
<Tooltip>
|
||||
{{
|
||||
title: () => '错误日志',
|
||||
default: () => (
|
||||
<Badge
|
||||
count={errorStore.getErrorListCountState}
|
||||
offset={[0, 10]}
|
||||
overflowCount={99}
|
||||
>
|
||||
{() => (
|
||||
<div class={`layout-header__action-item`} onClick={handleToErrorList}>
|
||||
<BugOutlined class={`layout-header__action-icon`} />
|
||||
</div>
|
||||
)}
|
||||
</Badge>
|
||||
),
|
||||
}}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showGithub && (
|
||||
// @ts-ignore
|
||||
<Tooltip>
|
||||
{{
|
||||
title: () => 'github',
|
||||
@@ -90,8 +120,7 @@ export default defineComponent({
|
||||
}}
|
||||
</Tooltip>
|
||||
)}
|
||||
{showGithub && (
|
||||
// @ts-ignore
|
||||
{useLockPage && (
|
||||
<Tooltip>
|
||||
{{
|
||||
title: () => '锁定屏幕',
|
||||
@@ -104,7 +133,6 @@ export default defineComponent({
|
||||
</Tooltip>
|
||||
)}
|
||||
{showRedo && (
|
||||
// @ts-ignore
|
||||
<Tooltip>
|
||||
{{
|
||||
title: () => '刷新',
|
||||
@@ -117,7 +145,6 @@ export default defineComponent({
|
||||
</Tooltip>
|
||||
)}
|
||||
{showFullScreen && (
|
||||
// @ts-ignore
|
||||
<Tooltip>
|
||||
{{
|
||||
title: () => (unref(isFullscreenRef) ? '退出全屏' : '全屏'),
|
||||
|
@@ -40,10 +40,11 @@ export default defineComponent({
|
||||
let password: string | undefined = '';
|
||||
|
||||
try {
|
||||
const values = (await validateFields()) as any;
|
||||
password = values.password;
|
||||
if (!valid) {
|
||||
password = undefined;
|
||||
} else {
|
||||
const values = (await validateFields()) as any;
|
||||
password = values.password;
|
||||
}
|
||||
setModalProps({
|
||||
visible: false,
|
||||
|
68
src/layouts/default/actions/notice/NoticeActionItem.tsx
Normal file
68
src/layouts/default/actions/notice/NoticeActionItem.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { Popover, Tabs } from 'ant-design-vue';
|
||||
|
||||
import NoticeList from './NoticeList';
|
||||
import { NoticeTabItem, NoticeListItem, noticeTabListData, noticeListData } from './data';
|
||||
import './index.less';
|
||||
|
||||
const prefixCls = 'notice-popover';
|
||||
export default defineComponent({
|
||||
name: 'NoticePopover',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
// 渲染卡片内容
|
||||
function renderContent() {
|
||||
return (
|
||||
<Tabs class={`${prefixCls}__tabs`}>
|
||||
{() => {
|
||||
return noticeTabListData.map((item: NoticeTabItem) => {
|
||||
const { key, name } = item;
|
||||
return (
|
||||
<Tabs.TabPane key={key} tab={renderTab(key, name)}>
|
||||
{() => <NoticeList list={getListData(key)} />}
|
||||
</Tabs.TabPane>
|
||||
);
|
||||
});
|
||||
}}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
// tab标题渲染
|
||||
function renderTab(key: string, name: string) {
|
||||
const list = getListData(key);
|
||||
const unreadlist = list.filter((item: NoticeListItem) => !item.read);
|
||||
return (
|
||||
<div>
|
||||
{name}
|
||||
{unreadlist.length > 0 && <span>({unreadlist.length})</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
function getListData(type: string) {
|
||||
return noticeListData.filter((item: NoticeListItem) => item.type === type);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const { visible } = props;
|
||||
return (
|
||||
<Popover
|
||||
title=""
|
||||
{...{
|
||||
...attrs,
|
||||
visible,
|
||||
}}
|
||||
content={renderContent}
|
||||
class={prefixCls}
|
||||
/>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
73
src/layouts/default/actions/notice/NoticeList.tsx
Normal file
73
src/layouts/default/actions/notice/NoticeList.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { List, Avatar, Tag } from 'ant-design-vue';
|
||||
|
||||
import { NoticeListItem } from './data';
|
||||
import './index.less';
|
||||
|
||||
const prefixCls = 'notice-popover';
|
||||
export default defineComponent({
|
||||
name: 'NoticeList',
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 头像渲染
|
||||
function renderAvatar(avatar: string) {
|
||||
return avatar ? <Avatar class="avatar" src={avatar} /> : <span>{avatar}</span>;
|
||||
}
|
||||
|
||||
// 描述渲染
|
||||
function renderDescription(description: string, datetime: string) {
|
||||
return (
|
||||
<div>
|
||||
<div class="description">{description}</div>
|
||||
<div class="datetime">{datetime}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 标题渲染
|
||||
function renderTitle(title: string, extra?: string, color?: string) {
|
||||
return (
|
||||
<div class="title">
|
||||
{title}
|
||||
{extra && (
|
||||
<div class="extra">
|
||||
<Tag class="tag" color={color}>
|
||||
{() => extra}
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const { list } = props;
|
||||
return (
|
||||
<List dataSource={list} class={`${prefixCls}__list`}>
|
||||
{() => {
|
||||
return list.map((item: NoticeListItem) => {
|
||||
const { id, avatar, title, description, datetime, extra, read, color } = item;
|
||||
return (
|
||||
<List.Item key={id} class={`${prefixCls}__list-item ${read ? 'read' : ''}`}>
|
||||
{() => (
|
||||
<List.Item.Meta
|
||||
class="meta"
|
||||
avatar={renderAvatar(avatar)}
|
||||
title={renderTitle(title, extra, color)}
|
||||
description={renderDescription(description, datetime)}
|
||||
/>
|
||||
)}
|
||||
</List.Item>
|
||||
);
|
||||
});
|
||||
}}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
@@ -102,12 +102,12 @@
|
||||
.setting-button {
|
||||
top: 45%;
|
||||
right: 0;
|
||||
padding: 14px;
|
||||
padding: 8px;
|
||||
border-radius: 6px 0 0 6px;
|
||||
|
||||
svg {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -30,7 +30,7 @@
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
// padding: 10px;
|
||||
color: @white;
|
||||
cursor: pointer;
|
||||
background: @primary-color;
|
||||
|
@@ -54,6 +54,7 @@ export default defineComponent({
|
||||
{...on}
|
||||
name={name || route.meta.transitionName || routerTransition}
|
||||
mode="out-in"
|
||||
appear={true}
|
||||
>
|
||||
{() => Content}
|
||||
</Transition>
|
||||
|
@@ -3,6 +3,7 @@ import { createApp } from 'vue';
|
||||
import router, { setupRouter } from '/@/router';
|
||||
import { setupStore } from '/@/store';
|
||||
import { setupAntd } from '/@/setup/ant-design-vue';
|
||||
import { setupErrorHandle } from '/@/setup/error-handle/index';
|
||||
import { setupDirectives } from '/@/setup/directives/index';
|
||||
|
||||
import { registerGlobComp } from '/@/components/registerGlobComp';
|
||||
@@ -21,10 +22,12 @@ setupRouter(app);
|
||||
// store
|
||||
setupStore(app);
|
||||
|
||||
registerGlobComp(app);
|
||||
|
||||
setupDirectives(app);
|
||||
|
||||
setupErrorHandle(app);
|
||||
|
||||
registerGlobComp(app);
|
||||
|
||||
router.isReady().then(() => {
|
||||
app.mount('#app');
|
||||
});
|
||||
|
@@ -25,6 +25,10 @@ const menu: MenuModule = {
|
||||
path: '/not-data',
|
||||
name: '无数据',
|
||||
},
|
||||
{
|
||||
path: '/error-log',
|
||||
name: '错误日志',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@@ -78,5 +78,13 @@ export default {
|
||||
afterCloseLoading: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/error-log',
|
||||
name: 'ErrorLog',
|
||||
component: () => import('/@/views/sys/error-log/index.vue'),
|
||||
meta: {
|
||||
title: '错误日志',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as AppRouteModule;
|
||||
|
@@ -30,7 +30,7 @@ const setting: ProjectConfig = {
|
||||
// theme
|
||||
theme: MenuThemeEnum.LIGHT,
|
||||
// 开启锁屏功能
|
||||
useLockPage: isProdMode(),
|
||||
useLockPage: true,
|
||||
// 显示刷新按钮
|
||||
showRedo: true,
|
||||
// 显示全屏按钮
|
||||
@@ -86,7 +86,7 @@ const setting: ProjectConfig = {
|
||||
// 是否开启KeepAlive缓存 开发时候最好关闭,不然每次都需要清除缓存
|
||||
openKeepAlive: true,
|
||||
|
||||
// 自动锁屏时间,为0不锁屏。 单位分钟 默认1个小时
|
||||
// 自动锁屏时间,为0不锁屏。 单位分钟 默认0
|
||||
lockTime: 0,
|
||||
// 显示面包屑
|
||||
showBreadCrumb: true,
|
||||
@@ -96,6 +96,7 @@ const setting: ProjectConfig = {
|
||||
|
||||
// 开启页面切换动画
|
||||
openRouterTransition: true,
|
||||
|
||||
// 路由切换动画
|
||||
routerTransition: RouterTransitionEnum.ZOOM_FADE,
|
||||
|
||||
|
149
src/setup/error-handle/index.ts
Normal file
149
src/setup/error-handle/index.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { errorStore, ErrorInfo } from '/@/store/modules/error';
|
||||
import { useSetting } from '/@/hooks/core/useSetting';
|
||||
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
|
||||
import { App } from 'vue';
|
||||
function processStackMsg(error: Error) {
|
||||
if (!error.stack) {
|
||||
return '';
|
||||
}
|
||||
let stack = error.stack
|
||||
.replace(/\n/gi, '') // 去掉换行,节省传输内容大小
|
||||
.replace(/\bat\b/gi, '@') // chrome中是at,ff中是@
|
||||
.split('@') // 以@分割信息
|
||||
.slice(0, 9) // 最大堆栈长度(Error.stackTraceLimit = 10),所以只取前10条
|
||||
.map((v) => v.replace(/^\s*|\s*$/g, '')) // 去除多余空格
|
||||
.join('~') // 手动添加分隔符,便于后期展示
|
||||
.replace(/\?[^:]+/gi, ''); // 去除js文件链接的多余参数(?x=1之类)
|
||||
const msg = error.toString();
|
||||
if (stack.indexOf(msg) < 0) {
|
||||
stack = msg + '@' + stack;
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
function formatComponentName(vm: any) {
|
||||
if (vm.$root === vm) {
|
||||
return {
|
||||
name: 'root',
|
||||
path: 'root',
|
||||
};
|
||||
}
|
||||
|
||||
const options = vm.$options as any;
|
||||
if (!options) {
|
||||
return {
|
||||
name: 'anonymous',
|
||||
path: 'anonymous',
|
||||
};
|
||||
}
|
||||
const name = options.name || options._componentTag;
|
||||
return {
|
||||
name: name,
|
||||
path: options.__file,
|
||||
};
|
||||
}
|
||||
|
||||
function vueErrorHandler(err: Error, vm: any, info: string) {
|
||||
const { name, path } = formatComponentName(vm);
|
||||
errorStore.commitErrorInfoState({
|
||||
type: ErrorTypeEnum.VUE,
|
||||
name,
|
||||
file: path,
|
||||
message: err.message,
|
||||
stack: processStackMsg(err),
|
||||
detail: info,
|
||||
url: window.location.href,
|
||||
});
|
||||
}
|
||||
|
||||
export function scriptErrorHandler(
|
||||
event: Event | string,
|
||||
source?: string,
|
||||
lineno?: number,
|
||||
colno?: number,
|
||||
error?: Error
|
||||
) {
|
||||
if (event === 'Script error.' && !source) {
|
||||
return false;
|
||||
}
|
||||
setTimeout(function () {
|
||||
const errorInfo: Partial<ErrorInfo> = {};
|
||||
colno = colno || (window.event && (window.event as any).errorCharacter) || 0;
|
||||
errorInfo.message = event as string;
|
||||
if (error && error.stack) {
|
||||
errorInfo.stack = error.stack;
|
||||
} else {
|
||||
errorInfo.stack = '';
|
||||
}
|
||||
const name = source ? source.substr(source.lastIndexOf('/') + 1) : 'script';
|
||||
errorStore.commitErrorInfoState({
|
||||
type: ErrorTypeEnum.SCRIPT,
|
||||
name: name,
|
||||
file: source as string,
|
||||
detail: 'lineno' + lineno,
|
||||
url: window.location.href,
|
||||
...(errorInfo as Pick<ErrorInfo, 'message' | 'stack'>),
|
||||
});
|
||||
}, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
function registerPromiseErrorHandler() {
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
function (event: any) {
|
||||
errorStore.commitErrorInfoState({
|
||||
type: ErrorTypeEnum.PROMISE,
|
||||
name: 'Promise Error!',
|
||||
file: 'none',
|
||||
detail: 'promise error!',
|
||||
url: window.location.href,
|
||||
stack: 'promise error!',
|
||||
message: event.reason,
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function registerResourceErrorHandler() {
|
||||
// 监控资源加载错误(img,script,css,以及jsonp)
|
||||
window.addEventListener(
|
||||
'error',
|
||||
function (e: Event) {
|
||||
const target = e.target ? e.target : (e.srcElement as any);
|
||||
|
||||
errorStore.commitErrorInfoState({
|
||||
type: ErrorTypeEnum.RESOURCE,
|
||||
name: 'Resouce Error!',
|
||||
file: (e.target || ({} as any)).currentSrc,
|
||||
detail: JSON.stringify({
|
||||
tagName: target.localName,
|
||||
html: target.outerHTML,
|
||||
type: e.type,
|
||||
}),
|
||||
url: window.location.href,
|
||||
stack: 'resouce is not found',
|
||||
message: (e.target || ({} as any)).localName + ' is load error',
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
export function setupErrorHandle(app: App) {
|
||||
const { projectSetting } = useSetting();
|
||||
const { useErrorHandle } = projectSetting;
|
||||
if (!useErrorHandle) {
|
||||
return;
|
||||
}
|
||||
// Vue异常监控;
|
||||
app.config.errorHandler = vueErrorHandler;
|
||||
// js错误
|
||||
window.onerror = scriptErrorHandler;
|
||||
// promise 异常
|
||||
registerPromiseErrorHandler();
|
||||
|
||||
// 静态资源异常
|
||||
registerResourceErrorHandler();
|
||||
}
|
@@ -1,15 +1,10 @@
|
||||
import store from '/@/store';
|
||||
import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper';
|
||||
import { VuexModule, getModule, Module, Mutation } from 'vuex-module-decorators';
|
||||
import { VuexModule, getModule, Module, Mutation, Action } from 'vuex-module-decorators';
|
||||
|
||||
import { formatToDateTime } from '/@/utils/dateUtil';
|
||||
export enum ErrorTypeEnum {
|
||||
VUE = 'vue',
|
||||
SCRIPT = 'script',
|
||||
RESOURCE = 'resource',
|
||||
AJAX = 'ajax',
|
||||
PROMISE = 'promise',
|
||||
}
|
||||
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
|
||||
import { useSetting } from '/@/hooks/core/useSetting';
|
||||
|
||||
export interface ErrorInfo {
|
||||
type: ErrorTypeEnum;
|
||||
@@ -43,10 +38,11 @@ class Error extends VuexModule implements ErrorState {
|
||||
|
||||
@Mutation
|
||||
commitErrorInfoState(info: ErrorInfo): void {
|
||||
this.errorInfoState.unshift({
|
||||
const item = {
|
||||
...info,
|
||||
time: formatToDateTime(new Date()),
|
||||
});
|
||||
};
|
||||
this.errorInfoState = [item, ...this.errorInfoState];
|
||||
this.errorListCountState += 1;
|
||||
}
|
||||
|
||||
@@ -54,6 +50,30 @@ class Error extends VuexModule implements ErrorState {
|
||||
commitErrorListCountState(count: number): void {
|
||||
this.errorListCountState = count;
|
||||
}
|
||||
|
||||
@Action
|
||||
setupErrorHandle(error: any) {
|
||||
const { projectSetting } = useSetting();
|
||||
const { useErrorHandle } = projectSetting;
|
||||
if (!useErrorHandle) return;
|
||||
|
||||
const errInfo: Partial<ErrorInfo> = {
|
||||
message: error.message,
|
||||
type: ErrorTypeEnum.AJAX,
|
||||
};
|
||||
if (error.response) {
|
||||
const {
|
||||
config: { url = '', data: params = '', method = 'get', headers = {} } = {},
|
||||
data = {},
|
||||
} = error.response;
|
||||
errInfo.url = url;
|
||||
errInfo.name = 'Ajax Error!';
|
||||
errInfo.file = '-';
|
||||
errInfo.stack = JSON.stringify(data);
|
||||
errInfo.detail = JSON.stringify({ params, method, headers });
|
||||
}
|
||||
this.commitErrorInfoState(errInfo as ErrorInfo);
|
||||
}
|
||||
}
|
||||
export { Error };
|
||||
export const errorStore = getModule<Error>(Error);
|
||||
|
@@ -18,36 +18,13 @@ import { RequestEnum, ResultEnum, ContentTypeEnum } from '/@/enums/httpEnum';
|
||||
import { isString } from '/@/utils/is';
|
||||
import { formatRequestDate } from '/@/utils/dateUtil';
|
||||
import { setObjToUrlParams, deepMerge } from '/@/utils';
|
||||
import { errorStore, ErrorTypeEnum, ErrorInfo } from '/@/store/modules/error';
|
||||
import { appStore } from '/@/store/modules/app';
|
||||
import { errorStore } from '/@/store/modules/error';
|
||||
import { errorResult } from './const';
|
||||
|
||||
const { globSetting } = useSetting();
|
||||
const prefix = globSetting.urlPrefix;
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
|
||||
function setupErrorHandle(error: any) {
|
||||
const { useErrorHandle } = appStore.getProjectConfig;
|
||||
if (!useErrorHandle) return;
|
||||
|
||||
const errInfo: Partial<ErrorInfo> = {
|
||||
message: error.message,
|
||||
type: ErrorTypeEnum.AJAX,
|
||||
};
|
||||
if (error.response) {
|
||||
const {
|
||||
config: { url = '', data: params = '', method = 'get', headers = {} } = {},
|
||||
data = {},
|
||||
} = error.response;
|
||||
errInfo.url = url;
|
||||
errInfo.name = 'Ajax Error!';
|
||||
errInfo.file = '-';
|
||||
errInfo.stack = JSON.stringify(data);
|
||||
errInfo.detail = JSON.stringify({ params, method, headers });
|
||||
}
|
||||
errorStore.commitErrorInfoState(errInfo as ErrorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 数据处理,方便区分多种处理方式
|
||||
*/
|
||||
@@ -175,7 +152,7 @@ const transform: AxiosTransform = {
|
||||
* @description: 响应错误处理
|
||||
*/
|
||||
responseInterceptorsCatch: (error: any) => {
|
||||
setupErrorHandle(error);
|
||||
errorStore.setupErrorHandle(error);
|
||||
const { response, code, message } = error || {};
|
||||
const msg: string =
|
||||
response && response.data && response.data.error ? response.data.error.message : '';
|
||||
|
32
src/views/sys/error-log/DetailModal.vue
Normal file
32
src/views/sys/error-log/DetailModal.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<BasicModal :width="800" title="错误详情" v-bind="$attrs">
|
||||
<Description :data="info" @register="register" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { BasicModal } from '/@/components/Modal/index';
|
||||
import { ErrorInfo } from '/@/store/modules/error';
|
||||
import { Description, useDescription } from '/@/components/Description/index';
|
||||
import { getDescSchema } from './data';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorLogDetailModal',
|
||||
components: { BasicModal, Description },
|
||||
props: {
|
||||
info: {
|
||||
type: Object as PropType<ErrorInfo>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const [register] = useDescription({
|
||||
column: 2,
|
||||
schema: getDescSchema(),
|
||||
});
|
||||
return {
|
||||
register,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
65
src/views/sys/error-log/data.tsx
Normal file
65
src/views/sys/error-log/data.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import { BasicColumn } from '/@/components/Table/index';
|
||||
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
|
||||
|
||||
export function getColumns(): BasicColumn[] {
|
||||
return [
|
||||
{
|
||||
dataIndex: 'type',
|
||||
title: '类型',
|
||||
width: 80,
|
||||
customRender: ({ text }) => {
|
||||
const color =
|
||||
text === ErrorTypeEnum.VUE
|
||||
? 'green'
|
||||
: text === ErrorTypeEnum.RESOURCE
|
||||
? 'cyan'
|
||||
: text === ErrorTypeEnum.PROMISE
|
||||
? 'blue'
|
||||
: ErrorTypeEnum.AJAX
|
||||
? 'red'
|
||||
: 'purple';
|
||||
return <Tag color={color}>{() => text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'url',
|
||||
title: '地址',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
dataIndex: 'time',
|
||||
title: '时间',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
dataIndex: 'file',
|
||||
title: '文件',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: 'Name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
dataIndex: 'message',
|
||||
title: '错误信息',
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataIndex: 'stack',
|
||||
title: 'stack信息',
|
||||
width: 300,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getDescSchema() {
|
||||
return getColumns().map((column) => {
|
||||
return {
|
||||
field: column.dataIndex!,
|
||||
label: column.title,
|
||||
};
|
||||
});
|
||||
}
|
97
src/views/sys/error-log/index.vue
Normal file
97
src/views/sys/error-log/index.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<template v-for="src in imgListRef" :key="src">
|
||||
<img :src="src" v-show="false" />
|
||||
</template>
|
||||
<DetailModal :info="rowInfoRef" @register="registerModal" />
|
||||
<BasicTable @register="register" class="error-handle-table">
|
||||
<template #toolbar>
|
||||
<a-button @click="fireVueError" type="primary"> 点击触发vue错误 </a-button>
|
||||
<a-button @click="fireResourceError" type="primary"> 点击触发resource错误 </a-button>
|
||||
<a-button @click="fireAjaxError" type="primary"> 点击触发ajax错误 </a-button>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="[{ label: '详情', onClick: handleDetail.bind(null, record) }]" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, ref, nextTick } from 'vue';
|
||||
|
||||
import DetailModal from './DetailModal.vue';
|
||||
import { useModal } from '/@/components/Modal/index';
|
||||
|
||||
import { BasicTable, useTable, TableAction } from '/@/components/Table/index';
|
||||
|
||||
import { errorStore, ErrorInfo } from '/@/store/modules/error';
|
||||
|
||||
import { fireErrorApi } from '/@/api/demo/error';
|
||||
|
||||
import { getColumns } from './data';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorHandler',
|
||||
components: { DetailModal, BasicTable, TableAction },
|
||||
setup() {
|
||||
const rowInfoRef = ref<ErrorInfo>();
|
||||
const imgListRef = ref<string[]>([]);
|
||||
const [register, { setTableData }] = useTable({
|
||||
titleHelpMessage: '只在`/src/settings/projectSetting.ts` 内的useErrorHandle=true时生效!',
|
||||
title: '错误日志列表',
|
||||
columns: getColumns(),
|
||||
actionColumn: {
|
||||
width: 80,
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
});
|
||||
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
watch(
|
||||
() => errorStore.getErrorInfoState,
|
||||
(list) => {
|
||||
nextTick(() => {
|
||||
setTableData(cloneDeep(list));
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 查看详情
|
||||
function handleDetail(row: ErrorInfo) {
|
||||
rowInfoRef.value = row;
|
||||
openModal(true);
|
||||
}
|
||||
|
||||
function fireVueError() {
|
||||
throw new Error('fire vue error!');
|
||||
}
|
||||
|
||||
function fireResourceError() {
|
||||
imgListRef.value.push(`${new Date().getTime()}.png`);
|
||||
}
|
||||
|
||||
async function fireAjaxError() {
|
||||
await fireErrorApi();
|
||||
}
|
||||
|
||||
return {
|
||||
register,
|
||||
registerModal,
|
||||
handleDetail,
|
||||
fireVueError,
|
||||
fireResourceError,
|
||||
fireAjaxError,
|
||||
imgListRef,
|
||||
rowInfoRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -118,8 +118,8 @@
|
||||
&__entry {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
height: 260px;
|
||||
padding: 80px 50px 0 50px;
|
||||
// height: 260px;
|
||||
padding: 80px 50px 50px 50px;
|
||||
margin-right: 50px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
|
Reference in New Issue
Block a user