This commit is contained in:
huzhushan 2021-04-20 18:36:21 +08:00
parent e52621c808
commit 15cd0e20da
18 changed files with 803 additions and 19 deletions

View File

@ -1,25 +1,36 @@
export default [ export default [
{ {
url: "/api/get", // 请求地址 url: '/api/get', // 请求地址
method: "get", // 请求方法 method: 'get', // 请求方法
response: ({ query }) => { // 响应内容 response: ({ query }) => {
// 响应内容
return { return {
code: 0, code: 0,
data: { data: {
name: "hello world", name: 'hello world',
}, },
}; }
}, },
}, },
{ {
url: "/api/post", url: '/api/post',
method: "post", method: 'post',
timeout: 2000, timeout: 2000,
response: { response: {
code: 0, code: 0,
data: { data: {
name: "hello world", name: 'hello world',
}, },
}, },
}, },
{
url: '/api/500',
method: 'get',
statusCode: 500,
response: {
code: 500,
message: '内部错误',
data: null,
},
},
] ]

View File

@ -13,7 +13,7 @@
"build": "vite build", "build": "vite build",
"build:mock": "vite build --mode mock", "build:mock": "vite build --mode mock",
"serve": "vite preview", "serve": "vite preview",
"lint": "eslint --ext .js,.vue src" "lint": "eslint --fix --ext .js,.vue src"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

9
src/api/test.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
// 测试
export const TestError = () => {
return request({
url: '/api/500',
method: 'get',
})
}

1
src/assets/svg/bug.svg Normal file
View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,96 @@
<template>
<div v-if="errorLogs.length > 0">
<el-badge
:is-dot="true"
style="line-height: 25px;margin-top: -5px;"
@click="dialogTableVisible = true"
>
<el-button style="padding: 8px 10px;" size="small" type="danger">
<svg-icon name="bug" />
</el-button>
</el-badge>
<el-dialog v-model="dialogTableVisible" width="80%">
<template #title>
<span style="padding-right: 10px;">Error Log</span>
<el-button
size="mini"
type="primary"
icon="el-icon-delete"
@click="clearAll"
>Clear All</el-button
>
</template>
<el-table :data="errorLogs" border>
<el-table-column label="Message">
<template v-slot="{ row }">
<div>
<span class="message-title">Msg:</span>
<el-tag type="danger">
{{ row.err.message }}
</el-tag>
</div>
<br />
<div>
<span class="message-title" style="padding-right: 10px;"
>Info:
</span>
<el-tag type="warning">
<!-- {{ row.vm.$vnode.tag }} error in {{ row.info }} -->
</el-tag>
</div>
<br />
<div>
<span class="message-title" style="padding-right: 16px;"
>Url:
</span>
<el-tag type="success">
{{ row.url }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="Stack">
<template v-slot="scope">
{{ scope.row.err.stack }}
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'ErrorLog',
data() {
return {
dialogTableVisible: false,
}
},
computed: {
errorLogs() {
const logs = this.$store.state.errorLog.logs
logs.forEach(log => {
console.log(11, log.vm)
})
return logs
},
},
methods: {
clearAll() {
this.dialogTableVisible = false
this.$store.dispatch('errorLog/clearErrorLog')
},
},
}
</script>
<style scoped>
.message-title {
font-size: 16px;
color: #333;
font-weight: bold;
padding-right: 8px;
}
</style>

23
src/error-log.js Normal file
View File

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

View File

@ -6,6 +6,7 @@
<breadcrumbs /> <breadcrumbs />
</div> </div>
<div class="action"> <div class="action">
<error-log class="errLog-container right-menu-item hover-effect" />
<userinfo /> <userinfo />
</div> </div>
</div> </div>
@ -16,6 +17,7 @@ import Logo from '@/layout/components/Sidebar/Logo.vue'
import Hamburger from './Hamburger.vue' import Hamburger from './Hamburger.vue'
import Breadcrumbs from './Breadcrumbs.vue' import Breadcrumbs from './Breadcrumbs.vue'
import Userinfo from './Userinfo.vue' import Userinfo from './Userinfo.vue'
import ErrorLog from '@/components/ErrorLog/index.vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
export default defineComponent({ export default defineComponent({
@ -24,6 +26,7 @@ export default defineComponent({
Hamburger, Hamburger,
Breadcrumbs, Breadcrumbs,
Userinfo, Userinfo,
ErrorLog,
}, },
setup() { setup() {
const store = useStore() const store = useStore()

View File

@ -28,6 +28,10 @@ Object.entries(Components).forEach(([key, component]) => {
app.component(key, component) app.component(key, component)
}) })
// 错误日志
import useErrorHandler from './error-log'
useErrorHandler(app)
app app
.use(ElementPlus, { .use(ElementPlus, {
locale, locale,

View File

@ -8,6 +8,7 @@ const NestPage1 = () => import('@/views/test/nest/Page1.vue')
const NestPage2 = () => import('@/views/test/nest/Page2.vue') const NestPage2 = () => import('@/views/test/nest/Page2.vue')
const Iscache = () => import('@/views/test/Cache.vue') const Iscache = () => import('@/views/test/Cache.vue')
const Nocache = () => import('@/views/test/Nocache.vue') const Nocache = () => import('@/views/test/Nocache.vue')
const ErrorLog = () => import('@/views/test/error-log/index.vue')
export default [ export default [
{ {
@ -77,6 +78,15 @@ export default [
noCache: true, // 不缓存页面 noCache: true, // 不缓存页面
}, },
}, },
{
path: 'error-log',
name: 'test-error-log',
component: ErrorLog,
meta: {
title: '测试错误日志',
roles: ['admin', 'visitor'],
},
},
{ {
path: 'nest', path: 'nest',
name: 'nest', name: 'nest',

View File

@ -1,15 +1,13 @@
//index.js //index.js
import { createStore } from 'vuex' import { createStore } from 'vuex'
import app from './modules/app'
import account from './modules/account' const modulesFiles = import.meta.globEager('./modules/*.js')
import menu from './modules/menu' const modules = Object.entries(modulesFiles).reduce((modules, [path, mod]) => {
import tags from './modules/tags' const moduleName = path.replace(/^\.\/modules\/(.*)\.\w+$/, '$1')
modules[moduleName] = mod.default
return modules
}, {})
export default createStore({ export default createStore({
modules: { modules,
app,
account,
menu,
tags,
},
}) })

View File

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

355
src/utils/index.js Normal file
View File

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

44
src/utils/open-window.js Normal file
View File

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

69
src/utils/scroll-to.js Normal file
View File

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

83
src/utils/validate.js Normal file
View File

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

View File

@ -0,0 +1,13 @@
<template>
<div>
<!--error code-->
{{ a.a }}
<!--error code-->
</div>
</template>
<script>
export default {
name: 'ErrorTestA',
}
</script>

View File

@ -0,0 +1,11 @@
<template>
<div />
</template>
<script>
export default {
created() {
this.b = b // eslint-disable-line
},
}
</script>

View File

@ -0,0 +1,26 @@
<template>
<div class="errPage-container">
<ErrorA />
<ErrorB />
<h3>这个页面是测试错误上报功能的</h3>
<div>
本页面主动抛出了错误你现在可以点击右上角的`debugger`图标查看错误日志
</div>
</div>
</template>
<script>
import ErrorA from './components/ErrorTestA.vue'
import ErrorB from './components/ErrorTestB.vue'
export default {
name: 'test-error-log',
components: { ErrorA, ErrorB },
}
</script>
<style scoped>
.errPage-container {
padding: 30px;
}
</style>