first commit

This commit is contained in:
2024-04-08 21:34:02 +08:00
commit a41a7f32ab
223 changed files with 44629 additions and 0 deletions

19
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,19 @@
const TOKEN_KEY = 'token'
const isLogin = () => {
return !!localStorage.getItem(TOKEN_KEY)
}
const getToken = () => {
return localStorage.getItem(TOKEN_KEY)
}
const setToken = (token: string) => {
localStorage.setItem(TOKEN_KEY, token)
}
const clearToken = () => {
localStorage.removeItem(TOKEN_KEY)
}
export { isLogin, getToken, setToken, clearToken }

16
src/utils/avatar.ts Normal file
View File

@@ -0,0 +1,16 @@
import Unknown from '../assets/images/avatar/unknown.png'
import Male from '../assets/images/avatar/male.png'
import Female from '../assets/images/avatar/female.png'
export default function getAvatar(avatar: string | undefined, gender: number | undefined) {
if (avatar) {
return avatar
}
if (gender === 1) {
return Male
}
if (gender === 2) {
return Female
}
return Unknown
}

76
src/utils/downloadFile.ts Normal file
View File

@@ -0,0 +1,76 @@
/**
* 根据文件url获取文件名
* @param url 文件url
*/
function getFileName(url: string) {
const num = url.lastIndexOf('/') + 1
let fileName = url.substring(num)
// 把参数和文件名分割开
fileName = decodeURI(fileName.split('?')[0])
return fileName
}
/**
* 根据文件地址下载文件
* @param {*} sUrl
*/
export function downloadByUrl({
url,
target = '_blank',
fileName
}: {
url: string
target?: '_self' | '_blank'
fileName?: string
}): Promise<boolean> {
// 是否同源
const isSameHost = new URL(url).host == location.host
return new Promise<boolean>((resolve, reject) => {
if (isSameHost) {
const link = document.createElement('a')
link.href = url
link.target = target
if (link.download !== undefined) {
link.download = fileName || getFileName(url)
}
if (document.createEvent) {
const e = document.createEvent('MouseEvents')
e.initEvent('click', true, true)
link.dispatchEvent(e)
return resolve(true)
}
if (url.indexOf('?') === -1) {
url += '?download'
}
window.open(url, target)
return resolve(true)
} else {
const canvas = document.createElement('canvas')
const img = document.createElement('img')
img.setAttribute('crossOrigin', 'Anonymous')
img.src = url
img.onload = () => {
canvas.width = img.width
canvas.height = img.height
const context = canvas.getContext('2d')!
context.drawImage(img, 0, 0, img.width, img.height)
// window.navigator.msSaveBlob(canvas.msToBlob(),'image.jpg');
// saveAs(imageDataUrl, '附件');
canvas.toBlob((blob) => {
const link = document.createElement('a')
if (!blob) return
link.href = window.URL.createObjectURL(blob)
link.download = getFileName(url)
link.click()
URL.revokeObjectURL(link.href)
resolve(true)
}, 'image/jpeg')
}
img.onerror = (e) => reject(e)
}
})
}

26
src/utils/encrypt.ts Normal file
View File

@@ -0,0 +1,26 @@
import Base64 from 'crypto-js/enc-base64'
import UTF8 from 'crypto-js/enc-utf8'
import { JSEncrypt } from 'jsencrypt'
import md5 from 'crypto-js/md5'
export function encodeByBase64(txt: string) {
return UTF8.parse(txt).toString(Base64)
}
export function decodeByBase64(txt: string) {
return Base64.parse(txt).toString(UTF8)
}
export function encryptByMd5(txt: string) {
return md5(txt).toString()
}
const publicKey =
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9u' +
'aUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ=='
export function encryptByRsa(txt: string) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}

52
src/utils/has.ts Normal file
View File

@@ -0,0 +1,52 @@
import { useUserStore } from '@/stores'
function authPermission(permission: string) {
const all_permission = '*:*:*'
const permissions = useUserStore().permissions
if (permission && permission.length > 0) {
return permissions.some((v) => {
return all_permission === v || v === permission
})
} else {
return false
}
}
function authRole(role: string) {
const super_admin = 'role_admin'
const roles = useUserStore().roles
if (role && role.length > 0) {
return roles.some((v) => {
return super_admin === v || v === role
})
} else {
return false
}
}
export default {
/** 验证用户是否具备某权限 */
hasPerm(permission: string) {
return authPermission(permission)
},
/** 验证用户是否含有指定权限,只需包含其中一个 */
hasPermOr(permissions: string[]) {
return permissions.some((item) => authPermission(item))
},
/** 验证用户是否含有指定权限,必须全部拥有 */
hasPermAnd(permissions: string[]) {
return permissions.every((item) => authPermission(item))
},
/** 验证用户是否具备某角色 */
hasRole(role: string) {
return authRole(role)
},
/** 验证用户是否含有指定角色,只需包含其中一个 */
hasRoleOr(roles: string[]) {
return roles.some((item) => authRole(item))
},
/** 验证用户是否含有指定角色,必须全部拥有 */
hasRoleAnd(roles: string[]) {
return roles.every((item) => authRole(item))
}
}

169
src/utils/http.ts Normal file
View File

@@ -0,0 +1,169 @@
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import modalErrorWrapper from '@/utils/modal-error-wrapper'
import messageErrorWrapper from '@/utils/message-error-wrapper'
import notificationErrorWrapper from '@/utils/notification-error-wrapper'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import router from '@/router'
import qs from 'query-string'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
interface ICodeMessage {
[propName: number]: string
}
const StatusCodeMessage: ICodeMessage = {
200: '服务器成功返回请求的数据',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)',
204: '删除数据成功',
400: '请求错误(400)',
401: '未授权,请重新登录(401)',
403: '拒绝访问(403)',
404: '请求出错(404)',
408: '请求超时(408)',
500: '服务器错误(500)',
501: '服务未实现(501)',
502: '网络错误(502)',
503: '服务不可用(503)',
504: '网络超时(504)'
}
const http: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_PREFIX,
timeout: 30 * 1000
})
// 请求拦截器
http.interceptors.request.use(
(config: AxiosRequestConfig) => {
NProgress.start() // 进度条
const token = getToken()
if (token) {
if (!config.headers) {
config.headers = {}
}
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
http.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
const { success, code, msg } = data
// 成功
if (success) {
NProgress.done()
return response
}
// Token 失效
if (code === 401 && response.config.url !== '/auth/user/info') {
modalErrorWrapper({
title: '提示',
content: msg,
maskClosable: false,
escToClose: false,
okText: '重新登录',
async onOk() {
NProgress.done()
const userStore = useUserStore()
userStore.logoutCallBack()
router.replace('/login')
}
})
} else {
NProgress.done()
// 如果错误信息长度过长,使用 Notification 进行提示
if (msg.length <= 15) {
messageErrorWrapper({
content: msg || '服务器端错误',
duration: 5 * 1000
})
} else {
notificationErrorWrapper(msg || '服务器端错误')
}
}
return Promise.reject(new Error(msg || '服务器端错误'))
},
(error) => {
NProgress.done()
const response = Object.assign({}, error.response)
response &&
messageErrorWrapper({
content: StatusCodeMessage[response.status] || '系统异常,请检查网络或联系管理员',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
const request = <T = unknown>(config: AxiosRequestConfig): Promise<ApiRes<T>> => {
return new Promise((resolve, reject) => {
http
.request<T>(config)
.then((res: AxiosResponse) => resolve(res.data))
.catch((err: { message: string }) => reject(err))
})
}
const get = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'get',
url,
params,
paramsSerializer: (obj) => {
return qs.stringify(obj)
},
...config
})
}
const post = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'post',
url,
data: params,
...config
})
}
const put = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'put',
url,
data: params,
...config
})
}
const patch = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'patch',
url,
data: params,
...config
})
}
const del = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'delete',
url,
data: params,
...config
})
}
export default { get, post, put, patch, del }

244
src/utils/index.ts Normal file
View File

@@ -0,0 +1,244 @@
import { isExternal } from "@/utils/validate";
import { browse, mapTree } from "xe-utils";
import _ from "lodash";
export function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
/**
* @desc 去除空格
* @param {string} str - 字符串
* @param {string} pos - 去除空格的位置
* pos="both": 去除两边空格
* pos="left": 去除左边空格
* pos="right": 去除右边空格
* pos="all": 去除所有空格 */
type Pos = 'both' | 'left' | 'right' | 'all'
export function trim(str: string, pos: Pos = 'both'): string {
if (pos == 'both') {
return str.replace(/^\s+|\s+$/g, '')
} else if (pos == 'left') {
return str.replace(/^\s*/, '')
} else if (pos == 'right') {
return str.replace(/(\s*$)/g, '')
} else if (pos == 'all') {
return str.replace(/\s+/g, '')
} else {
return str
}
}
/**
* 根据数字获取对应的汉字
* @param {number} num - 数字(0-10) */
export function getHanByNumber(num: number): string {
const str = '零一二三四五六七八九十'
return str.charAt(num)
}
/**
* 获取指定整数范围内的随机整数
* @param {number} start - 开始范围
* @param {number} end - 结束范围 */
export function getRandomInterger(start = 0, end: number): number {
const range = end - start
return Math.floor(Math.random() * range + start)
}
/** @desc 千分位格式化 */
export function formatMoney(money: string) {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}
/** @desc 数据类型检测方法 */
export function getTypeOf(value: any) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
}
/**
* @desc 格式化电话号码
* @demo 183-7983-6654 */
export function formatPhone(mobile: string, formatStr = '-') {
return mobile.replace(/(?=(\d{4})+$)/g, formatStr)
}
/**
* @desc 手机号脱敏
* @demo 155****8810 */
export function hidePhone(phone: string) {
return phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
}
/** @desc 检测数据是否为空数据 */
export function isEmpty(data: unknown) {
if (data === '' || data === 'undefined' || data === undefined || data === null || data === 'null') {
return true
}
return JSON.stringify(data) == '{}' || JSON.stringify(data) == '[]' || JSON.stringify(data) == '[{}]';
}
/**
* @desc 大小写转换
* @param {string} str 待转换的字符串
* @param {number} type 1:全大写 2:全小写 3:首字母大写 */
export function toCase(str: string, type: number) {
switch (type) {
case 1:
return str.toUpperCase()
case 2:
return str.toLowerCase()
case 3:
return str[0].toUpperCase() + str.substring(1).toLowerCase()
default:
return str
}
}
/**
* @desc 获取随机数
* @param {number} min 最小值
* @param {number} max 最大值
* */
export const randomNum = (min: number, max: number) => {
return Math.floor(min + Math.random() * (max + 1 - min))
}
/**
* @desc 获取最大值 */
export const max = (arr: number[]) => {
return Math.max.apply(null, arr)
}
/**
* @desc 获取最小值 */
export const min = (arr: number[]) => {
return Math.min.apply(null, arr)
}
/**
* @desc 求和 */
export const sum = (arr: number[]) => {
return arr.reduce((pre, cur) => pre + cur)
}
/**
* @desc 获取平均值 */
export const average = (arr: number[]) => {
return sum(arr) / arr.length
}
/**
* @desc 深拷贝 */
export const deepClone = (data: any) => {
if (typeof data !== 'object' || data === null) return '不是对象'
const newData: any = Array.isArray(data) ? [] : {}
for (const key in data) {
newData[key] = typeof data[key] === 'object' ? deepClone(data[key]) : data[key]
}
return newData
}
/**
* @desc 判断是否是闰年
* @param {number} year 年份 */
export const isLeapYear = (year: number) => {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}
/**
* @desc 判断是否是奇数
* @param {number} num 数字 */
export const isOdd = (num: number) => {
return num % 2 !== 0
}
/**
* @desc 判断是否是偶数
* @param {number} num 数字 */
export const isEven = (num: number) => {
return !isOdd(num)
}
/**
* @desc 将RGB转化为十六机制 */
export const rgbToHex = (r: number, g: number, b: number) => {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
}
/**
* @desc 获取随机十六进制颜色 */
export const randomHex = () => {
return `#${Math.floor(Math.random() * 0xffffff)
.toString(16)
.padEnd(6, '0')}`
}
/**
* @description 动态路由 path 转 name
* @demo /system => System
* @demo /system/menu => SystemMenu
* @demo /data-manage/detail => DataManageDetail
*/
export const transformPathToName = (path: string) => {
if (!path) return ''
if (isExternal(path)) return ''
return _.upperFirst(_.camelCase(path))
}
/**
* @desc 过滤树
* @param { values } 数组
*/
type FilterTree = <T extends { children?: T[] }>(
array: T[],
iterate: (item: T, index?: number, items?: T[]) => boolean
) => T[]
export const filterTree: FilterTree = (values, fn) => {
const arr = values.filter(fn)
const data = mapTree(arr, (item) => {
if (item.children && item.children.length) {
item.children = item.children.filter(fn)
}
return item
})
return data
}
type SortTree = <T extends { sort: number; children?: T[] }>(array: T[]) => T[]
/**
* @desc 排序树
* @param values /
*/
export const sortTree: SortTree = (values) => {
values?.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0)) // 排序
return mapTree(values, (item) => {
item.children?.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0)) // 排序
return item
})
}
/** @desc 是否是h5环境 */
export const isMobile = () => {
return browse().isMobile
}
/** @desc 问候 */
export function goodTimeText() {
const time = new Date()
const hour = time.getHours()
return hour < 9 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour <= 18 ? '下午好' : '晚上好'
}
/** @desc 格式化文件大小 */
export const formatFileSize = (fileSize: number) => {
if (fileSize == null || fileSize === 0) {
return '0 Bytes'
}
const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
let index = 0
const srcSize = parseFloat(fileSize.toString())
index = Math.floor(Math.log(srcSize) / Math.log(1024))
const size = srcSize / 1024 ** index
return `${size.toFixed(2)} ${unitArr[index]}`
}

View File

@@ -0,0 +1,11 @@
import { Message, type MessageReturn } from '@arco-design/web-vue'
let messageInstance: MessageReturn | null
const messageErrorWrapper = (options: any) => {
if (messageInstance) {
messageInstance.close()
}
messageInstance = Message.error(options)
}
export default messageErrorWrapper

11
src/utils/mitt.ts Normal file
View File

@@ -0,0 +1,11 @@
import mitt from 'mitt'
type Events = {
// 自定义事件名称
event: void
// 任意传递的参数
[parmas: string]: any
}
const mittBus = mitt<Events>()
export default mittBus

View File

@@ -0,0 +1,11 @@
import { Modal, type ModalReturn } from '@arco-design/web-vue'
let modalInstance: ModalReturn | null
const modalErrorWrapper = (options: any) => {
if (modalInstance) {
modalInstance.close()
}
modalInstance = Modal.error(options)
}
export default modalErrorWrapper

View File

@@ -0,0 +1,11 @@
import { Notification, type NotificationReturn } from '@arco-design/web-vue'
let notificationInstance: NotificationReturn | null
const notificationErrorWrapper = (options: any) => {
if (notificationInstance) {
notificationInstance.close()
}
notificationInstance = Notification.error(options)
}
export default notificationErrorWrapper

31
src/utils/regexp.ts Normal file
View File

@@ -0,0 +1,31 @@
/** @desc 正则-手机号码 */
export const Phone = /^1[3-9]\d{9}$/
/** @desc 正则-邮箱 */
export const Email = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
/** @desc 正则-密码(密码为8-18位数字/字符/符号的组合) */
// export const Password =
// /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/
/** @desc 正则-密码(密码为6位数字) */
export const Password = /^\d{6}$/
/** @desc 正则-6位数字验证码正则 */
export const Code_6 = /^\d{6}$/
/** @desc 正则-4位数字验证码正则 */
export const Code_4 = /^\d{4}$/
/** @desc 正则-url链接 */
export const Url =
/(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
/** @desc 正则-16进颜色值 #333 #8c8c8c */
export const ColorRegex = /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/
/** @desc 正则-只能是中文 */
export const OnlyCh = /^[\u4e00-\u9fa5]+$/gi
/** @desc 正则-只能是英文 */
export const OnlyEn = /^[a-zA-Z]*$/

45
src/utils/typeof.ts Normal file
View File

@@ -0,0 +1,45 @@
/** 判断变量类型 */
export function isNumber(value: unknown) {
return Object.prototype.toString.call(value) === '[object Number]'
}
export function isString(value: unknown) {
return Object.prototype.toString.call(value) === '[object String]'
}
export function isBoolean(value: unknown) {
return Object.prototype.toString.call(value) === '[object Boolean]'
}
export function isNull(value: unknown) {
return Object.prototype.toString.call(value) === '[object Null]'
}
export function isUndefined(value: unknown) {
return Object.prototype.toString.call(value) === '[object Undefined]'
}
export function isObject(value: unknown) {
return Object.prototype.toString.call(value) === '[object Object]'
}
export function isArray(value: unknown) {
return Object.prototype.toString.call(value) === '[object Array]'
}
export function isDate(data: unknown) {
return Object.prototype.toString.call(data) === '[object Date]'
}
export function isRegExp(value: unknown) {
return Object.prototype.toString.call(value) === '[object RegExp]'
}
export function isSet(value: unknown) {
return Object.prototype.toString.call(value) === '[object Set]'
}
export function isMap(value: unknown) {
return Object.prototype.toString.call(value) === '[object Map]'
}

10
src/utils/validate.ts Normal file
View File

@@ -0,0 +1,10 @@
/** 判断 path 是否为外链 */
export const isExternal = (path: string) => {
const reg = /^(https?:|mailto:|tel:)/
return reg.test(path)
}
/** 判断 url 是否是 http 或 https */
export function isHttp(url: string) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}