前端代码
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
const viewModules = import.meta.glob('../view/**/*.vue')
|
||||
const moduleViews = import.meta.glob('../modules/**/*.vue')
|
||||
|
||||
export const asyncRouterHandle = (asyncRouter) => {
|
||||
asyncRouter.forEach((item) => {
|
||||
if (item.component && typeof item.component === 'string') {
|
||||
item.meta.path = '/src/' + item.component
|
||||
if (item.component.split('/')[0] === 'view') {
|
||||
item.component = dynamicImport(viewModules, item.component)
|
||||
} else if (item.component.split('/')[0] === 'modules') {
|
||||
item.component = dynamicImport(moduleViews, item.component)
|
||||
}
|
||||
}
|
||||
if (item.children) {
|
||||
asyncRouterHandle(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function dynamicImport(dynamicViewsModules, component) {
|
||||
const keys = Object.keys(dynamicViewsModules)
|
||||
const matchKeys = keys.filter((key) => {
|
||||
const k = key.replace('../', '')
|
||||
return k === component
|
||||
})
|
||||
const matchKey = matchKeys[0]
|
||||
const matched = dynamicViewsModules[matchKey]
|
||||
// 找不到组件时返回占位组件,避免 component 为 undefined 导致路由白屏。
|
||||
if (!matched) {
|
||||
console.warn(`[asyncRouter] 未找到组件: ${component},已使用占位组件代替`)
|
||||
return {
|
||||
name: 'MissingComponentPlaceholder',
|
||||
render: () => null
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { useRoute } from 'vue-router'
|
||||
import { reactive } from 'vue'
|
||||
export const useBtnAuth = () => {
|
||||
const route = useRoute()
|
||||
return route.meta.btns || reactive({})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// using ES6 modules
|
||||
import mitt from 'mitt'
|
||||
|
||||
export const emitter = mitt()
|
||||
@@ -0,0 +1,5 @@
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
|
||||
export const closeThisPage = () => {
|
||||
emitter.emit('closeThisPage')
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 对Date的扩展,将 Date 转化为指定格式的String
|
||||
// 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
|
||||
// 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
|
||||
// (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
|
||||
// (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Date.prototype.Format = function(fmt) {
|
||||
const o = {
|
||||
'M+': this.getMonth() + 1, // 月份
|
||||
'd+': this.getDate(), // 日
|
||||
'h+': this.getHours(), // 小时
|
||||
'm+': this.getMinutes(), // 分
|
||||
's+': this.getSeconds(), // 秒
|
||||
'q+': Math.floor((this.getMonth() + 3) / 3), // 季度
|
||||
'S': this.getMilliseconds() // 毫秒
|
||||
}
|
||||
const reg = /(y+)/
|
||||
if (reg.test(fmt)) {
|
||||
const t = reg.exec(fmt)[1]
|
||||
fmt = fmt.replace(
|
||||
t,
|
||||
(this.getFullYear() + '').substring(4 - t.length)
|
||||
)
|
||||
}
|
||||
for (let k in o) {
|
||||
const regx = new RegExp('(' + k + ')')
|
||||
if (regx.test(fmt)) {
|
||||
const t = regx.exec(fmt)[1]
|
||||
fmt = fmt.replace(
|
||||
t,
|
||||
t.length === 1 ? o[k] : ('00' + o[k]).substring(('' + o[k]).length)
|
||||
)
|
||||
}
|
||||
}
|
||||
return fmt
|
||||
}
|
||||
|
||||
export function formatTimeToStr(times, pattern) {
|
||||
let d = new Date(times).Format('yyyy-MM-dd hh:mm:ss')
|
||||
if (pattern) {
|
||||
d = new Date(times).Format(pattern)
|
||||
}
|
||||
return d.toLocaleString()
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useDictionaryStore } from '@/pinia/modules/dictionary'
|
||||
|
||||
/**
|
||||
* 生成字典缓存key
|
||||
* @param {string} type - 字典类型
|
||||
* @param {number} depth - 深度参数
|
||||
* @param {string|number|null} value - 指定节点的value
|
||||
* @returns {string} 缓存key
|
||||
*/
|
||||
const generateCacheKey = (type, depth, value) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
return `${type}_value_${value}_depth_${depth}`
|
||||
}
|
||||
return depth === 0 ? `${type}_tree` : `${type}_depth_${depth}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
* @param {string} type - 字典类型,必填
|
||||
* @param {Object} options - 可选参数
|
||||
* @param {number} options.depth - 指定获取字典的深度,默认为0(完整树形结构)
|
||||
* @param {string|number|null} options.value - 指定节点的value,获取该节点的children,默认为null
|
||||
* @returns {Promise<Array>} 字典数据数组
|
||||
* @example
|
||||
* // 获取完整的字典树形结构
|
||||
* const dictTree = await getDict('user_status')
|
||||
*
|
||||
* // 获取指定深度的扁平化字典数据
|
||||
* const dictFlat = await getDict('user_status', {
|
||||
* depth: 2
|
||||
* })
|
||||
*
|
||||
* // 获取指定节点的children
|
||||
* const children = await getDict('user_status', {
|
||||
* value: 'active'
|
||||
* })
|
||||
*/
|
||||
export const getDict = async (
|
||||
type,
|
||||
options = {
|
||||
depth: 0,
|
||||
value: null
|
||||
}
|
||||
) => {
|
||||
// 参数验证
|
||||
if (!type || typeof type !== 'string') {
|
||||
console.warn('getDict: type参数必须是非空字符串')
|
||||
return []
|
||||
}
|
||||
|
||||
if (typeof options.depth !== 'number' || options.depth < 0) {
|
||||
console.warn('getDict: depth参数必须是非负数')
|
||||
options.depth = 0
|
||||
}
|
||||
|
||||
try {
|
||||
const dictionaryStore = useDictionaryStore()
|
||||
|
||||
// 调用store方法获取字典数据
|
||||
await dictionaryStore.getDictionary(type, options.depth, options.value)
|
||||
|
||||
// 生成缓存key
|
||||
const cacheKey = generateCacheKey(type, options.depth, options.value)
|
||||
|
||||
// 从缓存中获取数据
|
||||
const result = dictionaryStore.dictionaryMap[cacheKey]
|
||||
|
||||
// 返回数据,确保返回数组
|
||||
return Array.isArray(result) ? result : []
|
||||
} catch (error) {
|
||||
console.error('getDict: 获取字典数据失败', { type, options, error })
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 字典文字展示方法
|
||||
export const showDictLabel = (
|
||||
dict,
|
||||
code,
|
||||
keyCode = 'value',
|
||||
valueCode = 'label'
|
||||
) => {
|
||||
if (!dict) {
|
||||
return ''
|
||||
}
|
||||
const dictMap = {}
|
||||
dict.forEach((item) => {
|
||||
if (Reflect.has(item, keyCode) && Reflect.has(item, valueCode)) {
|
||||
dictMap[item[keyCode]] = item[valueCode]
|
||||
}
|
||||
})
|
||||
return Reflect.has(dictMap, code) ? dictMap[code] : ''
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const toDoc = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export const downloadImage = (imgsrc, name) => {
|
||||
// 下载图片地址和图片名
|
||||
var image = new Image()
|
||||
image.setAttribute('crossOrigin', 'anonymous')
|
||||
image.onload = function () {
|
||||
var canvas = document.createElement('canvas')
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
var context = canvas.getContext('2d')
|
||||
context.drawImage(image, 0, 0, image.width, image.height)
|
||||
var url = canvas.toDataURL('image/png') // 得到图片的base64编码数据
|
||||
|
||||
var a = document.createElement('a') // 生成一个a元素
|
||||
var event = new MouseEvent('click') // 创建一个单击事件
|
||||
a.download = name || 'photo' // 设置图片名称
|
||||
a.href = url // 将生成的URL设置为a.href属性
|
||||
a.dispatchEvent(event) // 触发a的单击事件
|
||||
}
|
||||
image.src = imgsrc
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const isDev = import.meta.env.DEV;
|
||||
|
||||
export const isProd = import.meta.env.PROD;
|
||||
@@ -0,0 +1,17 @@
|
||||
export function addEventListen(target, event, handler, capture = false) {
|
||||
if (
|
||||
target.addEventListener &&
|
||||
typeof target.addEventListener === 'function'
|
||||
) {
|
||||
target.addEventListener(event, handler, capture)
|
||||
}
|
||||
}
|
||||
|
||||
export function removeEventListen(target, event, handler, capture = false) {
|
||||
if (
|
||||
target.removeEventListener &&
|
||||
typeof target.removeEventListener === 'function'
|
||||
) {
|
||||
target.removeEventListener(event, handler, capture)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export const fmtTitle = (title, now) => {
|
||||
const reg = /\$\{(.+?)\}/
|
||||
const reg_g = /\$\{(.+?)\}/g
|
||||
const result = title.match(reg_g)
|
||||
if (result) {
|
||||
result.forEach((item) => {
|
||||
const key = item.match(reg)[1]
|
||||
const value = now.params[key] || now.query[key]
|
||||
title = title.replace(item, value)
|
||||
})
|
||||
}
|
||||
return title
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { formatTimeToStr } from '@/utils/date'
|
||||
import { getDict } from '@/utils/dictionary'
|
||||
import { ref } from 'vue'
|
||||
import { getUrl } from './image'
|
||||
|
||||
export const formatBoolean = (bool) => {
|
||||
if (bool !== null) {
|
||||
return bool ? '是' : '否'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
export const formatDate = (time) => {
|
||||
if (time !== null && time !== '') {
|
||||
var date = new Date(time)
|
||||
return formatTimeToStr(date, 'yyyy-MM-dd hh:mm:ss')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const filterDict = (value, options) => {
|
||||
// 递归查找函数
|
||||
const findInOptions = (opts, targetValue) => {
|
||||
if (!opts || !Array.isArray(opts)) return null
|
||||
|
||||
for (const item of opts) {
|
||||
if (item.value === targetValue) {
|
||||
return item
|
||||
}
|
||||
|
||||
if (item.children && Array.isArray(item.children)) {
|
||||
const found = findInOptions(item.children, targetValue)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const rowLabel = findInOptions(options, value)
|
||||
return rowLabel && rowLabel.label
|
||||
}
|
||||
|
||||
export const filterDataSource = (dataSource, value) => {
|
||||
// 递归查找函数
|
||||
const findInDataSource = (data, targetValue) => {
|
||||
if (!data || !Array.isArray(data)) return null
|
||||
|
||||
for (const item of data) {
|
||||
// 检查当前项是否匹配
|
||||
if (item.value === targetValue) {
|
||||
return item
|
||||
}
|
||||
|
||||
// 如果有children属性,递归查找
|
||||
if (item.children && Array.isArray(item.children)) {
|
||||
const found = findInDataSource(item.children, targetValue)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => {
|
||||
const rowLabel = findInDataSource(dataSource, item)
|
||||
return rowLabel?.label
|
||||
})
|
||||
}
|
||||
|
||||
const rowLabel = findInDataSource(dataSource, value)
|
||||
return rowLabel?.label
|
||||
}
|
||||
|
||||
export const getDictFunc = async (type) => {
|
||||
const dicts = await getDict(type)
|
||||
return dicts
|
||||
}
|
||||
|
||||
export const ReturnArrImg = (arr) => {
|
||||
const imgArr = []
|
||||
if (arr instanceof Array) {
|
||||
// 如果是数组类型
|
||||
for (const arrKey in arr) {
|
||||
imgArr.push(getUrl(arr[arrKey]))
|
||||
}
|
||||
} else {
|
||||
imgArr.push(getUrl(arr))
|
||||
}
|
||||
return imgArr
|
||||
}
|
||||
|
||||
export const returnArrImg = ReturnArrImg
|
||||
|
||||
export const onDownloadFile = (url) => {
|
||||
window.open(getUrl(url))
|
||||
}
|
||||
|
||||
const baseUrl = ref(import.meta.env.VITE_BASE_API)
|
||||
|
||||
export const getBaseUrl = () => {
|
||||
return baseUrl.value === '/' ? '' : baseUrl.value
|
||||
}
|
||||
|
||||
export const CreateUUID = () => {
|
||||
let d = new Date().getTime()
|
||||
if (window.performance && typeof window.performance.now === 'function') {
|
||||
d += performance.now()
|
||||
}
|
||||
return '00000000-0000-0000-0000-000000000000'.replace(/0/g, (c) => {
|
||||
const r = (d + Math.random() * 16) % 16 | 0 // d是随机种子
|
||||
d = Math.floor(d / 16)
|
||||
return (c === '0' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
export default class ImageCompress {
|
||||
constructor(file, fileSize, maxWH = 1920) {
|
||||
this.file = file
|
||||
this.fileSize = fileSize
|
||||
this.maxWH = maxWH // 最大长宽
|
||||
}
|
||||
|
||||
compress() {
|
||||
// 压缩
|
||||
const fileType = this.file.type
|
||||
const fileSize = this.file.size / 1024
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(this.file)
|
||||
reader.onload = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const img = document.createElement('img')
|
||||
img.src = reader.result
|
||||
img.onload = () => {
|
||||
const ctx = canvas.getContext('2d')
|
||||
const _dWH = this.dWH(img.width, img.height, this.maxWH)
|
||||
canvas.width = _dWH.width
|
||||
canvas.height = _dWH.height
|
||||
|
||||
// 清空后, 重写画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
const newImgData = canvas.toDataURL(fileType, 0.9)
|
||||
|
||||
// 压缩宽高后的图像大小
|
||||
const newImgSize = this.fileSizeKB(newImgData)
|
||||
|
||||
if (newImgSize > this.fileSize) {
|
||||
console.log('图片尺寸太大!' + fileSize + ' >> ' + newImgSize)
|
||||
}
|
||||
|
||||
const blob = this.dataURLtoBlob(newImgData, fileType)
|
||||
const nfile = new File([blob], this.file.name)
|
||||
resolve(nfile)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 长宽等比缩小
|
||||
* 图像的一边(长或宽)为最大目标值
|
||||
*/
|
||||
dWH(srcW, srcH, dMax) {
|
||||
const defaults = {
|
||||
width: srcW,
|
||||
height: srcH
|
||||
}
|
||||
if (Math.max(srcW, srcH) > dMax) {
|
||||
if (srcW > srcH) {
|
||||
defaults.width = dMax
|
||||
defaults.height = Math.round(srcH * (dMax / srcW))
|
||||
return defaults
|
||||
} else {
|
||||
defaults.height = dMax
|
||||
defaults.width = Math.round(srcW * (dMax / srcH))
|
||||
return defaults
|
||||
}
|
||||
} else {
|
||||
return defaults
|
||||
}
|
||||
}
|
||||
|
||||
fileSizeKB(dataURL) {
|
||||
let sizeKB = 0
|
||||
sizeKB = Math.round((dataURL.split(',')[1].length * 3) / 4 / 1024)
|
||||
return sizeKB
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为Blob
|
||||
*/
|
||||
dataURLtoBlob(dataURL, fileType) {
|
||||
const byteString = atob(dataURL.split(',')[1])
|
||||
let mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0]
|
||||
const ab = new ArrayBuffer(byteString.length)
|
||||
const ia = new Uint8Array(ab)
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i)
|
||||
}
|
||||
if (fileType) {
|
||||
mimeString = fileType
|
||||
}
|
||||
return new Blob([ab], { type: mimeString, lastModifiedDate: new Date() })
|
||||
}
|
||||
}
|
||||
|
||||
const path = import.meta.env.VITE_FILE_API
|
||||
export const getUrl = (url) => {
|
||||
if (url && url.slice(0, 4) !== 'http') {
|
||||
if (path === '/') {
|
||||
return url
|
||||
}
|
||||
if (url.slice(0, 1) === '/') {
|
||||
return path + url
|
||||
}
|
||||
return path + '/' + url
|
||||
} else {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const VIDEO_EXTENSIONS = ['.mp4', '.mov', '.webm', '.ogg']
|
||||
const VIDEO_MIME_TYPES = ['video/mp4', 'video/webm', 'video/ogg']
|
||||
const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml']
|
||||
|
||||
export const isVideoExt = (url) => {
|
||||
const urlLower = url?.toLowerCase() || ''
|
||||
return urlLower !== '' && VIDEO_EXTENSIONS.some(ext => urlLower.endsWith(ext))
|
||||
}
|
||||
|
||||
export const isVideoMime = (type) => {
|
||||
const typeLower = type?.toLowerCase() || ''
|
||||
return typeLower !== '' && VIDEO_MIME_TYPES.includes(typeLower)
|
||||
}
|
||||
|
||||
export const isImageMime = (type) => {
|
||||
const typeLower = type?.toLowerCase() || ''
|
||||
return typeLower !== '' && IMAGE_MIME_TYPES.includes(typeLower)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { fmtTitle } from '@/utils/fmtRouterTitle'
|
||||
import config from '@/core/config'
|
||||
export default function getPageTitle(pageTitle, route) {
|
||||
if (pageTitle) {
|
||||
const title = fmtTitle(pageTitle, route)
|
||||
return `${title} - ${config.appName}`
|
||||
}
|
||||
return `${config.appName}`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useParamsStore } from '@/pinia/modules/params'
|
||||
/*
|
||||
* 获取参数方法 使用示例 getParams('key').then(res) 或者 async函数下 const res = await getParams('key')
|
||||
* const res = ref('')
|
||||
* const fun = async () => {
|
||||
* res.value = await getParams('test')
|
||||
* }
|
||||
* fun()
|
||||
*/
|
||||
export const getParams = async(key) => {
|
||||
const paramsStore = useParamsStore()
|
||||
await paramsStore.getParams(key)
|
||||
return paramsStore.paramsMap[key]
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import axios from 'axios'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { emitter } from '@/utils/bus'
|
||||
import { isPasswordChangeRequiredError } from '@/utils/requestError'
|
||||
import router from '@/router/index'
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT = 1000 * 60 * 10
|
||||
const DEFAULT_LOADING_FORCE_CLOSE_DELAY = 30000
|
||||
|
||||
const service = axios.create()
|
||||
|
||||
let activeAxios = 0
|
||||
let persistentLoadingCount = 0
|
||||
let timer = null
|
||||
let forceCloseTimer = null
|
||||
let loadingInstance = null
|
||||
let isLoadingVisible = false
|
||||
|
||||
const clearLoadingTimers = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
if (forceCloseTimer) {
|
||||
clearTimeout(forceCloseTimer)
|
||||
forceCloseTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const closeLoadingInstance = () => {
|
||||
if (isLoadingVisible && loadingInstance) {
|
||||
loadingInstance.close()
|
||||
}
|
||||
loadingInstance = null
|
||||
isLoadingVisible = false
|
||||
}
|
||||
|
||||
const scheduleForceClose = () => {
|
||||
if (!isLoadingVisible || activeAxios <= 0 || persistentLoadingCount > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
forceCloseTimer = setTimeout(() => {
|
||||
if (isLoadingVisible && loadingInstance) {
|
||||
console.warn(
|
||||
`Loading force closed after ${DEFAULT_LOADING_FORCE_CLOSE_DELAY}ms`
|
||||
)
|
||||
closeLoadingInstance()
|
||||
activeAxios = 0
|
||||
persistentLoadingCount = 0
|
||||
}
|
||||
}, DEFAULT_LOADING_FORCE_CLOSE_DELAY)
|
||||
}
|
||||
|
||||
const showLoading = (
|
||||
option = {
|
||||
target: null
|
||||
}
|
||||
) => {
|
||||
const loadDom = document.getElementById('kra-base-load-dom')
|
||||
const loadingOption = {
|
||||
target: null,
|
||||
...option
|
||||
}
|
||||
const persistLoading = Boolean(loadingOption.persistLoading)
|
||||
|
||||
delete loadingOption.persistLoading
|
||||
|
||||
activeAxios++
|
||||
if (persistLoading) {
|
||||
persistentLoadingCount++
|
||||
}
|
||||
|
||||
clearLoadingTimers()
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (activeAxios > 0 && !isLoadingVisible) {
|
||||
if (!loadingOption.target) {
|
||||
loadingOption.target = loadDom
|
||||
}
|
||||
loadingInstance = ElLoading.service(loadingOption)
|
||||
isLoadingVisible = true
|
||||
}
|
||||
|
||||
scheduleForceClose()
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const closeLoading = (option = {}) => {
|
||||
activeAxios--
|
||||
if (option?.persistLoading && persistentLoadingCount > 0) {
|
||||
persistentLoadingCount--
|
||||
}
|
||||
|
||||
if (activeAxios <= 0) {
|
||||
activeAxios = 0
|
||||
persistentLoadingCount = 0
|
||||
clearLoadingTimers()
|
||||
closeLoadingInstance()
|
||||
return
|
||||
}
|
||||
|
||||
if (forceCloseTimer) {
|
||||
clearTimeout(forceCloseTimer)
|
||||
forceCloseTimer = null
|
||||
}
|
||||
|
||||
scheduleForceClose()
|
||||
}
|
||||
|
||||
const resetLoading = () => {
|
||||
activeAxios = 0
|
||||
persistentLoadingCount = 0
|
||||
clearLoadingTimers()
|
||||
closeLoadingInstance()
|
||||
}
|
||||
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
if (typeof config.timeout === 'undefined') {
|
||||
config.timeout = DEFAULT_REQUEST_TIMEOUT
|
||||
}
|
||||
|
||||
if (!config.donNotShowLoading) {
|
||||
showLoading(config.loadingOption)
|
||||
}
|
||||
|
||||
config.baseURL = config.baseURL || import.meta.env.VITE_BASE_API
|
||||
|
||||
const userStore = useUserStore()
|
||||
config.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-token': userStore.token,
|
||||
'x-user-id': userStore.userInfo.ID,
|
||||
...config.headers
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
if (!error.config?.donNotShowLoading) {
|
||||
closeLoading(error.config?.loadingOption)
|
||||
}
|
||||
|
||||
emitter.emit('show-error', {
|
||||
code: 'request',
|
||||
message: error.message || '请求发送失败'
|
||||
})
|
||||
|
||||
return error
|
||||
}
|
||||
)
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return error.response?.data?.msg || error.response?.statusText || '请求失败'
|
||||
}
|
||||
|
||||
// 错误提示去重:批量请求瞬间失败时,避免同一条错误反复弹出刷屏
|
||||
const MAX_ERROR_MESSAGE = 3
|
||||
const activeErrorMessages = new Map()
|
||||
|
||||
const showErrorMessage = (rawMessage) => {
|
||||
const message = rawMessage || '请求失败'
|
||||
|
||||
// 相同内容正在展示时不再重复弹出
|
||||
if (activeErrorMessages.has(message)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 不同内容也做上限保护,防止瞬间大量报错铺满整个屏幕
|
||||
if (activeErrorMessages.size >= MAX_ERROR_MESSAGE) {
|
||||
return
|
||||
}
|
||||
|
||||
const instance = ElMessage({
|
||||
showClose: true,
|
||||
message,
|
||||
type: 'error',
|
||||
grouping: true,
|
||||
onClose: () => {
|
||||
activeErrorMessages.delete(message)
|
||||
}
|
||||
})
|
||||
|
||||
activeErrorMessages.set(message, instance)
|
||||
}
|
||||
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (!response.config.donNotShowLoading) {
|
||||
closeLoading(response.config.loadingOption)
|
||||
}
|
||||
|
||||
if (response.headers['new-token']) {
|
||||
userStore.setToken(response.headers['new-token'])
|
||||
}
|
||||
|
||||
if (typeof response.data.code === 'undefined') {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response.data.code === 0 || response.headers.success === 'true') {
|
||||
if (response.headers.msg) {
|
||||
response.data.msg = decodeURI(response.headers.msg)
|
||||
}
|
||||
return response.data
|
||||
}
|
||||
|
||||
showErrorMessage(response.data.msg || decodeURI(response.headers.msg))
|
||||
|
||||
return response.data.msg ? response.data : response
|
||||
},
|
||||
(error) => {
|
||||
if (!error.config?.donNotShowLoading) {
|
||||
closeLoading(error.config?.loadingOption)
|
||||
}
|
||||
|
||||
if (!error.response) {
|
||||
resetLoading()
|
||||
emitter.emit('show-error', {
|
||||
code: 'network',
|
||||
message: getErrorMessage(error)
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (error.response.status === 401) {
|
||||
emitter.emit('show-error', {
|
||||
code: '401',
|
||||
message: getErrorMessage(error),
|
||||
fn: () => {
|
||||
const userStore = useUserStore()
|
||||
userStore.ClearStorage()
|
||||
router.push({ name: 'Login', replace: true })
|
||||
}
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isPasswordChangeRequiredError(error)) {
|
||||
router.push({ name: 'ForceChangePassword', replace: true })
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
emitter.emit('show-error', {
|
||||
code: error.response.status,
|
||||
message: getErrorMessage(error)
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeunload', resetLoading)
|
||||
window.addEventListener('unload', resetLoading)
|
||||
}
|
||||
|
||||
export { resetLoading }
|
||||
export default service
|
||||
@@ -0,0 +1,4 @@
|
||||
export const PASSWORD_CHANGE_REQUIRED = 10001
|
||||
|
||||
export const isPasswordChangeRequiredError = (error) =>
|
||||
error?.response?.data?.code === PASSWORD_CHANGE_REQUIRED
|
||||
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable */
|
||||
export const toUpperCase = (str) => {
|
||||
if (str[0]) {
|
||||
return str.replace(str[0], str[0].toUpperCase())
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const toLowerCase = (str) => {
|
||||
if (str[0]) {
|
||||
return str.replace(str[0], str[0].toLowerCase())
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 驼峰转换下划线
|
||||
export const toSQLLine = (str) => {
|
||||
if (str === 'ID') return 'ID'
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||
}
|
||||
|
||||
// 下划线转换驼峰
|
||||
export const toHump = (name) => {
|
||||
return name.replace(/\_(\w)/g, function (all, letter) {
|
||||
return letter.toUpperCase()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const getUploadErrorMessage = (response) => {
|
||||
if (response?.code === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return response?.msg || '上传失败'
|
||||
}
|
||||
Reference in New Issue
Block a user