Kaynağa Gözat

feat: add error handle

vben 4 yıl önce
ebeveyn
işleme
7101587b96

+ 1 - 1
.env.production

@@ -7,7 +7,7 @@ VITE_PUBLIC_PATH = ./
 # Delete console
 VITE_DROP_CONSOLE = true
 
-# Delete console
+# Whether to output gz file for packaging
 VITE_BUILD_GZIP = false
 
 # Basic interface address SPA

+ 3 - 3
README.en-US.md

@@ -225,17 +225,17 @@ yarn clean:lib # Delete node_modules, supported window
 - [x] First screen loading waiting animation
 - [x] Extract the production environment profile
 - [x] Build Gzip
+- [x] System performance optimization
+- [x] Data import and export
+- [x] Global error handling
 
 ## Developing features
 
 - [ ] Upload component
 - [ ] Rich text component
-- [ ] Data import and export
-- [ ] Global error handling
 - [ ] Theme configuration
 - [ ] Dark theme
 - [ ] Build CDN
-- [ ] System performance optimization
 
 If you have more components/functions/suggestions/bugs/, welcome to submit pr or issue.
 

+ 3 - 3
README.md

@@ -223,17 +223,17 @@ yarn clean:lib # 删除node_modules,兼容window系统
 - [x] 首屏加载等待动画
 - [x] 抽取生产环境配置文件
 - [x] 打包 Gzip
+- [x] 数据导入导出
+- [x] 系统性能优化
+- [x] 全局错误处理
 
 ## 正在开发的功能
 
 - [ ] 上传组件
 - [ ] 富文本组件
-- [ ] 数据导入导出
-- [ ] 全局错误处理
 - [ ] 主题配置
 - [ ] 黑暗主题
 - [ ] 打包 CDN
-- [ ] 系统性能优化
 
 更多组件/功能/建议/bug/欢迎提交 pr 或者 issue
 

+ 4 - 0
build/config/vite/proxy.ts

@@ -2,13 +2,17 @@ type ProxyItem = [string, string];
 
 type ProxyList = ProxyItem[];
 
+const reg = /^https:\/\//;
 export function createProxy(list: ProxyList = []) {
   const ret: any = {};
   for (const [prefix, target] of list) {
+    const isHttps = reg.test(target);
+
     ret[prefix] = {
       target: target,
       changeOrigin: true,
       rewrite: (path: string) => path.replace(new RegExp(`^${prefix}`), ''),
+      ...(isHttps ? { secure: false } : {}),
     };
   }
   return ret;

+ 0 - 0
build/plugin/vite-plugin-context-plugin/transform.ts → build/plugin/vite-plugin-context/transform.ts


+ 6 - 6
package.json

@@ -41,10 +41,10 @@
   "devDependencies": {
     "@commitlint/cli": "^11.0.0",
     "@commitlint/config-conventional": "^11.0.0",
-    "@iconify/json": "^1.1.242",
+    "@iconify/json": "^1.1.243",
     "@ls-lint/ls-lint": "^1.9.2",
     "@purge-icons/generated": "^0.4.1",
-    "@types/echarts": "^4.8.1",
+    "@types/echarts": "^4.8.3",
     "@types/fs-extra": "^9.0.2",
     "@types/html-minifier": "^4.0.0",
     "@types/inquirer": "^7.3.1",
@@ -55,7 +55,7 @@
     "@types/qrcode": "^1.3.5",
     "@types/rollup-plugin-visualizer": "^2.6.0",
     "@types/shelljs": "^0.8.8",
-    "@types/yargs": "^15.0.8",
+    "@types/yargs": "^15.0.9",
     "@types/zxcvbn": "^4.4.0",
     "@typescript-eslint/eslint-plugin": "^4.4.1",
     "@typescript-eslint/parser": "^4.4.1",
@@ -67,7 +67,7 @@
     "cross-env": "^7.0.2",
     "dotenv": "^8.2.0",
     "eslint": "^7.10.0",
-    "eslint-config-prettier": "^6.12.0",
+    "eslint-config-prettier": "^6.13.0",
     "eslint-plugin-prettier": "^3.1.4",
     "eslint-plugin-vue": "^7.0.1",
     "fs-extra": "^9.0.1",
@@ -76,7 +76,7 @@
     "inquirer": "^7.3.3",
     "koa-static": "^5.0.0",
     "less": "^3.12.2",
-    "lint-staged": "^10.4.0",
+    "lint-staged": "^10.4.2",
     "portfinder": "^1.0.28",
     "postcss-import": "^12.0.1",
     "prettier": "^2.1.2",
@@ -95,7 +95,7 @@
     "vite-plugin-mock": "^1.0.2",
     "vite-plugin-purge-icons": "^0.4.4",
     "vue-eslint-parser": "^7.1.1",
-    "yargs": "^16.0.3"
+    "yargs": "^16.1.0"
   },
   "repository": {
     "type": "git",

+ 1 - 0
src/App.vue

@@ -33,6 +33,7 @@
         const { on } = useLockPage();
         lockOn = on;
       }
+
       return {
         transformCellText,
         zhCN,

+ 16 - 0
src/api/demo/error.ts

@@ -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',
+  });
+}

+ 1 - 1
src/components/Table/src/BasicTable.vue

@@ -146,7 +146,7 @@
         }
         if (showSummary) {
           propsData.footer = renderFooter.bind(null, {
-            scroll,
+            scroll: scroll as any,
             columnsRef: getColumnsRef,
             summaryFunc: unref(getMergeProps).summaryFunc,
             dataSourceRef: getDataSourceRef,

+ 2 - 4
src/components/Table/src/components/TableAction.tsx

@@ -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}
                                   >

+ 8 - 0
src/enums/exceptionEnum.ts

@@ -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',
+}

+ 1 - 1
src/hooks/web/usePage.ts

@@ -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 {

+ 35 - 8
src/layouts/default/LayoutHeader.tsx

@@ -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) ? '退出全屏' : '全屏'),

+ 3 - 2
src/layouts/default/actions/LockActionItem.tsx

@@ -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 - 0
src/layouts/default/actions/notice/NoticeActionItem.tsx

@@ -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 - 0
src/layouts/default/actions/notice/NoticeList.tsx

@@ -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>
+      );
+    };
+  },
+});

+ 3 - 3
src/layouts/default/index.less

@@ -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;
     }
   }
 

+ 1 - 1
src/layouts/default/setting/index.vue

@@ -30,7 +30,7 @@
     position: absolute;
     z-index: 10;
     display: flex;
-    padding: 10px;
+    // padding: 10px;
     color: @white;
     cursor: pointer;
     background: @primary-color;

+ 1 - 0
src/layouts/page/index.tsx

@@ -54,6 +54,7 @@ export default defineComponent({
                     {...on}
                     name={name || route.meta.transitionName || routerTransition}
                     mode="out-in"
+                    appear={true}
                   >
                     {() => Content}
                   </Transition>

+ 5 - 2
src/main.ts

@@ -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');
 });

+ 4 - 0
src/router/menus/modules/demo/exception.ts

@@ -25,6 +25,10 @@ const menu: MenuModule = {
         path: '/not-data',
         name: '无数据',
       },
+      {
+        path: '/error-log',
+        name: '错误日志',
+      },
     ],
   },
 };

+ 8 - 0
src/router/routes/modules/demo/exception.ts

@@ -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;

+ 3 - 2
src/settings/projectSetting.ts

@@ -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 - 0
src/setup/error-handle/index.ts

@@ -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();
+}

+ 30 - 10
src/store/modules/error.ts

@@ -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);

+ 2 - 25
src/utils/http/axios/index.ts

@@ -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 - 0
src/views/sys/error-log/DetailModal.vue

@@ -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 - 0
src/views/sys/error-log/data.tsx

@@ -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 - 0
src/views/sys/error-log/index.vue

@@ -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>

+ 2 - 2
src/views/sys/lock/index.vue

@@ -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;

+ 1 - 1
vite.config.ts

@@ -13,7 +13,7 @@ import { createProxy } from './build/config/vite/proxy';
 import { createMockServer } from 'vite-plugin-mock';
 import PurgeIcons from 'vite-plugin-purge-icons';
 import gzipPlugin from './build/plugin/gzip/index';
-import globbyTransform from './build/plugin/vite-plugin-context-plugin/transform';
+import globbyTransform from './build/plugin/vite-plugin-context/transform';
 
 import { isDevFn, isReportMode, isProdFn, loadEnv, isBuildGzip, isSiteMode } from './build/utils';
 const pkg = require('./package.json');

+ 46 - 41
yarn.lock

@@ -386,10 +386,10 @@
   resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.1.tgz#a8bae29d71016d5af98c69f56a73c4a040217b3a"
   integrity sha512-ji5H04VjYtR4seIEgVVLPxg1KRhrFquOiyfPyLVS6vYPkuqV6bcWdssi05YSmf/OAzG4E7Qsg80/bOKyd5tYTw==
 
-"@iconify/json@^1.1.242":
-  version "1.1.242"
-  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.242.tgz#f43b83acb7c3fe7906eb46cbe174a946aa17f9ff"
-  integrity sha512-uxiUvrINyZlITzKxa2J/75AzReenah83XxQLyryVHjGCSqNWQjQICmBLlrCsch5PMr5nF9JeNMbIPDzxlS59Og==
+"@iconify/json@^1.1.243":
+  version "1.1.243"
+  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.243.tgz#8013fc781621b2549e379aab4e3e945974ec8032"
+  integrity sha512-vf8N+fUJysvCoJknS4XqorlvWLqjw/+Mid8CuzxQxMX8/RlWRU1yMFIgRpGAkMnCYe38KUtHqFVu0FlVNGP/Lw==
 
 "@ls-lint/ls-lint@^1.9.2":
   version "1.9.2"
@@ -577,10 +577,10 @@
     "@types/keygrip" "*"
     "@types/node" "*"
 
-"@types/echarts@^4.8.1":
-  version "4.8.1"
-  resolved "https://registry.npmjs.org/@types/echarts/-/echarts-4.8.1.tgz#e03aed60bbf25b7629affab699175df2e980fbb2"
-  integrity sha512-+kyP8TUkyJgmIBioPBJiTay9G7f/xcW7/8CYgh3iWa8kQ/SbGmAIpXyyCXtiWqPXT+tnsIONLC4hcNfmxVfxAg==
+"@types/echarts@^4.8.3":
+  version "4.8.3"
+  resolved "https://registry.npmjs.org/@types/echarts/-/echarts-4.8.3.tgz#78ef1ede01c3705b52342da997b3d54571d3604e"
+  integrity sha512-5aFZ7/6f+SPonLh4Nuso6pEZWwX8VBMYh2e83x1GVEpGkcN3GC0HzxPoF6ZSZPwoe5Rg4bhNwD9f2TVYxgU/QQ==
   dependencies:
     "@types/zrender" "*"
 
@@ -849,10 +849,10 @@
   resolved "https://registry.npm.taobao.org/@types/yargs-parser/download/@types/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
   integrity sha1-yz+fdBhp4gzOMw/765JxWQSDiC0=
 
-"@types/yargs@^15.0.8":
-  version "15.0.8"
-  resolved "https://registry.npm.taobao.org/@types/yargs/download/@types/yargs-15.0.8.tgz?cache=0&sync_timestamp=1602182032636&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fyargs%2Fdownload%2F%40types%2Fyargs-15.0.8.tgz#7644904cad7427eb704331ea9bf1ee5499b82e23"
-  integrity sha1-dkSQTK10J+twQzHqm/HuVJm4LiM=
+"@types/yargs@^15.0.9":
+  version "15.0.9"
+  resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19"
+  integrity sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g==
   dependencies:
     "@types/yargs-parser" "*"
 
@@ -1735,10 +1735,10 @@ cliui@^6.0.0:
     strip-ansi "^6.0.0"
     wrap-ansi "^6.2.0"
 
-cliui@^7.0.0:
-  version "7.0.1"
-  resolved "https://registry.npm.taobao.org/cliui/download/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3"
-  integrity sha1-pMtnqtRc2D2NBRKPyfTY+7iH5rM=
+cliui@^7.0.2:
+  version "7.0.3"
+  resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.3.tgz#ef180f26c8d9bff3927ee52428bfec2090427981"
+  integrity sha512-Gj3QHTkVMPKqwP3f7B4KPkBZRMR9r4rfi5bXFpg1a+Svvj8l7q5CnkBkVQzfxT5DFSsGk2+PascOgL0JYkL2kw==
   dependencies:
     string-width "^4.2.0"
     strip-ansi "^6.0.0"
@@ -2537,11 +2537,16 @@ esbuild@^0.7.1:
   resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.7.14.tgz#9de555e75669187c2315317fbf489b229b1a4cbb"
   integrity sha512-w2CEVeRcUhCGYMHnNNwb8q+9w42scL7RcNzJm85gZVzNBE3AF0sLq5YP/IdaTBJIFBphIKG3bGbwRH+zsgH/ig==
 
-escalade@^3.0.2, escalade@^3.1.0:
+escalade@^3.1.0:
   version "3.1.0"
   resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
   integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
 
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
 escape-goat@^2.0.0:
   version "2.1.1"
   resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
@@ -2557,10 +2562,10 @@ escape-string-regexp@^1.0.5:
   resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
-eslint-config-prettier@^6.12.0:
-  version "6.12.0"
-  resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2"
-  integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==
+eslint-config-prettier@^6.13.0:
+  version "6.13.0"
+  resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.13.0.tgz#207d88796b5624e5bb815bbbdfc5891ceb9ebffa"
+  integrity sha512-LcT0i0LSmnzqK2t764pyIt7kKH2AuuqKRTtJTdddWxOiUja9HdG5GXBVF2gmCTvVYWVsTu8J2MhJLVGRh+pj8w==
   dependencies:
     get-stdin "^6.0.0"
 
@@ -4207,10 +4212,10 @@ lines-and-columns@^1.1.6:
   resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
   integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
 
-lint-staged@^10.4.0:
-  version "10.4.0"
-  resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.0.tgz#d18628f737328e0bbbf87d183f4020930e9a984e"
-  integrity sha512-uaiX4U5yERUSiIEQc329vhCTDDwUcSvKdRLsNomkYLRzijk3v8V9GWm2Nz0RMVB87VcuzLvtgy6OsjoH++QHIg==
+lint-staged@^10.4.2:
+  version "10.4.2"
+  resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.2.tgz#9fee4635c4b5ddb845746f237c6d43494ccd21c1"
+  integrity sha512-OLCA9K1hS+Sl179SO6kX0JtnsaKj/MZalEhUj5yAgXsb63qPI/Gfn6Ua1KuZdbfkZNEu3/n5C/obYCu70IMt9g==
   dependencies:
     chalk "^4.1.0"
     cli-truncate "^2.1.0"
@@ -7268,10 +7273,10 @@ y18n@^4.0.0:
   resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
   integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
 
-y18n@^5.0.1:
-  version "5.0.2"
-  resolved "https://registry.npm.taobao.org/y18n/download/y18n-5.0.2.tgz?cache=0&sync_timestamp=1601576683926&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fy18n%2Fdownload%2Fy18n-5.0.2.tgz#48218df5da2731b4403115c39a1af709c873f829"
-  integrity sha1-SCGN9donMbRAMRXDmhr3Cchz+Ck=
+y18n@^5.0.2:
+  version "5.0.4"
+  resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.4.tgz#0ab2db89dd5873b5ec4682d8e703e833373ea897"
+  integrity sha512-deLOfD+RvFgrpAmSZgfGdWYE+OKyHcVHaRQ7NphG/63scpRvTHHeQMAxGGvaLVGJ+HYVcCXlzcTK0ZehFf+eHQ==
 
 yallist@^3.0.2:
   version "3.1.1"
@@ -7304,10 +7309,10 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
-yargs-parser@^20.0.0:
-  version "20.2.1"
-  resolved "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-20.2.1.tgz?cache=0&sync_timestamp=1601576684570&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs-parser%2Fdownload%2Fyargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77"
-  integrity sha1-KPN3PFRs3Ypp3a5oEWtIpdoyjnc=
+yargs-parser@^20.2.2:
+  version "20.2.3"
+  resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26"
+  integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww==
 
 yargs@^13.2.4:
   version "13.3.2"
@@ -7342,18 +7347,18 @@ yargs@^15.0.0, yargs@^15.1.0:
     y18n "^4.0.0"
     yargs-parser "^18.1.2"
 
-yargs@^16.0.3:
-  version "16.0.3"
-  resolved "https://registry.npm.taobao.org/yargs/download/yargs-16.0.3.tgz?cache=0&sync_timestamp=1600660006050&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs%2Fdownload%2Fyargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c"
-  integrity sha1-epGbnkPJD4DUoUKol5XoU5mn5Uw=
+yargs@^16.1.0:
+  version "16.1.0"
+  resolved "https://registry.npmjs.org/yargs/-/yargs-16.1.0.tgz#fc333fe4791660eace5a894b39d42f851cd48f2a"
+  integrity sha512-upWFJOmDdHN0syLuESuvXDmrRcWd1QafJolHskzaw79uZa7/x53gxQKiR07W59GWY1tFhhU/Th9DrtSfpS782g==
   dependencies:
-    cliui "^7.0.0"
-    escalade "^3.0.2"
+    cliui "^7.0.2"
+    escalade "^3.1.1"
     get-caller-file "^2.0.5"
     require-directory "^2.1.1"
     string-width "^4.2.0"
-    y18n "^5.0.1"
-    yargs-parser "^20.0.0"
+    y18n "^5.0.2"
+    yargs-parser "^20.2.2"
 
 ylru@^1.2.0:
   version "1.2.1"