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(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