32 changed files with 674 additions and 116 deletions
@ -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', |
|||
}); |
|||
} |
|||
@ -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} |
|||
/> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -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> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -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(); |
|||
} |
|||
@ -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> |
|||
@ -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, |
|||
}; |
|||
}); |
|||
} |
|||
@ -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> |
|||
Loading…
Reference in new issue