first commit

This commit is contained in:
2025-12-24 18:19:05 +08:00
commit 78407f1cbd
283 changed files with 170690 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
export default class EventDispatcher {
constructor() {
this.listeners = {}
}
/**
* 添加事件监听器
* @param {string} type - 事件类型
* @param {Function} listener - 监听函数,当事件触发时会执行此函数
*/
addEventListener(type, listener) {
if (!this.listeners[type]) {
this.listeners[type] = []
}
// 检查监听函数是否已经存在于该事件类型的监听队列中,如果不存在则添加
if (this.listeners[type].indexOf(listener) === -1) {
this.listeners[type].push(listener)
}
}
/**
* 根据事件类型移除所有的事件监听器
* @param {string} type - 事件类型
*/
removeEventListener(type) {
this.listeners[type] = []
}
/**
* 触发指定类型的事件,并向事件监听器传递数据
* @param {string} type - 事件类型
* @param {*} data - 要传递给事件监听器的数据
*/
dispatchEvent(type, data) {
const listenerArray = this.listeners[type] || []
if (listenerArray.length) {
listenerArray.forEach((listener) => {
listener.call(this, data)
})
}
}
}

View File

@@ -0,0 +1,158 @@
import EventDispatcher from '@/utils/EventDispatcher'
export default class WebSocketClient extends EventDispatcher {
url = '' // socket链接地址
socket = null // socket实例
reconnectCount = 0 // 重连次数
maxReconnectCount = 5 // 最大重连次数
reconnectTimer = null // 重连定时器
reconnectInterval = 1000 * 10 // 重连时间间隔
heartbeatInterval = 1000 * 30 // 心跳时间间隔
heartbeatTimer = null // 定时器
isStop = false // 是否停止
constructor(url) {
super()
this.url = url
}
onopen(callback) {
this.addEventListener('open', callback)
}
onmessage(callback) {
this.addEventListener('message', callback)
}
onclose(callback) {
this.addEventListener('close', callback)
}
onerror(callback) {
this.addEventListener('error', callback)
}
/**
* 消息发送
* @param {*} message
*/
send(message) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(message)
} else {
console.error('[WebSocket] 未连接')
}
}
/**
* 初始化连接
*/
connect() {
if (this.reconnectCount === 0) {
console.log('websocket', `初始化连接中...${this.url}`)
}
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
return
}
this.socket = new WebSocket(this.url)
// 连接成功
this.socket.onopen = (event) => {
this.isStop = false
// 重置重连次数
this.reconnectCount = 0
// 在连接成功时停止当前的心跳检测并重新启动
this.startHeartbeat()
console.log('WebSocket', `连接成功,等待服务端数据推送[onopen]...${this.url}`)
this.dispatchEvent('open', event)
}
this.socket.onmessage = (event) => {
this.dispatchEvent('message', event)
}
this.socket.onclose = (event) => {
if (this.reconnectCount === 0) {
console.log('websocket', `连接断开[onclose]...${this.url}`)
}
if (!this.isStop) {
this.reconnect()
}
this.closeHeartbeat()
this.dispatchEvent('close', event)
}
this.socket.onerror = (event) => {
if (this.reconnectCount === 0) {
console.log('websocket', `连接异常[onclose]...${this.url}`)
}
this.closeHeartbeat()
this.dispatchEvent('error', event)
}
}
/**
* 重新连接
*/
reconnect() {
if (this.reconnectCount < this.maxReconnectCount) {
this.reconnectCount++
console.log('WebSocket', `尝试重连(${this.reconnectCount}/${this.maxReconnectCount})...${this.url}`)
if(this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.reconnectTimer = setTimeout(() => {
this.connect()
}, this.reconnectInterval)
} else {
this.closeHeartbeat()
console.log('WebSocket', `最大重连失败,终止重连: ${this.url}`)
}
}
/**
* 关闭连接
*/
close() {
if (this.socket) {
this.isStop = true
this.socket.close()
this.socket = null
this.removeEventListener('open')
this.removeEventListener('message')
this.removeEventListener('close')
this.removeEventListener('error')
}
if(this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.closeHeartbeat()
}
/**
* 开始心跳检测
*/
startHeartbeat() {
this.closeHeartbeat()
this.heartbeatTimer = setInterval(() => {
if (this.socket) {
// this.socket.send({ type: 'heartbeat', data: {} })
// console.log('WebSocket', '发送心跳数据...')
} else {
console.log('WebSocket', '未连接')
}
}, this.heartbeatInterval)
}
/**
* 关闭心跳检测
*/
closeHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
}

78
src/utils/cache.js Normal file
View File

@@ -0,0 +1,78 @@
// 缓存数据处理
const sessionCache = {
set (key, value) {
if (!sessionStorage) {
return
}
if (key != null && value != null) {
sessionStorage.setItem(key, value)
}
},
get (key) {
if (!sessionStorage) {
return null
}
if (key == null) {
return null
}
return sessionStorage.getItem(key)
},
setJSON (key, jsonValue) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
},
remove (key) {
sessionStorage.removeItem(key)
}
}
const localCache = {
set (key, value) {
if (!localStorage) {
return
}
if (key != null && value != null) {
localStorage.setItem(key, value)
}
},
get (key) {
if (!localStorage) {
return null
}
if (key == null) {
return null
}
return localStorage.getItem(key)
},
setJSON (key, jsonValue) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
},
remove (key) {
localStorage.removeItem(key)
}
}
export default {
/**
* 会话级缓存
*/
session: sessionCache,
/**
* 本地缓存
*/
local: localCache
}

208
src/utils/common.js Normal file
View File

@@ -0,0 +1,208 @@
import { download } from '@/utils/request'
/**
* 获取assets文件夹下的静态图片资源
* @param url // 文件路径
*/
export const getAssetsFile = (url) => new URL(`../assets/images/${url}`, import.meta.url).href
// 日期格式化
export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) {
return null
}
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '')
}
if (typeof time === 'number' && time.toString().length === 10) {
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 timeStr = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') {
return [ '日', '一', '二', '三', '四', '五', '六' ][value]
}
if (result.length > 0 && value < 10) {
value = `0${value}`
}
return value || 0
})
return timeStr
}
/**
* 参数处理
* @param {*} params 参数
*/
export const tansParams = (params) => {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName]
let part = encodeURIComponent(propName) + '='
if (value !== null && value !== '' && typeof value !== 'undefined') {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
let params = propName + '[' + key + ']'
let subPart = encodeURIComponent(params) + '='
result += subPart + encodeURIComponent(value[key]) + '&'
}
}
} else {
result += part + encodeURIComponent(value) + '&'
}
}
}
return result
}
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
*/
export const handleTree = (data, id, parentId, children) => {
let config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
}
let childrenListMap = {}
let nodeIds = {}
let tree = []
for (let d of data) {
let parentId = d[config.parentId]
if (!childrenListMap[parentId]) {
childrenListMap[parentId] = []
}
nodeIds[d[config.id]] = d
childrenListMap[parentId].push(d)
}
for (let d of data) {
let parentId = d[config.parentId]
if (!nodeIds[parentId]) {
tree.push(d)
}
}
for (let t of tree) {
adaptToChildrenList(t)
}
function adaptToChildrenList(o) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]]
}
if (o[config.childrenList]) {
for (let c of o[config.childrenList]) {
adaptToChildrenList(c)
}
}
}
return tree
}
// 添加日期范围
export const addDateRange = (params, dateRange, propName) => {
let search = params
search.params = typeof search.params === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}
dateRange = Array.isArray(dateRange) ? dateRange : []
if (typeof propName === 'undefined') {
search.params.beginTime = dateRange[0]
search.params.endTime = dateRange[1]
} else {
search.params['begin' + propName] = dateRange[0]
search.params['end' + propName] = dateRange[1]
}
return search
}
// 验证是否为blob格式
export function blobValidate(data) {
return data.type !== 'application/json'
}
/**
* 判断url是否是http或https
* @param {string} url
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
export function downloadFile(url, params, filename, config, method) {
return download(url, params, filename, config, method)
}
/**
* 拖拽事件
* 添加到元素上的mousedown事件 @mousedown
*/
export function dragEvent(e, className, moving, moveEnd) {
if (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA' || e.target.nodeName === 'svg' || e.target.nodeName === 'path') {
return
}
if (className && !e.target.className.includes(className)) {
return
}
// if (className && className !== e.target.className) return
const divDom = e.target // 获取目标元素
const [ disX, disY ] = [ e.clientX - divDom.offsetLeft, e.clientY - divDom.offsetTop ]
document.onmousemove = (e) => { // 鼠标按下并移动的事件//用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
const left = e.clientX - disX
const top = e.clientY - disY
divDom.style.left = `${left}px`
divDom.style.top = `${top}px`
if (moving) {
moving()
}
}
document.onmouseup = (e) => {
document.onmousemove = null
document.onmouseup = null
if (moveEnd) {
moveEnd()
}
}
}
/**
* @param value 将十进制转为度分秒
* @returns { degrees: number, minutes: number, seconds: number } degrees:度 minutes:分 seconds:秒
*/
export const formatDecimalToDegrees = (value) => {
const degrees = Math.floor(value)
const minutes = Math.floor((value - degrees) * 60)
const seconds = (value - degrees - minutes / 60) * 3600
return {
degrees: degrees,
minutes: minutes,
seconds: seconds.toFixed(2)
}
}

View File

@@ -0,0 +1,29 @@
/**
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/
import useUserStore from '@/store/modules/user'
export default {
mounted(el, binding) {
const { value } = binding
const all_permission = '*:*:*'
const permissions = useUserStore().permissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
const hasPermissions = permissions.some((permission) => {
return (
all_permission === permission || permissionFlag.includes(permission)
)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
// throw new Error('请设置操作权限标签值')
}
}
}

View File

@@ -0,0 +1,5 @@
import hasPermi from './hasPermi'
export default function directive(app) {
app.directive('hasPermi', hasPermi)
}

26
src/utils/encryption.js Normal file
View File

@@ -0,0 +1,26 @@
import * as CryptoSM4 from 'sm-crypto'
const pwdKey = stringToHex('egtEdgfrkasjsafr')
const pwdIV = stringToHex('9475352547656596')
function stringToHex(str) {
let hex = Array.prototype.map
.call(str, (c) => {
return c.charCodeAt(0).toString(16)
})
.join('')
return hex
}
function encryptedSM4(text) {
const gmCryptConfigSet = {
mode: 'cbc',
iv: pwdIV,
cipherType: 'base64'
}
return CryptoSM4.sm4.encrypt(text || '', pwdKey, gmCryptConfigSet)
}
export {
encryptedSM4
}

25832
src/utils/fakeData.js Normal file

File diff suppressed because it is too large Load Diff

179
src/utils/request.js Normal file
View File

@@ -0,0 +1,179 @@
import axios from 'axios'
import { ElNotification, ElMessage, ElLoading } from 'element-plus'
import { tansParams, blobValidate } from '@/utils/common'
import cache from '@/utils/cache'
import { saveAs } from 'file-saver'
import useUserStore from '@/store/modules/user'
// import {encryptedSM4} from '@/utils/encryption'
let downloadLoadingInstance
const errorCode = {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
'default': '系统未知错误,请反馈给管理员'
}
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_URL,
// 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
const token = cache.local.get('token')
if (token && !isToken) {
config.headers.Authorization = 'Bearer ' + token // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1)
config.params = {}
config.url = url
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const _url = sessionObj.url // 请求地址
const _data = sessionObj.data // 请求数据
const _time = sessionObj.time // 请求时间
const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交
if (_data === requestObj.data && requestObj.time - _time < interval && _url === requestObj.url) {
const message = '数据正在处理,请勿重复提交'
console.warn(`[${_url}]: ` + message)
return Promise.reject(new Error(message))
}
cache.session.setJSON('sessionObj', requestObj)
}
}
// 数据加密过密SM4暂时注释
// if (config.method === 'post' && config.data && !(config.data instanceof FormData)) {
// const encryptedData = encryptedSM4(JSON.stringify(config.data)) // 将明文加密为十六进制字符串
// let data = {
// paramValue: encryptedData
// }
// config.data = data
// }
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode.default
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
useUserStore().logout().then(() => {
location.href = '/login'
})
// todo 退出登录跳转到登录页
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code !== 200) {
ElNotification.error({ title: msg })
return Promise.reject('error')
}
return Promise.resolve(res.data)
},
(error) => {
console.log('err' + error)
let { message } = error
if (message == 'Network Error') {
message = '后端接口连接异常'
} else if (message.includes('timeout')) {
message = '系统接口请求超时'
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常'
}
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
})
// 通用下载方法
export function download(url, params, filename, config, method = 'post') {
downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' })
const requestConfig = {
...config,
responseType: 'blob',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
if (method.toLowerCase() === 'post') {
requestConfig.transformRequest = [ params => tansParams(params) ]
return service.post(url, params, requestConfig).then(async (data) => {
const isBlob = blobValidate(data)
if (isBlob) {
const blob = new Blob([ data ], { type: 'application/octet-stream' }) // 你可以根据实际需要设置MIME类型
saveAs(blob, filename)
} else {
const resText = await data.text()
const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode.default
ElMessage.error(errMsg)
}
downloadLoadingInstance.close()
})
.catch((r) => {
console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close()
})
} else if (method.toLowerCase() === 'get') {
// 如果是GET请求需要将params附加到url上并且不设置transformRequest
const queryString = new URLSearchParams(params).toString()
url += (url.includes('?') ? '&' : '?') + queryString
return service.get(url, requestConfig).then(async (data) => {
const isBlob = blobValidate(data)
if (isBlob) {
const blob = new Blob([ data ], { type: 'application/octet-stream' }) // 你可以根据实际需要设置MIME类型
saveAs(blob, filename)
} else {
const resText = await data.text()
const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode.default
ElMessage.error(errMsg)
}
downloadLoadingInstance.close()
})
.catch((r) => {
console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close()
})
}
throw new Error('Unsupported method: ' + method)
}
export default service