diff --git a/mock/test.js b/mock/test.js index 47876a2..b221eaa 100644 --- a/mock/test.js +++ b/mock/test.js @@ -1,25 +1,36 @@ export default [ { - url: "/api/get", // 请求地址 - method: "get", // 请求方法 - response: ({ query }) => { // 响应内容 + url: '/api/get', // 请求地址 + method: 'get', // 请求方法 + response: ({ query }) => { + // 响应内容 return { code: 0, data: { - name: "hello world", + name: 'hello world', }, - }; + } }, }, { - url: "/api/post", - method: "post", + url: '/api/post', + method: 'post', timeout: 2000, response: { code: 0, data: { - name: "hello world", + name: 'hello world', }, }, }, + { + url: '/api/500', + method: 'get', + statusCode: 500, + response: { + code: 500, + message: '内部错误', + data: null, + }, + }, ] diff --git a/package.json b/package.json index 012ede5..b642778 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build": "vite build", "build:mock": "vite build --mode mock", "serve": "vite preview", - "lint": "eslint --ext .js,.vue src" + "lint": "eslint --fix --ext .js,.vue src" }, "browserslist": [ "> 1%", diff --git a/src/api/test.js b/src/api/test.js new file mode 100644 index 0000000..6c7a30c --- /dev/null +++ b/src/api/test.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 测试 +export const TestError = () => { + return request({ + url: '/api/500', + method: 'get', + }) +} diff --git a/src/assets/svg/bug.svg b/src/assets/svg/bug.svg new file mode 100644 index 0000000..05a150d --- /dev/null +++ b/src/assets/svg/bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ErrorLog/index.vue b/src/components/ErrorLog/index.vue new file mode 100644 index 0000000..aad4315 --- /dev/null +++ b/src/components/ErrorLog/index.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/error-log.js b/src/error-log.js new file mode 100644 index 0000000..d57dfe3 --- /dev/null +++ b/src/error-log.js @@ -0,0 +1,23 @@ +import { nextTick } from 'vue' +import store from '@/store' + +export default app => { + // 判断环境,决定是否需要监控错误,一般生产环境才需要进行错误上报 + // import.meta.env.DEV代表开发环境 + // import.meta.env.PROD代表生产环境 + + // if (import.meta.env.PROD) { + app.config.errorHandler = function(err, vm, info) { + nextTick(() => { + store.dispatch('errorLog/addErrorLog', { + err, + vm, + info, + url: window.location.href, + id: Date.now(), + }) + console.error(err, info) + }) + } + // } +} diff --git a/src/layout/components/Topbar/index.vue b/src/layout/components/Topbar/index.vue index df7bcdb..7fef7db 100644 --- a/src/layout/components/Topbar/index.vue +++ b/src/layout/components/Topbar/index.vue @@ -6,6 +6,7 @@
+
@@ -16,6 +17,7 @@ import Logo from '@/layout/components/Sidebar/Logo.vue' import Hamburger from './Hamburger.vue' import Breadcrumbs from './Breadcrumbs.vue' import Userinfo from './Userinfo.vue' +import ErrorLog from '@/components/ErrorLog/index.vue' import { useStore } from 'vuex' export default defineComponent({ @@ -24,6 +26,7 @@ export default defineComponent({ Hamburger, Breadcrumbs, Userinfo, + ErrorLog, }, setup() { const store = useStore() diff --git a/src/main.js b/src/main.js index 2ddc7d9..e948f1a 100644 --- a/src/main.js +++ b/src/main.js @@ -28,6 +28,10 @@ Object.entries(Components).forEach(([key, component]) => { app.component(key, component) }) +// 错误日志 +import useErrorHandler from './error-log' +useErrorHandler(app) + app .use(ElementPlus, { locale, diff --git a/src/router/modules/test.js b/src/router/modules/test.js index 13d53de..fbec95f 100644 --- a/src/router/modules/test.js +++ b/src/router/modules/test.js @@ -8,6 +8,7 @@ const NestPage1 = () => import('@/views/test/nest/Page1.vue') const NestPage2 = () => import('@/views/test/nest/Page2.vue') const Iscache = () => import('@/views/test/Cache.vue') const Nocache = () => import('@/views/test/Nocache.vue') +const ErrorLog = () => import('@/views/test/error-log/index.vue') export default [ { @@ -77,6 +78,15 @@ export default [ noCache: true, // 不缓存页面 }, }, + { + path: 'error-log', + name: 'test-error-log', + component: ErrorLog, + meta: { + title: '测试错误日志', + roles: ['admin', 'visitor'], + }, + }, { path: 'nest', name: 'nest', diff --git a/src/store/index.js b/src/store/index.js index 6b00599..cc950d5 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,15 +1,13 @@ //index.js import { createStore } from 'vuex' -import app from './modules/app' -import account from './modules/account' -import menu from './modules/menu' -import tags from './modules/tags' + +const modulesFiles = import.meta.globEager('./modules/*.js') +const modules = Object.entries(modulesFiles).reduce((modules, [path, mod]) => { + const moduleName = path.replace(/^\.\/modules\/(.*)\.\w+$/, '$1') + modules[moduleName] = mod.default + return modules +}, {}) export default createStore({ - modules: { - app, - account, - menu, - tags, - }, + modules, }) diff --git a/src/store/modules/errorLog.js b/src/store/modules/errorLog.js new file mode 100644 index 0000000..20120aa --- /dev/null +++ b/src/store/modules/errorLog.js @@ -0,0 +1,28 @@ +const state = { + logs: [], +} + +const mutations = { + ADD_ERROR_LOG: (state, log) => { + state.logs.push(log) + }, + CLEAR_ERROR_LOG: state => { + state.logs.splice(0) + }, +} + +const actions = { + addErrorLog({ commit }, log) { + commit('ADD_ERROR_LOG', log) + }, + clearErrorLog({ commit }) { + commit('CLEAR_ERROR_LOG') + }, +} + +export default { + namespaced: true, + state, + mutations, + actions, +} diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..4c6064c --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,355 @@ +/** + * Parse the time to string + * @param {(Object|string|number)} time + * @param {string} cFormat + * @returns {string | null} + */ +export function parseTime(time, cFormat) { + if (arguments.length === 0 || !time) { + return null + } + const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if (typeof time === 'string') { + if (/^[0-9]+$/.test(time)) { + // support "1548221490638" + time = parseInt(time) + } else { + // support safari + // https://stackoverflow.com/questions/4310953/invalid-date-in-safari + time = time.replace(new RegExp(/-/gm), '/') + } + } + + if (typeof time === 'number' && time.toString().length === 10) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay(), + } + const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { + const value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { + return ['日', '一', '二', '三', '四', '五', '六'][value] + } + return value.toString().padStart(2, '0') + }) + return time_str +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function getQueryObject(url) { + url = url == null ? window.location.href : url + const search = url.substring(url.lastIndexOf('?') + 1) + const obj = {} + const reg = /([^?&=]+)=([^?&=]*)/g + search.replace(reg, (rs, $1, $2) => { + const name = decodeURIComponent($1) + let val = decodeURIComponent($2) + val = String(val) + obj[name] = val + return rs + }) + return obj +} + +/** + * @param {string} input value + * @returns {number} output value + */ +export function byteLength(str) { + // returns the byte length of an utf8 string + let s = str.length + for (var i = str.length - 1; i >= 0; i--) { + const code = str.charCodeAt(i) + if (code > 0x7f && code <= 0x7ff) s++ + else if (code > 0x7ff && code <= 0xffff) s += 2 + if (code >= 0xdc00 && code <= 0xdfff) i-- + } + return s +} + +/** + * @param {Array} actual + * @returns {Array} + */ +export function cleanArray(actual) { + const newArray = [] + for (let i = 0; i < actual.length; i++) { + if (actual[i]) { + newArray.push(actual[i]) + } + } + return newArray +} + +/** + * @param {Object} json + * @returns {Array} + */ +export function param(json) { + if (!json) return '' + return cleanArray( + Object.keys(json).map(key => { + if (json[key] === undefined) return '' + return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]) + }) + ).join('&') +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} + +/** + * @param {string} val + * @returns {string} + */ +export function html2Text(val) { + const div = document.createElement('div') + div.innerHTML = val + return div.textContent || div.innerText +} + +/** + * Merges two objects, giving the last one precedence + * @param {Object} target + * @param {(Object|Array)} source + * @returns {Object} + */ +export function objectMerge(target, source) { + if (typeof target !== 'object') { + target = {} + } + if (Array.isArray(source)) { + return source.slice() + } + Object.keys(source).forEach(property => { + const sourceProperty = source[property] + if (typeof sourceProperty === 'object') { + target[property] = objectMerge(target[property], sourceProperty) + } else { + target[property] = sourceProperty + } + }) + return target +} + +/** + * @param {HTMLElement} element + * @param {string} className + */ +export function toggleClass(element, className) { + if (!element || !className) { + return + } + let classString = element.className + const nameIndex = classString.indexOf(className) + if (nameIndex === -1) { + classString += '' + className + } else { + classString = + classString.substr(0, nameIndex) + + classString.substr(nameIndex + className.length) + } + element.className = classString +} + +/** + * @param {string} type + * @returns {Date} + */ +export function getTime(type) { + if (type === 'start') { + return new Date().getTime() - 3600 * 1000 * 24 * 90 + } else { + return new Date(new Date().toDateString()) + } +} + +/** + * @param {Function} func + * @param {number} wait + * @param {boolean} immediate + * @return {*} + */ +export function debounce(func, wait, immediate) { + let timeout, args, context, timestamp, result + + const later = function() { + // 据上一次触发时间间隔 + const last = +new Date() - timestamp + + // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last) + } else { + timeout = null + // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 + if (!immediate) { + result = func.apply(context, args) + if (!timeout) context = args = null + } + } + } + + return function(...args) { + context = this + timestamp = +new Date() + const callNow = immediate && !timeout + // 如果延时不存在,重新设定延时 + if (!timeout) timeout = setTimeout(later, wait) + if (callNow) { + result = func.apply(context, args) + context = args = null + } + + return result + } +} + +/** + * This is just a simple version of deep copy + * Has a lot of edge cases bug + * If you want to use a perfect deep copy, use lodash's _.cloneDeep + * @param {Object} source + * @returns {Object} + */ +export function deepClone(source) { + if (!source && typeof source !== 'object') { + throw new Error('error arguments', 'deepClone') + } + const targetObj = source.constructor === Array ? [] : {} + Object.keys(source).forEach(keys => { + if (source[keys] && typeof source[keys] === 'object') { + targetObj[keys] = deepClone(source[keys]) + } else { + targetObj[keys] = source[keys] + } + }) + return targetObj +} + +/** + * @param {Array} arr + * @returns {Array} + */ +export function uniqueArr(arr) { + return Array.from(new Set(arr)) +} + +/** + * @returns {string} + */ +export function createUniqueString() { + const timestamp = +new Date() + '' + const randomNum = parseInt((1 + Math.random()) * 65536) + '' + return (+(randomNum + timestamp)).toString(32) +} + +/** + * Check if an element has a class + * @param {HTMLElement} elm + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele, cls) { + return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) +} + +/** + * Add class to element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function addClass(ele, cls) { + if (!hasClass(ele, cls)) ele.className += ' ' + cls +} + +/** + * Remove class from element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function removeClass(ele, cls) { + if (hasClass(ele, cls)) { + const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)') + ele.className = ele.className.replace(reg, ' ') + } +} diff --git a/src/utils/open-window.js b/src/utils/open-window.js new file mode 100644 index 0000000..de8f906 --- /dev/null +++ b/src/utils/open-window.js @@ -0,0 +1,44 @@ +/** + * @param {Sting} url + * @param {Sting} title + * @param {Number} w + * @param {Number} h + */ +export default function openWindow(url, title, w, h) { + // Fixes dual-screen position Most browsers Firefox + const dualScreenLeft = + window.screenLeft !== undefined ? window.screenLeft : screen.left + const dualScreenTop = + window.screenTop !== undefined ? window.screenTop : screen.top + + const width = window.innerWidth + ? window.innerWidth + : document.documentElement.clientWidth + ? document.documentElement.clientWidth + : screen.width + const height = window.innerHeight + ? window.innerHeight + : document.documentElement.clientHeight + ? document.documentElement.clientHeight + : screen.height + + const left = width / 2 - w / 2 + dualScreenLeft + const top = height / 2 - h / 2 + dualScreenTop + const newWindow = window.open( + url, + title, + 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + + w + + ', height=' + + h + + ', top=' + + top + + ', left=' + + left + ) + + // Puts focus on the newWindow + if (window.focus) { + newWindow.focus() + } +} diff --git a/src/utils/scroll-to.js b/src/utils/scroll-to.js new file mode 100644 index 0000000..f7f9dc2 --- /dev/null +++ b/src/utils/scroll-to.js @@ -0,0 +1,69 @@ +Math.easeInOutQuad = function(t, b, c, d) { + t /= d / 2 + if (t < 1) { + return (c / 2) * t * t + b + } + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} + +// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts +var requestAnimFrame = (function() { + return ( + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000 / 60) + } + ) +})() + +/** + * Because it's so fucking difficult to detect the scrolling element, just move them all + * @param {number} amount + */ +function move(amount) { + document.documentElement.scrollTop = amount + document.body.parentNode.scrollTop = amount + document.body.scrollTop = amount +} + +function position() { + return ( + document.documentElement.scrollTop || + document.body.parentNode.scrollTop || + document.body.scrollTop + ) +} + +/** + * @param {number} to + * @param {number} duration + * @param {Function} callback + */ +export function scrollTo(to, duration, callback) { + const start = position() + const change = to - start + const increment = 20 + let currentTime = 0 + duration = typeof duration === 'undefined' ? 500 : duration + var animateScroll = function() { + // increment the time + currentTime += increment + // find the value with the quadratic in-out easing function + var val = Math.easeInOutQuad(currentTime, start, change, duration) + // move the document.body + move(val) + // do the animation unless its over + if (currentTime < duration) { + requestAnimFrame(animateScroll) + } else { + if (callback && typeof callback === 'function') { + // the animation is done so lets callback + callback() + } + } + } + animateScroll() +} diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..ea4da42 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,83 @@ +/** + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername(str) { + const valid_map = ['admin', 'editor'] + return valid_map.indexOf(str.trim()) >= 0 +} + +/** + * @param {string} url + * @returns {Boolean} + */ +export function validURL(url) { + const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ + return reg.test(url) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validLowerCase(str) { + const reg = /^[a-z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUpperCase(str) { + const reg = /^[A-Z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validAlphabets(str) { + const reg = /^[A-Za-z]+$/ + return reg.test(str) +} + +/** + * @param {string} email + * @returns {Boolean} + */ +export function validEmail(email) { + const reg = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return reg.test(email) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function isString(str) { + if (typeof str === 'string' || str instanceof String) { + return true + } + return false +} + +/** + * @param {Array} arg + * @returns {Boolean} + */ +export function isArray(arg) { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) +} diff --git a/src/views/test/error-log/components/ErrorTestA.vue b/src/views/test/error-log/components/ErrorTestA.vue new file mode 100644 index 0000000..8715c1c --- /dev/null +++ b/src/views/test/error-log/components/ErrorTestA.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/views/test/error-log/components/ErrorTestB.vue b/src/views/test/error-log/components/ErrorTestB.vue new file mode 100644 index 0000000..060d554 --- /dev/null +++ b/src/views/test/error-log/components/ErrorTestB.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/views/test/error-log/index.vue b/src/views/test/error-log/index.vue new file mode 100644 index 0000000..a36d47d --- /dev/null +++ b/src/views/test/error-log/index.vue @@ -0,0 +1,26 @@ + + + + +