Files
dzc d86bc810cc fix:图片访问
fix:权限修改
2026-10-29 14:26:56 +08:00

699 lines
18 KiB
TypeScript

import { accessFileUrl, refreshFileAccess, isProtectedFile } from './file-access'
import { nextTick } from "vue"
import router from "/@/router/index"
import pinia from "/@/stores/index"
import { storeToRefs } from "pinia"
import { useThemeConfig } from "/@/stores/themeConfig"
import { i18n } from "/@/i18n/index"
import { Local } from "/@/utils/storage"
import { verifyUrl } from "/@/utils/toolsValidate"
import request from "/@/utils/request"
import { useMessage } from "/@/hooks/message"
import * as CryptoJS from "crypto-js"
import { sm4 } from "sm-crypto"
import { validateNull } from "./validate"
/**
* 设置浏览器标题国际化
* @method const title = useTitle(); ==> title()
*/
export function useTitle() {
const stores = useThemeConfig(pinia)
const { themeConfig } = storeToRefs(stores)
nextTick(() => {
let globalTitle: string = themeConfig.value.globalTitle
let webTitle = setTagsViewNameI18n(router.currentRoute.value)
document.title = `${webTitle} - ${globalTitle}` || globalTitle
})
}
/**
* 设置 自定义 tagsView 名称、 自定义 tagsView 名称国际化
* @param params 路由 query、params 中的 tagsViewName
* @returns 返回当前 tagsViewName 名称
*/
export function setTagsViewNameI18n(item: any) {
let tagsViewName: string = ""
const { query, params } = item
//修复tagsViewName匹配到其他含下列单词的路由
const pattern = /^\{("(zh-cn|en|zh-tw)":"[^,]+",?){1,3}}$/
if (query?.tagsViewName || params?.tagsViewName) {
if (pattern.test(query?.tagsViewName) || pattern.test(params?.tagsViewName)) {
// 国际化
const urlTagsParams =
(query?.tagsViewName && JSON.parse(query?.tagsViewName)) || (params?.tagsViewName && JSON.parse(params?.tagsViewName))
tagsViewName = urlTagsParams[i18n.global.locale.value]
} else {
// 非国际化
tagsViewName = query?.tagsViewName || params?.tagsViewName
}
} else {
// 非自定义 tagsView 名称
tagsViewName = i18n.global.t(item.name)
}
return tagsViewName
}
/**
* 图片懒加载
* @param el dom 目标元素
* @param arr 列表数据
* @description data-xxx 属性用于存储页面或应用程序的私有自定义数据
*/
export const lazyImg = (el: string, arr: EmptyArrayType) => {
const io = new IntersectionObserver((res) => {
res.forEach((v: any) => {
if (v.isIntersecting) {
const { img, key } = v.target.dataset
v.target.src = img
v.target.onload = () => {
io.unobserve(v.target)
arr[key]["loading"] = false
}
}
})
})
nextTick(() => {
document.querySelectorAll(el).forEach((img) => io.observe(img))
})
}
/**
* 全局组件大小
* @returns 返回 `window.localStorage` 中读取的缓存值 `globalComponentSize`
*/
export const globalComponentSize = (): string => {
const stores = useThemeConfig(pinia)
const { themeConfig } = storeToRefs(stores)
return Local.get("themeConfig")?.globalComponentSize || themeConfig.value?.globalComponentSize
}
/**
* 对象深克隆
* @param obj 源对象
* @returns 克隆后的对象
*/
export function deepClone(obj: EmptyObjectType) {
let newObj: EmptyObjectType
try {
newObj = obj.push ? [] : {}
} catch (error) {
newObj = {}
}
for (let attr in obj) {
if (obj[attr] && typeof obj[attr] === "object") {
newObj[attr] = deepClone(obj[attr])
} else {
newObj[attr] = obj[attr]
}
}
return newObj
}
/**
* 判断是否是移动端
*/
export function isMobile() {
if (
navigator.userAgent.match(
/('phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone')/i,
)
) {
return true
} else {
return false
}
}
/**
* 判断数组对象中所有属性是否为空,为空则删除当前行对象
* @description @感谢大黄
* @param list 数组对象
* @returns 删除空值后的数组对象
*/
export function handleEmpty(list: EmptyArrayType) {
const arr = [] as any[]
for (const i in list) {
const d = [] as any[]
for (const j in list[i]) {
d.push(list[i][j])
}
const leng = d.filter((item) => item === "").length
if (leng !== d.length) {
arr.push(list[i])
}
}
return arr
}
/**
* 打开外部链接
* @param val 当前点击项菜单
*/
export function handleOpenLink(val: RouteItem) {
router.push(val.path)
if (verifyUrl(<string>val.meta?.isLink)) window.open(val.meta?.isLink)
else window.open(`${val.meta?.isLink}`)
}
/**
* 打开小窗口
*/
export const openWindow = (url: string, title: string, w: number, h: number) => {
// @ts-ignore
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
// @ts-ignore
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
return 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,
)
}
/**
*加密处理
*/
export function encryption(src: string, keyWord: string) {
const key = CryptoJS.enc.Utf8.parse(keyWord)
// 加密
var encrypted = CryptoJS.AES.encrypt(src, key, {
iv: key,
mode: CryptoJS.mode.CFB,
padding: CryptoJS.pad.NoPadding,
})
return encrypted.toString()
}
/**
* 解密
* @param {*} params 参数列表
* @returns 明文
*/
export function decryption(src: string, keyWord: string) {
const key = CryptoJS.enc.Utf8.parse(keyWord)
// 解密逻辑
var decryptd = CryptoJS.AES.decrypt(src, key, {
iv: key,
mode: CryptoJS.mode.CFB,
padding: CryptoJS.pad.NoPadding,
})
return decryptd.toString(CryptoJS.enc.Utf8)
}
/**
* SM4加密处理
*/
export function sm4Encryption(src: string, keyWord: string) {
return sm4.encrypt(src, keyWord)
}
/**
* SM4解密处理
* @param {*} params 参数列表
* @returns 明文
*/
export function sm4Decryption(src: string, keyWord: string) {
return sm4.decrypt(src, keyWord)
}
/**
* Base64 加密
* @param {*} src 明文
* @returns 密文
*/
export function base64Encrypt(src: string) {
const encodedWord = CryptoJS.enc.Utf8.parse(src)
return CryptoJS.enc.Base64.stringify(encodedWord)
}
/**
*
* @param url 目标下载接口
* @param query 查询参数
* @param fileName 文件名称
* @returns {*}
*/
export function downBlobFile(url: any, query: any, fileName: string, retried = false) {
return request({
url: accessFileUrl(url),
...(isProtectedFile(url) ? { baseURL: '' } : {}),
method: "get",
responseType: "blob",
params: query,
})
.then((response) => {
handleBlobFile(response, fileName)
})
.catch(async (error) => {
if (!retried && isProtectedFile(url)) {
await refreshFileAccess(true)
return downBlobFile(url, query, fileName, true)
}
let msg = "下载失败"
try {
if (error instanceof Blob) {
const text = typeof (error as any).text === "function" ? await (error as any).text() : ""
if (text) {
try {
const json = JSON.parse(text)
msg = json.msg || json.message || msg
} catch {
msg = text || msg
}
}
} else if (typeof error === "object") {
msg = (error as any).msg || (error as any).message || msg
}
} catch {
msg = "下载失败"
}
useMessage().error(msg)
})
}
export function postDownBlobFile(url: any, data: any, fileName: string) {
return request({
url: url,
method: "post",
responseType: "blob",
params: {
current: data?.current ?? 1,
size: data?.size ?? 99999,
ascs: data?.ascs ?? [],
descs: data?.descs ?? [],
},
data,
timeout: 360 * 1000,
})
.then((response) => {
handleBlobFile(response, fileName)
})
.catch(async (error) => {
let msg = "下载失败"
try {
if (error instanceof Blob) {
const text = typeof (error as any).text === "function" ? await (error as any).text() : ""
if (text) {
try {
const json = JSON.parse(text)
msg = json.msg || json.message || msg
} catch {
msg = text || msg
}
}
} else if (typeof error === "object") {
msg = (error as any).msg || (error as any).message || msg
}
} catch {
msg = "下载失败"
}
useMessage().error(msg)
})
}
/**
* blob 文件刘处理
* @param response 响应结果
* @returns
*/
export function handleBlobFile(response: any, fileName: string) {
// 处理返回的文件流
// const blob = response
// if (blob && blob.size === 0) {
// useMessage().error("内容为空,无法下载")
// return
// }
// const link = document.createElement("a")
// // 兼容一下 入参不是 File Blob 类型情况
// var binaryData = [] as any
// binaryData.push(response)
// link.href = window.URL.createObjectURL(new Blob(binaryData))
// link.download = fileName
// document.body.appendChild(link)
// link.click()
// window.setTimeout(function () {
// // @ts-ignore
// URL.revokeObjectURL(blob)
// document.body.removeChild(link)
// }, 0)
// 1. 验证response类型和结构
if (!response || typeof response !== "object" || response.size === 0) {
useMessage().error("无效的响应数据")
return
}
// 2. 验证blob类型
let blob: Blob
try {
if (response instanceof Blob) {
blob = response
} else if (response instanceof File) {
blob = response
} else if (response.data && response.data instanceof Blob) {
blob = response.data
} else {
const safeData = Array.isArray(response) ? response : [response]
blob = new Blob(safeData, { type: "application/octet-stream" })
}
} catch (error) {
console.error("导出错误", error)
useMessage().error("文件数据格式错误")
return
}
if (blob.type === "application/json" || blob.type === "text/plain") {
try {
const handleText = (t: string) => {
try {
const json = JSON.parse(t)
useMessage().error(json.msg || json.message || "下载失败")
} catch {
useMessage().error(t || "下载失败")
}
}
if (typeof (blob as any).text === "function") {
(blob as any).text().then((t: string) => handleText(t))
} else {
const reader = new FileReader()
reader.onload = () => handleText(String(reader.result || ""))
reader.readAsText(blob)
}
} catch {
useMessage().error("下载失败")
}
return
}
// 3. 验证文件大小 (防止过大文件)
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB限制
if (blob.size > MAX_FILE_SIZE) {
useMessage().error("文件过大,无法下载")
return
}
// 4. 验证文件类型 (白名单)
const allowedTypes = [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/plain",
"image/jpeg",
"image/png",
// "application/zip" // 添加 ZIP 文件类型支持
"application/octet-stream",
"text/xml",
]
if (!allowedTypes.includes(blob.type) && blob.type !== "") {
useMessage().error("不支持的文件类型")
return
}
// 5. 安全的文件下载
const link = document.createElement("a")
link.href = window.URL.createObjectURL(blob)
link.download = fileName.replace(/[<>:"/\\|?*]/g, "_").substring(0, 255) // 文件名清理,移除危险字符
link.style.display = "none" // 隐藏链接
document.body.appendChild(link)
link.click()
// 6. 安全的清理操作
setTimeout(() => {
try {
window.URL.revokeObjectURL(link.href)
document.body.removeChild(link)
} catch (error) {
// 静默处理清理错误
}
}, 10)
}
/**
* @description 生成唯一 uuid
* @return string
*/
export function generateUUID() {
if (typeof crypto === "object") {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID()
}
if (typeof crypto.getRandomValues === "function" && typeof Uint8Array === "function") {
const callback = (c: any) => {
const num = Number(c)
return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16)
}
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback)
}
}
let timestamp = new Date().getTime()
let performanceNow = (typeof performance !== "undefined" && performance.now && performance.now() * 1000) || 0
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
let random = Math.random() * 16
if (timestamp > 0) {
random = (timestamp + random) % 16 | 0
timestamp = Math.floor(timestamp / 16)
} else {
random = (performanceNow + random) % 16 | 0
performanceNow = Math.floor(performanceNow / 16)
}
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
})
}
/**
* 统一批量导出
* @method useTitle 设置浏览器标题国际化
* @method setTagsViewNameI18n 设置 自定义 tagsView 名称、 自定义 tagsView 名称国际化
* @method lazyImg 图片懒加载
* @method globalComponentSize() element plus 全局组件大小
* @method deepClone 对象深克隆
* @method isMobile 判断是否是移动端
* @method handleEmpty 判断数组对象中所有属性是否为空,为空则删除当前行对象
* @method handleOpenLink 打开外部链接
*/
const other = {
useTitle: () => {
useTitle()
},
setTagsViewNameI18n(route: RouteToFrom) {
return setTagsViewNameI18n(route)
},
lazyImg: (el: string, arr: EmptyArrayType) => {
lazyImg(el, arr)
},
globalComponentSize: () => {
return globalComponentSize()
},
deepClone: (obj: EmptyObjectType) => {
return deepClone(obj)
},
isMobile: () => {
return isMobile()
},
handleEmpty: (list: EmptyArrayType) => {
return handleEmpty(list)
},
handleOpenLink: (val: RouteItem) => {
handleOpenLink(val)
},
encryption: (src: string, keyWord: string) => {
return encryption(src, keyWord)
},
decryption: (src: string, keyWord: string) => {
return decryption(src, keyWord)
},
base64Encrypt: (data: any) => {
return base64Encrypt(data)
},
downBlobFile: (url: any, query: any, fileName: string) => {
return downBlobFile(url, query, fileName)
},
postDownBlobFile: (url: any, data: any, fileName: string) => {
return postDownBlobFile(url, data, fileName)
},
toUnderline: (str: string) => {
return toUnderline(str)
},
openWindow: (url: string, title: string, w: number, h: number) => {
return openWindow(url, title, w, h)
},
getQueryString: (url: string, paraName: string) => {
return getQueryString(url, paraName)
},
adaptationUrl: (url?: string) => {
return adaptationUrl(url)
},
resolveAllEunuchNodeId: (json: any[], idArr: any[], temp: any[] = []) => {
return resolveAllEunuchNodeId(json, idArr, temp)
},
getNonDuplicateID: () => {
return getNonDuplicateID()
},
addUnit: (value: string | number, unit = "px") => {
return addUnit(value, unit)
},
validateNull: (value: any) => {
return validateNull(value)
},
getNumberRadixNum: (input: Number) => {
return getNumberRadixNum(input)
},
}
export function getNumberRadixNum(input: Number) {
let strings = input.toString().split(".")
if (strings.length <= 1) {
return 0
}
return strings[1].toString().length
}
export function getQueryString(url: string, paraName: string) {
const arrObj = url.split("?")
if (arrObj.length > 1) {
const arrPara = arrObj[1].split("&")
let arr
for (let i = 0; i < arrPara.length; i++) {
arr = arrPara[i].split("=")
// eslint-disable-next-line eqeqeq
if (arr != null && arr[0] == paraName) {
return arr[1]
}
}
return ""
} else {
return ""
}
}
/**
* 列表结构转树结构
* @param data
* @param id
* @param parentId
* @param children
* @param rootId
* @returns {*}
*/
export function handleTree(data: any, id: any, parentId: any, children: any, rootId: any) {
id = id || "id"
parentId = parentId || "parentId"
children = children || "children"
rootId =
rootId ||
Math.min.apply(
Math,
data.map((item: any) => {
return item[parentId]
}),
) ||
0
//对源数据深度克隆
const cloneData = JSON.parse(JSON.stringify(data))
//循环所有项
const treeData = cloneData.filter((father: any) => {
const branchArr = cloneData.filter((child: any) => {
//返回每一项的子级数组
return father[id] === child[parentId]
})
branchArr.length > 0 ? (father[children] = branchArr) : ""
//返回第一层
return father[parentId] === rootId
})
return treeData !== "" ? treeData : data
}
/**
* 解析所有太监节点ID
* @returns
*/
const resolveAllEunuchNodeId = (json: any[], idArr: any[], temp: any[] = []) => {
for (const item of json) {
if (item.children && item.children.length !== 0) {
resolveAllEunuchNodeId(item.children, idArr, temp)
} else {
temp.push(...idArr.filter((id) => id === item.id))
}
}
return temp
}
/**
*
* @param str 驼峰转下划线
* @returns 下划线
*/
export function toUnderline(str: string) {
return str.replace(/([A-Z])/g, "_$1").toLowerCase()
}
/**
* 自动适配不同的后端架构
* 1. 例如 /act/oa/task ,在微服务架构保持不变,在单体架构编程 /admin/oa/task
* 2. 特殊 /gen/xxx ,在微服务架构、单体架构编程 都需保持不变
*
* @param originUrl 原始路径
*/
const adaptationUrl = (originUrl?: string) => {
// 微服务架构 不做路径转换,为空不做路径转换
const isMicro = import.meta.env.VITE_IS_MICRO
if (validateNull(isMicro) || isMicro === "true") {
return originUrl
}
// 转为 /admin 路由前缀的请求
return `/admin/${originUrl?.split("/").splice(2).join("/")}`
}
/**
* @description 获取不重复的id
* @param length { Number } id的长度
* @return { String } id
*/
const getNonDuplicateID = (length = 8) => {
let idStr = Date.now().toString(36)
idStr += Math.random().toString(36).substring(3, length)
return idStr
}
/**
* @description 添加单位
* @param {String | Number} value 值 100
* @param {String} unit 单位 px em rem
*/
const addUnit = (value: string | number, unit = "px") => {
return !Object.is(Number(value), NaN) ? `${value}${unit}` : value
}
// 统一批量导出
export default other