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 @@
+
+
+
+
+
+
+
+
+
+
+ Error Log
+ Clear All
+
+
+
+
+
+ Msg:
+
+ {{ row.err.message }}
+
+
+
+
+ Info:
+
+
+
+
+
+
+
+ Url:
+
+
+ {{ row.url }}
+
+
+
+
+
+
+ {{ scope.row.err.stack }}
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ {{ a.a }}
+
+
+
+
+
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 @@
+
+
+
+
+
这个页面是测试错误上报功能的
+
+ 本页面主动抛出了错误,你现在可以点击右上角的`debugger`图标查看错误日志
+
+
+
+
+
+
+