7个文件已添加
24个文件已修改
1 文件已重命名
| | |
| | | # Delete console |
| | | VITE_DROP_CONSOLE = true |
| | | |
| | | # Delete console |
| | | # Whether to output gz file for packaging |
| | | VITE_BUILD_GZIP = false |
| | | |
| | | # Basic interface address SPA |
| | |
| | | - [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. |
| | | |
| | |
| | | - [x] 首屏加载等待动画 |
| | | - [x] 抽取生产环境配置文件 |
| | | - [x] 打包 Gzip |
| | | - [x] 数据导入导出 |
| | | - [x] 系统性能优化 |
| | | - [x] 全局错误处理 |
| | | |
| | | ## 正在开发的功能 |
| | | |
| | | - [ ] 上传组件 |
| | | - [ ] 富文本组件 |
| | | - [ ] 数据导入导出 |
| | | - [ ] 全局错误处理 |
| | | - [ ] 主题配置 |
| | | - [ ] 黑暗主题 |
| | | - [ ] 打包 CDN |
| | | - [ ] 系统性能优化 |
| | | |
| | | 更多组件/功能/建议/bug/欢迎提交 pr 或者 issue |
| | | |
| | |
| | | |
| | | 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; |
| | |
| | | "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", |
| | |
| | | "@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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | const { on } = useLockPage(); |
| | | lockOn = on; |
| | | } |
| | | |
| | | return { |
| | | transformCellText, |
| | | zhCN, |
New file |
| | |
| | | import { defHttp } from '/@/utils/http/axios'; |
| | | |
| | | enum Api { |
| | | // 该地址不存在 |
| | | Error = '/error', |
| | | } |
| | | |
| | | /** |
| | | * @description: 触发ajax错误 |
| | | */ |
| | | export function fireErrorApi() { |
| | | return defHttp.request({ |
| | | url: Api.Error, |
| | | method: 'GET', |
| | | }); |
| | | } |
| | |
| | | } |
| | | if (showSummary) { |
| | | propsData.footer = renderFooter.bind(null, { |
| | | scroll, |
| | | scroll: scroll as any, |
| | | columnsRef: getColumnsRef, |
| | | summaryFunc: unref(getMergeProps).summaryFunc, |
| | | dataSourceRef: getDataSourceRef, |
| | |
| | | const { |
| | | disabled = false, |
| | | label, |
| | | props, |
| | | icon, |
| | | color = '', |
| | | type = 'link', |
| | |
| | | size="small" |
| | | disabled={disabled} |
| | | color={color} |
| | | {...props} |
| | | {...action} |
| | | key={index} |
| | | > |
| | | {() => ( |
| | |
| | | const { |
| | | disabled = false, |
| | | label, |
| | | props, |
| | | icon, |
| | | color = '', |
| | | type = 'link', |
| | |
| | | <Button |
| | | type={type} |
| | | size="small" |
| | | {...props} |
| | | {...action} |
| | | disabled={disabled} |
| | | color={color} |
| | | > |
| | |
| | | // 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', |
| | | } |
| | |
| | | // 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 { |
| | |
| | | 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'; |
| | |
| | | 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', |
| | |
| | | const { refreshPage } = useTabs(); |
| | | const [register, { openModal }] = useModal(); |
| | | const { toggleFullscreen, isFullscreenRef } = useFullscreen(); |
| | | const go = useGo(); |
| | | const getProjectConfigRef = computed(() => { |
| | | return appStore.getProjectConfig; |
| | | }); |
| | |
| | | const theme = unref(getProjectConfigRef).headerSetting.theme; |
| | | return theme ? `layout-header__header--${theme}` : ''; |
| | | }); |
| | | |
| | | function handleToErrorList() { |
| | | errorStore.commitErrorListCountState(0); |
| | | go('/exception/error-log'); |
| | | } |
| | | |
| | | /** |
| | | * @description: 锁定屏幕 |
| | | */ |
| | |
| | | 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; |
| | |
| | | </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', |
| | |
| | | }} |
| | | </Tooltip> |
| | | )} |
| | | {showGithub && ( |
| | | // @ts-ignore |
| | | {useLockPage && ( |
| | | <Tooltip> |
| | | {{ |
| | | title: () => '锁定屏幕', |
| | |
| | | </Tooltip> |
| | | )} |
| | | {showRedo && ( |
| | | // @ts-ignore |
| | | <Tooltip> |
| | | {{ |
| | | title: () => '刷新', |
| | |
| | | </Tooltip> |
| | | )} |
| | | {showFullScreen && ( |
| | | // @ts-ignore |
| | | <Tooltip> |
| | | {{ |
| | | title: () => (unref(isFullscreenRef) ? '退出全屏' : '全屏'), |
| | |
| | | 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, |
New file |
| | |
| | | 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} |
| | | /> |
| | | ); |
| | | }; |
| | | }, |
| | | }); |
New file |
| | |
| | | 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> |
| | | ); |
| | | }; |
| | | }, |
| | | }); |
| | |
| | | .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; |
| | | } |
| | | } |
| | | |
| | |
| | | position: absolute; |
| | | z-index: 10; |
| | | display: flex; |
| | | padding: 10px; |
| | | // padding: 10px; |
| | | color: @white; |
| | | cursor: pointer; |
| | | background: @primary-color; |
| | |
| | | {...on} |
| | | name={name || route.meta.transitionName || routerTransition} |
| | | mode="out-in" |
| | | appear={true} |
| | | > |
| | | {() => Content} |
| | | </Transition> |
| | |
| | | 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'; |
| | |
| | | // store |
| | | setupStore(app); |
| | | |
| | | registerGlobComp(app); |
| | | |
| | | setupDirectives(app); |
| | | |
| | | setupErrorHandle(app); |
| | | |
| | | registerGlobComp(app); |
| | | |
| | | router.isReady().then(() => { |
| | | app.mount('#app'); |
| | | }); |
| | |
| | | path: '/not-data', |
| | | name: '无数据', |
| | | }, |
| | | { |
| | | path: '/error-log', |
| | | name: '错误日志', |
| | | }, |
| | | ], |
| | | }, |
| | | }; |
| | |
| | | afterCloseLoading: true, |
| | | }, |
| | | }, |
| | | { |
| | | path: '/error-log', |
| | | name: 'ErrorLog', |
| | | component: () => import('/@/views/sys/error-log/index.vue'), |
| | | meta: { |
| | | title: '错误日志', |
| | | }, |
| | | }, |
| | | ], |
| | | } as AppRouteModule; |
| | |
| | | // theme |
| | | theme: MenuThemeEnum.LIGHT, |
| | | // 开启锁屏功能 |
| | | useLockPage: isProdMode(), |
| | | useLockPage: true, |
| | | // 显示刷新按钮 |
| | | showRedo: true, |
| | | // 显示全屏按钮 |
| | |
| | | // 是否开启KeepAlive缓存 开发时候最好关闭,不然每次都需要清除缓存 |
| | | openKeepAlive: true, |
| | | |
| | | // 自动锁屏时间,为0不锁屏。 单位分钟 默认1个小时 |
| | | // 自动锁屏时间,为0不锁屏。 单位分钟 默认0 |
| | | lockTime: 0, |
| | | // 显示面包屑 |
| | | showBreadCrumb: true, |
| | |
| | | |
| | | // 开启页面切换动画 |
| | | openRouterTransition: true, |
| | | |
| | | // 路由切换动画 |
| | | routerTransition: RouterTransitionEnum.ZOOM_FADE, |
| | | |
New file |
| | |
| | | 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(); |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | @Mutation |
| | | commitErrorInfoState(info: ErrorInfo): void { |
| | | this.errorInfoState.unshift({ |
| | | const item = { |
| | | ...info, |
| | | time: formatToDateTime(new Date()), |
| | | }); |
| | | }; |
| | | this.errorInfoState = [item, ...this.errorInfoState]; |
| | | this.errorListCountState += 1; |
| | | } |
| | | |
| | |
| | | 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); |
| | |
| | | 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: 数据处理,方便区分多种处理方式 |
| | |
| | | * @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 : ''; |
New file |
| | |
| | | <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> |
New file |
| | |
| | | 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, |
| | | }; |
| | | }); |
| | | } |
New file |
| | |
| | | <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> |
| | |
| | | &__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; |
| | |
| | | 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'); |
| | |
| | | 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" |
| | |
| | | "@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" "*" |
| | | |
| | |
| | | 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" "*" |
| | | |
| | |
| | | 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" |
| | |
| | | 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-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" |
| | | |
| | |
| | | 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" |
| | |
| | | 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" |
| | |
| | | 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" |
| | |
| | | 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" |