commit f801f7e8b6cb3c7df0ecce97d503d5b53b1fec86 Author: LiuJiaNan <15703339975@163.com> Date: Wed Oct 22 11:19:51 2025 +0800 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..271822f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35a3a68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/src/test/ +/target/ +.idea + +/node_modules +*.local +env.d.ts +package-lock.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..66949b7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,42 @@ +# 开发相关文件 +.eslintrc.cjs +.eslintignore +.prettierrc.cjs +.gitignore +vite.config.js +vitest.config.js +tsconfig.json +jsconfig.json + +# 构建和缓存目录 +node_modules/ +dist/ +.vite/ +.cache/ + +# 开发工具配置 +.vscode/ +.idea/ +.editorconfig + +# 日志文件 +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 测试相关 +coverage/ +.nyc_output/ +test/ +tests/ +__tests__/ +*.test.js +*.test.ts +*.spec.js +*.spec.ts + +# 其他 +.DS_Store +.env* +!.env.example diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..42765ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 LiuJiaNan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e8b55b --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# zy-vue-library + +## 📦 安装 + +```bash +# npm +npm install zy-vue-library + +# yarn +yarn add zy-vue-library + +# pnpm +pnpm add zy-vue-library +``` + +### 环境要求 + +- Node.js >= 18.0.0 +- Vue >= 3.5.0 +- Element Plus >= 2.11.0 + +## 🔨 使用 + +### 按需导入 + +```javascript +import { AppTable, AppFormBuilder, useListData } from 'zy-vue-library' +``` + +## 📖 组件 + +#### AppFormBuilder - 动态表单构建器 +#### AppSearch - 搜索表单 +#### AppUpload - 文件上传 +#### AppEditor - 富文本编辑器 +#### AppImportFile - Excel导入 +#### AppMap - 地图选择点位 +#### AppTable - 数据表格 +#### AppPagination - 分页组件 +#### AppInfoBuilder - 信息展示 +#### AppVideo - 视频播放器 +#### AppAliPlayer - 视频播放器 +#### AppPreviewImg - 查看页面图片预览 +#### AppPreviewPdf - 查看页面PDF预览 +#### AppPdf - PDF预览 +#### AppTooltipImg - 表格列中预览图片 +#### AppTxt - 预览txt文件 +#### AppQrCode - 二维码生成 +#### AppSign - 电子签名 +#### AppVerification - 滑块验证码 +#### AppVerificationCode - 数字验证码 +#### AppViewTree - 左侧树形菜单 +#### AppCascader - 级联选择 + +## 🖼️ 布局 + +#### AppLayout - 默认布局 + +## 🛠️ Hooks + +#### useDataDictionary - 获取数据字典 +#### useListData - 列表数据管理 +#### useForm - 表单操作 +#### useDownloadFile - 文件下载 +#### useDownloadBlob - 文件下载Blob +#### useIsExistenceDuplicateSelection - 判断数组中是否存在重复项 +#### useQueryCriteria - 查询条件缓存 +#### useRequestLoading - 请求加载状态 +#### useUploadFile - 上传附件 + +## 🔨 工具函数 + +#### serialNumber(pagination, index) - 计算表格序号 +#### numFormat(num) - 千位分隔符格式化 +#### randoms(min, max) - 生成指定范围随机数 +#### secondConversion(second) - 秒转时分秒 +#### calculateFileSize(size) - 计算文件大小 +#### ArrayDeduplication(arr) - 数组去重 +#### arrayObjectDeduplication(arr, name) - 数组对象去重 +#### toArrayString(value) - 字符串数组转数组 +#### paging(list, currentPage, pageSize) - 数据分页 +#### getSelectAppointItemList(list, value, idKey) - 获取指定项数组 +#### listTransTree(json, idStr, pidStr, childrenStr) - JSON转树形结构 +#### getFileName(name) - 获取文件名 +#### getFileSuffix(name) - 获取文件后缀 +#### interceptTheSuffix(name, suffix) - 判断文件后缀 +#### findCharIndex(str, char, num) - 查找字符位置 +#### getUrlParam(name) - 获取URL参数 +#### isEmpty(value) - 验证是否为空 +#### getDataType(data) - 获取数据类型 +#### isEmptyToWhether(value, options) - 值转换为是否显示 +#### image2Base64(imgUrl) - 图片转base64 +#### image2Base642(file) - 文件转base64 +#### checkImgExists(imgUrl) - 检查图片是否可访问 +#### readTxtDocument(filePath) - 读取文本文档 +#### getLabelName(status, list, idKey, nameKey) - 翻译状态 +#### idCardGetDateAndGender(idCard) - 身份证号获取信息 +#### addingPrefixToFile(list, options) - 文件添加前缀 +#### verifyDuplicateSelection(list, index, key, id) - 验证重复选择 +#### getRowSpans(data, field, rowIndex) - 计算表格合并行 +#### createGuid(len) - 生成GUID +#### getFileUrl() - 获取文件前缀地址 +#### getBaseUrl() - 获取基础URL +#### getWebUrl() - 获取当前页面URL + +## 🎯 正则表达式 + +#### PHONE - 匹配中国手机号码,可包含国家代码86,支持各种运营商号段。 +#### UNIFIED_SOCIAL_CREDIT_CODE - 匹配中国大陆的统一社会信用代码。 +#### ID_NUMBER - 匹配中国大陆的身份证号码,包括15位和18位号码,并验证最后一位校验码。 +#### MOBILE_PHONE - 匹配中国大陆的移动电话号码,不包含国家代码。 +#### FLOATING_POINT_NUMBER - 匹配浮点数,允许整数、一位或两位小数,以及零的情况。 +#### ONE_DECIMAL_PLACES - 两位小数。 +#### TWO_DECIMAL_PLACES - 一位小数(非必须)。 +#### LICENSE_PLATE_NUMBER - 匹配中国大陆的车牌号码。 +#### STRONG_PASSWORD - 匹配强密码,要求至少8个字符,包含大小写字母、数字和特殊字符。 +#### HTML_TAG - 匹配完整的HTML标签,包括开始标签和结束标签。 + +## 🗃️ 状态管理 + +### useQueryCriteriaStore - 查询条件缓存 + +#### getQueryCriteria - 获取缓存的查询条件 +#### setQueryCriteria - 设置缓存的查询条件 +#### resetQueryCriteria - 清空缓存的查询条件 +#### getTabsActiveName - 获取当前激活的 tabs 名称 +#### setTabsActiveName - 设置当前激活的 tabs 名称 + +## 🎛️ 指令 + +#### v-permission - 权限指令 + +## 🔄 动态路由 + +#### configureDynamicRouter - 配置动态路由 +#### resetDynamicRouter - 重置动态路由 +#### getStorageRouter - 获取存储的动态路由 + +## 🔐 AES加密服务 + +#### configureAesSecret - 配置AES加密服务 +#### aesEncrypt - 加密 +#### aesDecrypt - 解密 + +## 🌐 Axios + +#### configureAxios - 配置Axios +#### postRequest - post请求 +#### getRequest - get请求 +#### putRequest - put请求 +#### deleteRequest - delete请求 +#### patchRequest - patch请求 +#### uploadRequest - upload请求 + +## 📋 枚举 + +#### formItemTypeEnum - 表单类型枚举 + +## 📄 更新日志 + +### v1.0.0 (2025-09-22) + +- 🎉 初始版本发布 + +### v1.1.0 (2025-09-29) + +- 🚀 稳定版本发布 \ No newline at end of file diff --git a/aesSecret/index.js b/aesSecret/index.js new file mode 100644 index 0000000..ed550df --- /dev/null +++ b/aesSecret/index.js @@ -0,0 +1,111 @@ +import CryptoJS from "crypto-js"; + +/** + * AES加密服务类 + */ +class AESSecretService { + constructor() { + /** + * 默认配置 + */ + this.config = { + key: "", // 加密密钥 + encryptKey: "", // 加密解密密钥 + decryptKey: "", // 加密密钥 + }; + } + + /** + * 配置AES加密密钥 + * @param {Object} config - 配置选项 + * @param {string} [config.key] - 加密解密密钥 + * @param {string} [config.encryptKey] - 加密密钥 + * @param {string} [config.decryptKey] - 解密密钥 + */ + configure(config) { + if (!config) throw new Error('配置不能为空'); + if (!config.key && !config.encryptKey && !config.decryptKey) { + throw new Error('key、encryptKey、decryptKey 必须传入一个'); + } + + this.config = { ...this.config, ...config }; + if (this.config.key) { + this.encryptKey = CryptoJS.enc.Utf8.parse(this.config.key); + this.decryptKey = CryptoJS.enc.Utf8.parse(this.config.key); + } else { + this.encryptKey = CryptoJS.enc.Utf8.parse(this.config.encryptKey); + this.decryptKey = CryptoJS.enc.Utf8.parse(this.config.decryptKey); + } + } + + /** + * 加密方法 + * @param {string|Object} word - 要加密的内容 + * @returns {string} - 加密后的字符串 + */ + encrypt(word) { + let encrypted = ""; + if (typeof word === "string") { + const src = CryptoJS.enc.Utf8.parse(word); + encrypted = CryptoJS.AES.encrypt(src, this.encryptKey, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + } else if (typeof word === "object") { + // 对象格式的转成json字符串 + const data = JSON.stringify(word); + const src = CryptoJS.enc.Utf8.parse(data); + encrypted = CryptoJS.AES.encrypt(src, this.encryptKey, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + } + return encrypted.ciphertext.toString(CryptoJS.enc.Base64); + } + + /** + * 解密方法 + * @param {string} params - 要解密的内容 + * @returns {string} - 解密后的字符串 + */ + decrypt(params) { + const bytes = CryptoJS.AES.decrypt(params, this.decryptKey, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + const decryptedText = bytes.toString(CryptoJS.enc.Utf8); + return JSON.parse(decryptedText); + } +} + +/** + * 配置AES加密服务 + * @param {Object} config - 配置选项 + * @param {string} [config.key] - 加密解密密钥 + * @param {string} [config.encryptKey] - 加密密钥 + * @param {string} [config.decryptKey] - 加密密钥 + */ +export function configureAesSecret(config) { + if (!window.aesSecretService) window.aesSecretService = new AESSecretService(); + return window.aesSecretService.configure(config); +} + +/** + * 加密方法 + * @param {string|Object} word - 要加密的内容 + * @returns {string} - 加密后的字符串 + */ +export function aesEncrypt(word) { + if (!window.aesSecretService) throw new Error('未配置AES加密服务,请先调用 configureAesSecret'); + return window.aesSecretService.encrypt(word); +} + +/** +* 解密方法 +* @param {string} params - 要解密内容 +* @returns {string} - 解密后的字符串 +*/ +export function aesDecrypt(params) { + if (!window.aesSecretService) throw new Error('未配置AES加密服务,请先调用 configureAesSecret'); + return window.aesSecretService.decrypt(params); +} diff --git a/axios/index.js b/axios/index.js new file mode 100644 index 0000000..59b8949 --- /dev/null +++ b/axios/index.js @@ -0,0 +1,408 @@ +import axios from "axios"; +import { ElMessage } from "element-plus"; +import dayjs from 'dayjs' +import useRequestLoading from "../hooks/useRequestLoading/index.js"; +import { getBaseUrl } from "../utils/index.js"; +import CryptoJS from "crypto-js"; +import {aesDecrypt} from "../aesSecret/index.js"; + +/** + * Axios服务类 + */ +class AxiosService { + constructor() { + /** + * Axios配置选项 + */ + this.config = { + router: null, + store: null, + tokenRefreshUrl: '/sys/refreshToken', + loginPath: '/login', + tokenRefreshInterval: 5, // 分钟 + baseURL: getBaseUrl(), + timeout: 1000 * 60 * 10, + requestParamsSign: { + use: false, + key: '' + }, + responseParamsDecrypt: { + use: false, + } + }; + + this.loading = useRequestLoading(); + this.isTipTokenFailure = false; + } + + /** + * 配置axios插件 + * @param {Object} [config] - 配置选项 + * @param {Object} config.router - Vue Router实例 + * @param {Object} config.store - token的piniaStore + * @param {string} [config.tokenRefreshUrl="/sys/refreshToken"] - token刷新接口URL + * @param {string} [config.loginPath="/login"] - 登录页面路由 + * @param {number} [config.tokenRefreshInterval=5] - token刷新间隔(分钟) + * @param {string} [config.baseURL=getBaseUrl()] - API基础URL + * @param {number} [config.timeout=60000] - 请求超时时间(毫秒) + * @param {Object} [config.requestParamsSign] - 请求参数是否签名 + * @param {boolean} [config.requestParamsSign.use=false] - 是否使用签名 + * @param {string} [config.requestParamsSign.key] - 签名密钥 + * @param {Object} [config.responseParamsDecrypt] - 响应参数是否需要解密 + * @param {boolean} [config.responseParamsDecrypt.use=false] - 是否需要解密 + */ + configure(config = {}) { + if (!config.router) throw new Error('router 参数必传'); + if (!config.store) throw new Error('store 参数必传'); + this.config = { ...this.config, ...config }; + + // 设置默认配置 + axios.defaults.baseURL = this.config.baseURL; + axios.defaults.timeout = this.config.timeout; + + // 清除现有拦截器 + axios.interceptors.request.clear(); + axios.interceptors.response.clear(); + + // 重新设置拦截器 + this.setupInterceptors(); + } + + /** + * token刷新逻辑 + */ + async refreshToken() { + const currentRoute = this.config.router.currentRoute?.value; + if (currentRoute?.meta?.isLogin) { + const store = this.config.store; + if (store.getTokenTime) { + const diffMinutes = dayjs().diff(dayjs(store.getTokenTime), "minute"); + if (diffMinutes >= this.config.tokenRefreshInterval) { + await store.setTokenTime(dayjs().format("YYYY-MM-DD HH:mm:ss")); + await this.postRequest(this.config.tokenRefreshUrl); + } + } + } + } + + /** + * 请求参数签名 + */ + requestParamsSign(params) { + if (!this.config.requestParamsSign.key) throw new Error('请传入签名密钥'); + const Timestamp = new Date().getTime(); + const cloneParams = { ...params }; + const keys = Object.keys(cloneParams).sort(); + const sortData = {}; + for (let i = 0; i < keys.length; i++) { + if (cloneParams[keys[i]] !== null && cloneParams[keys[i]] !== undefined) { + sortData[keys[i]] = cloneParams[keys[i]]; + } + } + const Sign = CryptoJS.MD5( + Timestamp + + JSON.stringify(sortData) + + this.config.requestParamsSign.key + ).toString(); + return { + Sign, + Timestamp, + }; + } + + /** + * 设置拦截器 + */ + setupInterceptors() { + // 请求拦截器 + axios.interceptors.request.use( + async (request) => { + request.headers.Token = this.config.store.getToken; + let params = request.data || request.params || {}; + // 根据配置决定是否启用参数签名 + if (this.config.requestParamsSign.use) { + const { Timestamp, Sign } = this.requestParamsSign(params); + request.headers.Timestamp = Timestamp; + request.headers.Sign = Sign; + } + + this.loading.value = true; + return request; + }, + (error) => Promise.reject(error) + ); + + // 响应拦截器 + axios.interceptors.response.use( + (response) => { + if (this.config.responseParamsDecrypt.use) response.data = { ...response.data, ...aesDecrypt(response.data.data)} + this.loading.value = false; + if (response.data.code === 401) { + if (!this.isTipTokenFailure) { + this.isTipTokenFailure = true; + ElMessage.error("登录失效,请重新登录"); + this.config.router.push(this.config.loginPath); + this.isTipTokenFailure = false; + } + return Promise.reject(response.data.msg); + } else { + this.refreshToken().then(); + } + return response; + }, + (error) => { + if (error && error.response) { + if (import.meta.env.DEV) ElMessage.error(`连接错误${error.response.status}`); + } else ElMessage.error(error.message); + return Promise.reject(error.message); + } + ); + } + + /** + * 发送POST请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ + postRequest(url, params = {}) { + return new Promise((resolve, reject) => { + axios + .post(url, params) + .then((res) => { + if (res.data.result === "success") { + resolve(res.data); + } else { + ElMessage.error(res.data.msg || "系统开小差了"); + reject(res.data); + } + }) + .catch((err) => { + reject(err); + }); + }); + } + + /** + * 获取GET请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @param {string} splicingURL - URL拼接字符串 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ + getRequest(url, params = {}, splicingURL = "") { + return new Promise((resolve, reject) => { + axios + .get(url + (splicingURL ? "/" + splicingURL : ""), { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + params, + }) + .then((res) => { + if (res.data.result === "success") { + resolve(res.data); + } else { + ElMessage.error(res.data.msg || "系统开小差了"); + reject(res.data); + } + }) + .catch((err) => { + reject(err); + }); + }); + } + + /** + * 获取PUT请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ + putRequest(url, params = {}) { + return new Promise((resolve, reject) => { + axios + .put(url, params) + .then((res) => { + if (res.data.result === "success") { + resolve(res.data); + } else { + ElMessage.error(res.data.msg || "系统开小差了"); + reject(res.data); + } + }) + .catch((err) => { + reject(err); + }); + }); + } + + /** + * 获取DELETE请求 + * @param {string} url - 请求URL + * @param {Object} params - 删除参数 + * @param {string} splicingURL - URL拼接字符串 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ + deleteRequest(url, params = {}, splicingURL = "") { + return new Promise((resolve, reject) => { + axios + .delete(url + (splicingURL ? "/" + splicingURL : ""), { data: params }) + .then((res) => { + if (res.data.result === "success") { + resolve(res.data); + } else { + ElMessage.error(res.data.msg || "系统开小差了"); + reject(res.data); + } + }) + .catch((err) => { + reject(err); + }); + }); + } + + /** + * 获取PATCH请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @param {string} splicingURL - URL拼接字符串 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ + patchRequest(url, params = {}, splicingURL = "") { + return new Promise((resolve, reject) => { + axios + .patch(url + (splicingURL ? "/" + splicingURL : ""), params) + .then((res) => { + if (res.data.result === "success") { + resolve(res.data); + } else { + ElMessage.error(res.data.msg || "系统开小差了") + reject(res.data); + } + }) + .catch((err) => { + reject(err); + }); + }); + } + + /** + * 上传文件请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ + uploadRequest(url, params = {}) { + return new Promise((resolve, reject) => { + axios + .post(url, params, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((res) => { + if (res.data.result === "success") { + resolve(res.data); + } else { + ElMessage.error(res.data.msg || "系统开小差了"); + reject(res.data); + } + }) + .catch((err) => { + reject(err); + }); + }); + } +} + +/** + * 配置axios插件 + * @param {Object} [config] - 配置选项 + * @param {Object} config.router - Vue Router实例 + * @param {Object} config.store - token的piniaStore + * @param {string} [config.tokenRefreshUrl="/sys/refreshToken"] - token刷新接口URL + * @param {string} [config.loginPath="/login"] - 登录页面路由 + * @param {number} [config.tokenRefreshInterval=5] - token刷新间隔(分钟) + * @param {string} [config.baseURL=getBaseUrl()] - API基础URL + * @param {number} [config.timeout=60000] - 请求超时时间(毫秒) + * @param {Object} [config.requestParamsSign] - 请求参数是否签名 + * @param {boolean} [config.requestParamsSign.use=false] - 是否使用签名 + * @param {string} [config.requestParamsSign.key] - 签名密钥 + * @param {Object} [config.responseParamsDecrypt] - 响应参数是否需要解密 + * @param {boolean} [config.responseParamsDecrypt.use=false] - 是否需要解密 + */ +export function configureAxios(config = {}) { + if(!window.axiosService) window.axiosService = new AxiosService(); + return axiosService.configure(config); +} + +/** + * 发送POST请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ +export function postRequest(url, params = {}) { + if (!window.axiosService) throw new Error('未配置Axios,请先调用 configureAxios'); + return window.axiosService.postRequest(url, params); +} + +/** + * 获取GET请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @param {string} splicingURL - URL拼接字符串 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ +export function getRequest(url, params = {}, splicingURL = "") { + if (!window.axiosService) throw new Error('未配置Axios,请先调用 configureAxios'); + return window.axiosService.getRequest(url, params, splicingURL); +} + +/** + * 获取PUT请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ +export function putRequest(url, params = {}) { + if (!window.axiosService) throw new Error('未配置Axios,请先调用 configureAxios'); + return window.axiosService.putRequest(url, params); +} + +/** + * 获取DELETE请求 + * @param {string} url - 请求URL + * @param {Object} params - 删除参数 + * @param {string} splicingURL - URL拼接字符串 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ +export function deleteRequest(url, params = {}, splicingURL = "") { + if (!window.axiosService) throw new Error('未配置Axios,请先调用 configureAxios'); + return window.axiosService.deleteRequest(url, params, splicingURL); +} + +/** + * 获取PATCH请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @param {string} splicingURL - URL拼接字符串 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ +export function patchRequest(url, params = {}, splicingURL = "") { + if (!window.axiosService) throw new Error('未配置Axios,请先调用 configureAxios'); + return window.axiosService.patchRequest(url, params, splicingURL); +} + +/** + * 上传文件请求 + * @param {string} url - 请求URL + * @param {Object} params - 请求参数 + * @returns {Promise} - 返回Promise对象,包含请求结果 + */ +export function uploadRequest(url, params = {}) { + if (!window.axiosService) throw new Error('未配置Axios,请先调用 configureAxios'); + return window.axiosService.uploadRequest(url, params); +} + diff --git a/components/ali-player/index.vue b/components/ali-player/index.vue new file mode 100644 index 0000000..298f1cc --- /dev/null +++ b/components/ali-player/index.vue @@ -0,0 +1,152 @@ + + + + + + + diff --git a/components/cascader/index.vue b/components/cascader/index.vue new file mode 100644 index 0000000..1b7aab4 --- /dev/null +++ b/components/cascader/index.vue @@ -0,0 +1,62 @@ + + + + + + + diff --git a/components/children/index.vue b/components/children/index.vue new file mode 100644 index 0000000..e0a5c6d --- /dev/null +++ b/components/children/index.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/components/editor/index.vue b/components/editor/index.vue new file mode 100644 index 0000000..7fc6991 --- /dev/null +++ b/components/editor/index.vue @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/components/form_builder/form_items_renderer.vue b/components/form_builder/form_items_renderer.vue new file mode 100644 index 0000000..f60a929 --- /dev/null +++ b/components/form_builder/form_items_renderer.vue @@ -0,0 +1,272 @@ + + + + + + + + {{ option.label }} + + + + + + + + + + + + + + + {{ item[option.labelKey] || item["name"] }} + + + + + {{ item[option.labelKey] || item["name"] }} + + + + + + + + + + + + + {{ option.label }} + + + + + + + + + + + + + + diff --git a/components/form_builder/index.vue b/components/form_builder/index.vue new file mode 100644 index 0000000..10a983a --- /dev/null +++ b/components/form_builder/index.vue @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/components/import_file/index.vue b/components/import_file/index.vue new file mode 100644 index 0000000..fcccd25 --- /dev/null +++ b/components/import_file/index.vue @@ -0,0 +1,73 @@ + + + + + + + + + + + 导出模板 + + 确定 + 取消 + + + + + + + diff --git a/components/info_builder/index.vue b/components/info_builder/index.vue new file mode 100644 index 0000000..b57fa41 --- /dev/null +++ b/components/info_builder/index.vue @@ -0,0 +1,29 @@ + + + + + + {{ item.value }} + {{ info[item.key] }} + + + + + + + + + diff --git a/components/map/index.vue b/components/map/index.vue new file mode 100644 index 0000000..e30e0b8 --- /dev/null +++ b/components/map/index.vue @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + 点击定位 + + + + + + + + + + diff --git a/components/map/map.vue b/components/map/map.vue new file mode 100644 index 0000000..675ee37 --- /dev/null +++ b/components/map/map.vue @@ -0,0 +1,141 @@ + + + + + + + + + + + + 搜索 + + + + + + + + + + + + + + + + + + + + + 关闭 + 确定 + + + + + + + diff --git a/components/pagination/index.vue b/components/pagination/index.vue new file mode 100644 index 0000000..6443544 --- /dev/null +++ b/components/pagination/index.vue @@ -0,0 +1,40 @@ + + + + + + + diff --git a/components/pdf/index.vue b/components/pdf/index.vue new file mode 100644 index 0000000..63e76ce --- /dev/null +++ b/components/pdf/index.vue @@ -0,0 +1,65 @@ + + + + + + + + 下载 + + + + + + + + + diff --git a/components/preview_img/index.vue b/components/preview_img/index.vue new file mode 100644 index 0000000..9321302 --- /dev/null +++ b/components/preview_img/index.vue @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/components/preview_pdf/index.vue b/components/preview_pdf/index.vue new file mode 100644 index 0000000..7406803 --- /dev/null +++ b/components/preview_pdf/index.vue @@ -0,0 +1,45 @@ + + + {{ name }} + + 预览 + + + + + {{ item.name || item.fileName || item[nameKey] }} + + 预览 + + + + + + + + + diff --git a/components/qr_code/index.vue b/components/qr_code/index.vue new file mode 100644 index 0000000..a0a24b2 --- /dev/null +++ b/components/qr_code/index.vue @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/components/search/index.vue b/components/search/index.vue new file mode 100644 index 0000000..6bae9d0 --- /dev/null +++ b/components/search/index.vue @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + 搜索 + + 重置 + + + + 展开 + + + 收起 + + + + + + + + + + + + + + + + diff --git a/components/sign/index.vue b/components/sign/index.vue new file mode 100644 index 0000000..0174004 --- /dev/null +++ b/components/sign/index.vue @@ -0,0 +1,90 @@ + + + + 手写签字 + + + + + + + + + 重签 + 确定 + 关闭 + + + + + + + diff --git a/components/table/index.vue b/components/table/index.vue new file mode 100644 index 0000000..8597d0e --- /dev/null +++ b/components/table/index.vue @@ -0,0 +1,135 @@ + + + + + + + {{ serialNumber(pagination, $index) }} + + + + + + + + + + + + diff --git a/components/tooltip_img/index.vue b/components/tooltip_img/index.vue new file mode 100644 index 0000000..1fbb0fb --- /dev/null +++ b/components/tooltip_img/index.vue @@ -0,0 +1,39 @@ + + + + + + + 暂无图片 + + 预览 + + + + + + diff --git a/components/txt/index.vue b/components/txt/index.vue new file mode 100644 index 0000000..2490770 --- /dev/null +++ b/components/txt/index.vue @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/components/upload/index.vue b/components/upload/index.vue new file mode 100644 index 0000000..16b3d55 --- /dev/null +++ b/components/upload/index.vue @@ -0,0 +1,150 @@ + + + + 点击选择文件上传 + + + + {{ tip.join(",") }}。 + + + + + + + + + + + + diff --git a/components/verification/index.vue b/components/verification/index.vue new file mode 100644 index 0000000..e82ded0 --- /dev/null +++ b/components/verification/index.vue @@ -0,0 +1,198 @@ + + + + + + + + + + 点击按钮进行验证 + + + + + + + + + + + + + 通过验证 + + + + + + + + + diff --git a/components/verification_code/index.vue b/components/verification_code/index.vue new file mode 100644 index 0000000..fba1951 --- /dev/null +++ b/components/verification_code/index.vue @@ -0,0 +1,122 @@ + + + + + + + + + diff --git a/components/video/index.vue b/components/video/index.vue new file mode 100644 index 0000000..d7609ca --- /dev/null +++ b/components/video/index.vue @@ -0,0 +1,83 @@ + + + + + + + + + + + + diff --git a/components/view_tree/index.vue b/components/view_tree/index.vue new file mode 100644 index 0000000..a7bbd01 --- /dev/null +++ b/components/view_tree/index.vue @@ -0,0 +1,69 @@ + + + + + + + + diff --git a/conversionRouterMeta/index.js b/conversionRouterMeta/index.js new file mode 100644 index 0000000..77c67dc --- /dev/null +++ b/conversionRouterMeta/index.js @@ -0,0 +1,15 @@ +export function conversionRouterMeta(menuList) { + for (let i = 0; i < menuList.length; i++) { + menuList[i].meta = JSON.parse(menuList[i].meta); + if (menuList[i].list.length > 0) conversionRouterMeta(menuList[i].list); + } + return menuList; +} +export function conversionNavMeta(navList) { + const navTempList = []; + for (let i = 0; i < navList.length; i++) { + const item = { title: navList[i].name, model: navList[i].bianma }; + navTempList.push(item); + } + return navTempList; +} diff --git a/css/common.scss b/css/common.scss new file mode 100644 index 0000000..bccfa96 --- /dev/null +++ b/css/common.scss @@ -0,0 +1,269 @@ +// 文字超出几行隐藏,最多5行 +// 使用超出1行隐藏,如果使用了flex,则需要给父元素设置min-width: 0; +@for $i from 1 through 5 { + .line-#{$i} { + @if $i == 1 { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } @else { + display: -webkit-box !important; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + -webkit-line-clamp: $i; + -webkit-box-orient: vertical !important; + } + } +} + +// 生成1-50的margin和padding(正负) +@for $i from 1 through 50 { + .m-#{$i} { + margin: #{$i}px; + } + .mt-#{$i} { + margin-top: #{$i}px; + } + .mr-#{$i} { + margin-right: #{$i}px; + } + .mb-#{$i} { + margin-bottom: #{$i}px; + } + .ml-#{$i} { + margin-left: #{$i}px; + } + .mtb-#{$i} { + margin-top: #{$i}px; + margin-bottom: #{$i}px; + } + .mlr-#{$i} { + margin-left: #{$i}px; + margin-right: #{$i}px; + } + .p-#{$i} { + padding: #{$i}px; + } + .pt-#{$i} { + padding-top: #{$i}px; + } + .pr-#{$i} { + padding-right: #{$i}px; + } + .pb-#{$i} { + padding-bottom: #{$i}px; + } + .pl-#{$i} { + padding-left: #{$i}px; + } + .ptb-#{$i} { + padding-top: #{$i}px; + padding-bottom: #{$i}px; + } + .plr-#{$i} { + padding-left: #{$i}px; + padding-right: #{$i}px; + } + .m--#{$i} { + margin: -#{$i}px; + } + .mt--#{$i} { + margin-top: -#{$i}px; + } + .mr--#{$i} { + margin-right: -#{$i}px; + } + .mb--#{$i} { + margin-bottom: -#{$i}px; + } + .ml--#{$i} { + margin-left: -#{$i}px; + } + .mtb--#{$i} { + margin-top: -#{$i}px; + margin-bottom: -#{$i}px; + } + .mlr--#{$i} { + margin-left: -#{$i}px; + margin-right: -#{$i}px; + } + .p--#{$i} { + padding: -#{$i}px; + } + .pt--#{$i} { + padding-top: -#{$i}px; + } + .pr--#{$i} { + padding-right: -#{$i}px; + } + .pb--#{$i} { + padding-bottom: -#{$i}px; + } + .pl--#{$i} { + padding-left: -#{$i}px; + } + .ptb--#{$i} { + padding-top: -#{$i}px; + padding-bottom: -#{$i}px; + } + .plr--#{$i} { + padding-left: -#{$i}px; + padding-right: -#{$i}px; + } +} + +* { + box-sizing: border-box; + font-size: 14px; + + &:not(dd,dl,dt,h1, h2, h3, h4, h5, h6) { + margin: 0; + padding: 0; + } +} + +h1, h2, h3, h4, h5, h6 { + font-size: revert; +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-transition-delay: 99999s; + -webkit-transition: color 99999s ease-out, background-color 99999s ease-out; +} + +#app { + background-color: #f6f8f9; + min-height: 100vh; +} + +a { + text-decoration: none; + color: var(--el-color-primary); +} + +.end { + .el-form-item__content { + justify-content: end; + } +} + +.text-blue { + color: #0000ff; +} + +.text-yellow { + color: #bebe05; +} + +.text-orange { + color: #de9004; +} + +.text-red { + color: #ff0000; +} + +.text-green { + color: #0bb20c; +} + +.tc { + text-align: center; +} + +.tr { + text-align: right; +} + +.tl { + text-align: left; +} + +.dn { + display: none; +} + +.table { + border-collapse: collapse; + width: 100%; + + td, + th { + border: 1px solid #eaeaea; + padding: 8px; + line-height: 1.6; + text-align: center; + } + + .title { + background-color: #f9f9f9; + } +} + +img.ml-10 { + &:first-child { + margin-left: 0; + } +} + +.print_use { + display: none; +} + +.viewer-zoom-in, .viewer-zoom-out, .viewer-one-to-one, .viewer-reset, .viewer-prev, .viewer-play, .viewer-next, .viewer-rotate-left, .viewer-rotate-right, .viewer-flip-horizontal, .viewer-flip-vertical, .viewer-fullscreen, .viewer-fullscreen-exit, .viewer-close { + &::before { + font-size: unset !important; + } +} + +.vue-pdf__wrapper-annotation-layer { + height: 0 !important; +} + +.w-e-bar { + border: 1px solid var(--el-border-color); + border-bottom: none; +} + +.w-e-text-container { + border: 1px solid var(--el-border-color); +} + +.w-e-bar-divider { + display: none !important; +} + +// 打印时去掉页眉页脚 +@page { + size: auto; + margin: 3mm; +} + +@media print { + + .print_use { + border-collapse: collapse; + width: 100%; + display: table; + + td, th { + border: 1px solid #eaeaea; + padding: 8px; + line-height: 1.6; + text-align: center; + } + } + + .print_no_use { + display: none; + } +} + +.iframe { + position: relative; + border: none; +} diff --git a/css/element.scss b/css/element.scss new file mode 100644 index 0000000..db78112 --- /dev/null +++ b/css/element.scss @@ -0,0 +1,157 @@ +.el-select, .el-cascader, .el-date-editor.el-input, .el-date-editor.el-input__wrapper, .el-input__wrapper, .el-input-number, .el-select-v2 { + width: 100% !important; +} + +.el-pagination .el-select { + width: 128px !important; +} + +.el-pagination--small .el-select { + width: 100px !important; +} + +.el-table .el-table__cell { + text-align: left; +} + +.el-descriptions__label { + width: 200px; +} + +.el-descriptions__content { + width: auto; +} + +.el-divider__text { + font-size: 16px !important; + font-weight: 700 !important; +} + +.el-form-item__label { + font-weight: 700; +} + +.el-dialog { + --el-dialog-margin-top: 50px !important; + padding: 0 !important; + + .el-dialog__header { + border-bottom: 1px solid #f1f1f1; + padding-left: 20px; + height: 48px; + line-height: 48px; + } + + .el-dialog__body { + padding: 16px 20px; + } + + .el-dialog__footer { + border-top: 1px solid #f1f1f1; + padding: 16px 20px; + } +} + +.el-table { + * { + font-size: 12px; + } + + th.el-table__cell { + --el-table-header-bg-color: rgb(245, 247, 250); + font-weight: bold; + color: rgb(0, 0, 0); + } + + .el-table__cell { + padding: 8px 0 !important; + text-align: center !important; + } +} + +.el-page-header { + border-bottom: 1px solid #eaeaea; + padding: 0 20px 20px 20px; + margin: 0 -20px 20px -20px; + + .el-page-header__content { + font-size: 17px; + } +} + +.el-form-item__label { + font-weight: normal; + font-size: 13px; +} + +.el-button > span { + font-size: 12px; +} + +.el-input__inner { + font-size: 13px !important; +} + +.el-table-v2 { + * { + font-size: 12px; + } + + --el-text-color-secondary: rgb(0, 0, 0); + + .el-table-v2__row { + color: #606266; + } + + .el-table-v2__row-cell, .el-table-v2__header-cell { + justify-content: center; + text-align: center; + } + + .el-table-v2__row { + &:nth-child(even) { + background: var(--el-fill-color-lighter); + } + } + + background: var(--el-table-row-hover-bg-color); + border: var(--el-table-border); + + .el-table-v2__header-wrapper { + border-right: 1px var(--el-table-border-color) solid; + background: var(--el-table-row-hover-bg-color); + } + + .el-table-v2__header { + background: var(--el-table-row-hover-bg-color); + } + + .el-table-v2__header-cell { + background: var(--el-table-row-hover-bg-color); + border-right: 1px var(--el-table-border-color) solid; + } + + .el-table-v2__row-cell { + border-right: 1px var(--el-table-border-color) solid; + } + + .el-vl__wrapper.el-table-v2__body { + border-right: 1px var(--el-table-border-color) solid; + } + + .el-table-v2__header-cell:last-child { + border-right: 0; + } + + .el-table-v2__row-cell:last-child { + border-right: 0; + } +} + +.el-pagination .el-select .el-select__clear { + display: none !important; +} + +.el-icon svg { + font-size: unset; +} diff --git a/css/index.scss b/css/index.scss new file mode 100644 index 0000000..e5c0946 --- /dev/null +++ b/css/index.scss @@ -0,0 +1,3 @@ +@import 'element'; +@import 'transition'; +@import 'common'; diff --git a/css/transition.scss b/css/transition.scss new file mode 100644 index 0000000..59a12a4 --- /dev/null +++ b/css/transition.scss @@ -0,0 +1,36 @@ +//router-view动画 +.view-leave-active { + opacity: 1; + transform: scaleY(1); + transition: all .5s; + transform-origin: center top; +} + +.view-enter-active .view-leave-active { + transform-origin: center bottom; +} + +.view-enter-from, .view-leave-active { + opacity: 0; + transform: scaleY(0); +} + +//面包屑动画 +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/directives/permission/index.js b/directives/permission/index.js new file mode 100644 index 0000000..05288c3 --- /dev/null +++ b/directives/permission/index.js @@ -0,0 +1,14 @@ +export default { + /** + * 安装权限指令插件 + * @param {import('vue').App} app - Vue应用实例 + */ + install: (app) => { + app.directive("permission", { + mounted(el, { value }) { + if (!value) throw new Error("传入的 value 参数无效"); + if (window.permissions && !window.permissions.includes(value)) el.parentNode?.removeChild(el); + } + }); + }, +}; diff --git a/dynamicRouter/index.js b/dynamicRouter/index.js new file mode 100644 index 0000000..0ffc7b8 --- /dev/null +++ b/dynamicRouter/index.js @@ -0,0 +1,197 @@ +import { cloneDeep } from "lodash-es"; +import { conversionRouterMeta, conversionNavMeta } from "../conversionRouterMeta/index.js"; +import { resetQueryCriteria } from "../hooks/useQueryCriteria/index.js"; +import { postRequest } from "../axios/index.js"; +import Children from '../components/children/index.vue' + +/** + * 动态路由配置对象 + */ +let dynamicRouterConfig = { + router: null, + stores: { + routerStore: null, + menuStore: null, + userStore: null, + navStore: null + }, + modules: null, + childrenComponent: Children, + getRouterUrl: '/sys/menu/nav', + loginPath: '/login', + notFoundPath: '/404', + parentRouteName: 'app' +}; + +// 用来获取后台拿到的路由 +let storageRouter = null; + +// 后台返回的默认选中的导航 +let defaultSelectionNav = ""; + +/** + * 配置动态路由 + * @param {Object} config - 配置对象 + * @param {Object} config.router - Vue Router 实例 + * @param {Object} config.stores - Pinia Store 实例集合 + * @param {Object} config.stores.routerStore - 路由状态管理 + * @param {Object} config.stores.menuStore - 菜单状态管理 + * @param {Object} config.stores.userStore - 用户状态管理 + * @param {Object} config.stores.navStore - 导航状态管理 + * @param {Object} config.modules - 组件模块映射 + * @param {Object} [config.childrenComponent] - 路由为children时渲染的组件 + * @param {string} [config.getRouterUrl='/sys/menu/nav'] - 获取菜单数据的API端点,要求返回 { menuList, permissions, navList, defaultSelection } + * @param {string} [config.loginPath='/login'] - 登录页面路径 + * @param {string} [config.notFoundPath='/404'] - 404页面路径 + * @param {string} [config.parentRouteName='app'] - 父路由名称 + */ +export function configureDynamicRouter(config) { + if (!config.router) throw new Error('router 参数必传'); + if (!config.stores || !config.stores.routerStore || !config.stores.menuStore || + !config.stores.userStore || !config.stores.navStore) + throw new Error('stores (routerStore, menuStore, userStore, navStore) 必传'); + if (!config.modules) throw new Error('modules 参数必传'); + + dynamicRouterConfig = { + router: config.router, + stores: config.stores, + modules: config.modules, + childrenComponent: config.childrenComponent || Children, + getRouterUrl: config.getRouterUrl || '/sys/menu/nav', + loginPath: config.loginPath || '/login', + notFoundPath: config.notFoundPath || '/404', + parentRouteName: config.parentRouteName || 'app' + }; + + setupRouterGuard(); +} + +/** + * 设置路由守卫 + */ +function setupRouterGuard() { + const { router, stores } = dynamicRouterConfig; + + router.beforeEach(async (to, from, next) => { + const { routerStore, userStore, navStore } = stores; + // 需要登陆 + if (to.meta.isLogin !== false) { + if (!userStore.getUserInfo.userId) { + next(dynamicRouterConfig.loginPath); + return; + } + if (!storageRouter) { + // 变量里没有储存路由 + if (routerStore.getRouters.length === 0) { + // pinia里没有储存路由,去后台获取路由 + const { menuList, permissions, navList, defaultSelection } = await postRequest(dynamicRouterConfig.getRouterUrl); + // 后台返回的默认选中导航 + defaultSelectionNav = defaultSelection; + // 后台返回的权限 + userStore.setPermissions(permissions); + // 后台返回的路由 + storageRouter = conversionRouterMeta(menuList); + // 后台返回的导航 + navStore.setNavList(conversionNavMeta(navList)); + // 存储路由 + routerStore.setRouters(storageRouter); + // 执行路由跳转方法 + routerGo(to, next); + } else { + // pinia里储存了路由 + // 拿到路由 + storageRouter = routerStore.getRouters; + // 执行路由跳转方法 + routerGo(to, next); + } + } else { + next(); + } + } else { + // 不需要登陆,清空储存路由 + resetDynamicRouter(); + next(); + } + }); +} + +function routerGo(to, next) { + const { router, stores, parentRouteName, notFoundPath } = dynamicRouterConfig; + const { menuStore, userStore } = stores; + // 储存权限,给permission指令使用 + window.permissions = userStore.getPermissions; + // 过滤路由 + storageRouter = filterAsyncRouter(cloneDeep(storageRouter)); + for (let i = 0; i < storageRouter.length; i++) { + // 动态添加路由 + router.addRoute(parentRouteName, storageRouter[i]); + } + // 将404路由添加到最后 + router.addRoute({ path: "/:pathMatch(.*)*", redirect: notFoundPath }); + for (let i = 0; i < router.options.routes.length; i++) { + if (router.options.routes[i].path === "/") { + // 将路由数据存到一个新的pinia里,做菜单渲染 + menuStore.setMenus(router.options.routes[i].children.concat(storageRouter)); + // 如果没有选中任何导航,则默认选中一个 + if (!menuStore.getModel) menuStore.setModel(defaultSelectionNav); + break; + } + } + // 等待addRoute执行完毕跳转路由 + next({ ...to, replace: true }); +} + +function filterAsyncRouter(asyncRouterMap) { + // 遍历后台传来的路由字符串,转换为组件对象 + return asyncRouterMap.filter((route) => { + // 后台将name存成了routeKey,将meta.title存成了name,这里使用routeKey作为name + route.name = route.routeKey || ""; + route.props = route.props === 1; + if (route.component) { + if (route.component === "children") { + route.component = dynamicRouterConfig.childrenComponent; + } else { + const { modules } = dynamicRouterConfig; + if (route.component.charAt(0) === "/") { + // 如果路径的第一位是/,则直接使用modules里的组件 + route.component = modules[`./views${route.component}.vue`]; + } else { + // 如果不是,则拼接成路径 + route.component = modules[`./views/${route.component}.vue`]; + } + } + } + if (route.list && route.list.length) { + // 如果存在子级递归 + route.children = filterAsyncRouter(route.list); + // 因为后台返回的子级是list,所以需要删除 + delete route.list; + } + return true; + }); +} + +/** + * 重置动态路由状态 + * 用于登出或重新配置时清理状态 + */ +export function resetDynamicRouter() { + storageRouter = null; + defaultSelectionNav = ""; + const { stores } = dynamicRouterConfig; + const { routerStore, menuStore, navStore, userStore } = stores; + routerStore?.$reset(); + menuStore?.$reset(); + navStore?.$reset(); + userStore?.$reset(); + window.permissions = undefined; + resetQueryCriteria(); +} + +/** + * 获取当前存储的路由数据 + * @returns {Array|null} 路由数据数组或null + */ +export function getStorageRouter() { + return storageRouter; +} diff --git a/enum/formItemType/index.js b/enum/formItemType/index.js new file mode 100644 index 0000000..8079527 --- /dev/null +++ b/enum/formItemType/index.js @@ -0,0 +1,35 @@ +/** + * 表单项类型枚举 + */ +const formItemTypeEnum = { + // 映射为el-input + INPUT: 'INPUT', + // 映射为el-input的textarea + TEXTAREA: 'TEXTAREA', + // 映射为el-input-number + INPUT_NUMBER: 'INPUT_NUMBER', + // 映射为el-input-number + NUMBER: 'NUMBER', + // 映射为el-select + SELECT: 'SELECT', + // 映射为el-radio-group + RADIO: 'RADIO', + // 映射为el-checkbox-group + CHECKBOX: 'CHECKBOX', + // 映射为el-date-picker,日期格式为YYYY-MM-DD + DATE: 'DATE', + // 映射为el-date-picker,日期格式为YYYY-MM + DATE_MONTH: 'DATE_MONTH', + // 映射为el-date-picker,日期格式为YYYY + DATE_YEAR: 'DATE_YEAR', + // 映射为el-date-picker,日期格式为YYYY-MM-DD + DATE_RANGE: 'DATE_RANGE', + // 映射为el-date-picker,日期格式为YYYY-MM-DD HH:mm:ss + DATETIME: 'DATETIME', + // 映射为el-date-picker,日期格式为YYYY-MM-DD HH:mm:ss + DATETIME_RANGE: 'DATETIME_RANGE', + // 映射为el-divider + DIVIDER: 'DIVIDER', +}; + +export default formItemTypeEnum; diff --git a/hooks/useDataDictionary/index.js b/hooks/useDataDictionary/index.js new file mode 100644 index 0000000..59961d9 --- /dev/null +++ b/hooks/useDataDictionary/index.js @@ -0,0 +1,18 @@ +import { ref } from "vue"; +import { getDataType } from "../../utils/index.js"; + +/** + * @param {Function} api - 接口请求函数,用于获取数据字典内容 + * @param {String} id - 数据字典的id + * @returns {Array} 返回对象包含以下属性: + * - {Ref} 0 - 数据字典数据,使用 Vue 的 ref 包裹的数组,包含字典项列表 + */ +export default function useDataDictionary(api, id) { + if (getDataType(api) !== "Function" && getDataType(api) !== "AsyncFunction") + throw new Error("api必须是一个函数"); + const data = ref([]); + api({ parentId: id }).then((res) => { + data.value = res; + }); + return [data]; +} diff --git a/hooks/useDownloadBlob/index.js b/hooks/useDownloadBlob/index.js new file mode 100644 index 0000000..ebaf6f8 --- /dev/null +++ b/hooks/useDownloadBlob/index.js @@ -0,0 +1,58 @@ +import { ElMessage } from "element-plus"; +import dayjs from "dayjs"; +import {getFileUrl} from "../../utils/index.js"; + +/** + * @param {string} url - 请求的 API 地址(GET) + * @param {Object} [option] - 配置项(可选) + * @param {string} [option.name] - 下载文件的自定义文件名(不含后缀),默认为当前时间戳 + * @param {string} [option.type] - Blob 对象的 MIME 类型,默认为 Excel 类型 + * @param {Object} [option.params] - 请求时携带的查询参数对象 + * @returns {Promise} 如果下载成功则 resolve 响应数据,失败则 reject 错误信息 + */ +export default function useDownloadBlob( + url, + option = { name: "", type: "", params: {} } +) { + const FILE_URL = getFileUrl(); + return new Promise((resolve, reject) => { + const finalUrl = url.indexOf(FILE_URL) === -1 ? FILE_URL + url : url; + Object.entries(option.params).forEach(([key, value]) => { + finalUrl.searchParams.append(key, value); + }); + fetch(finalUrl, { + method: "GET", + mode: "cors", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.blob(); + }) + .then((blob) => { + // 保持原有下载逻辑 + const finalBlob = new Blob([blob], { + type: option.type || "application/vnd.ms-excel", + }); + const downloadElement = document.createElement("a"); + const href = window.URL.createObjectURL(finalBlob); + downloadElement.style.display = "none"; + downloadElement.href = href; + downloadElement.download = + option.name || dayjs().format("YYYY-MM-DD HH:mm:ss"); + document.body.appendChild(downloadElement); + downloadElement.click(); + document.body.removeChild(downloadElement); + window.URL.revokeObjectURL(href); + resolve({ data: finalBlob }); + }) + .catch((err) => { + ElMessage.error("导出失败"); + reject(err); + }); + }); +} diff --git a/hooks/useDownloadFile/index.js b/hooks/useDownloadFile/index.js new file mode 100644 index 0000000..9a6b90e --- /dev/null +++ b/hooks/useDownloadFile/index.js @@ -0,0 +1,32 @@ +import { ElMessage, ElMessageBox } from "element-plus"; +import { getFileName, getFileSuffix, getFileUrl } from "../../utils/index.js"; + +/** + * @param {string} url - 文件的下载地址 URL + * @param {string} [name] - 可选的文件名,不带后缀。若未提供,则从 URL 中提取文件名 + * @returns {Promise} 无返回值,但会触发浏览器下载行为 + */ +export default async function useDownloadFile(url, name) { + if (!url) throw new Error("没有下载地址"); + await ElMessageBox.confirm("确定要下载此文件吗?", { type: "warning" }); + const FILE_URL = getFileUrl(); + if (name) { + if (!getFileSuffix(url)) name = name + getFileSuffix(url); + } else name = getFileName(url); + fetch(url.indexOf(FILE_URL) === -1 ? FILE_URL + url : url) + .then((res) => res.blob()) + .then((blob) => { + const a = document.createElement("a"); + document.body.appendChild(a); + a.style.display = "none"; + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = `${name}`; + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }) + .catch(() => { + ElMessage.error("下载失败"); + }); +} diff --git a/hooks/useForm/index.js b/hooks/useForm/index.js new file mode 100644 index 0000000..f2eef04 --- /dev/null +++ b/hooks/useForm/index.js @@ -0,0 +1,41 @@ +import { ElMessage } from "element-plus"; +import { useTemplateRef } from "vue"; + +/** + * @returns {Object} 返回一个包含以下属性的对象: + * - validate {Function} 触发表单验证的异步方法 + * - formRef {Ref} 表单引用对象(用于 Vue 模板绑定) + * - reset {Function} 重置表单字段的方法 + */ +export default function useForm() { + const formRef = useTemplateRef("formRef"); + /** + * @param {string} [message="请补全必填项!"] - 验证失败时显示的提示信息 + * @returns {Promise} 验证通过返回 resolve(true),失败 reject(false) + */ + const validate = (message = "请补全必填项!") => { + return new Promise((resolve, reject) => { + formRef.value.validate((valid) => { + if (valid) { + resolve(valid); + } else { + reject(valid); + ElMessage.warning(message); + setTimeout(() => { + const element = document.querySelectorAll( + ".el-form-item__error" + )[0]; + element.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }, 100); + } + }); + }); + }; + const reset = () => { + formRef.value.resetFields(); + }; + return { validate, formRef, reset }; +} diff --git a/hooks/useIsExistenceDuplicateSelection/index.js b/hooks/useIsExistenceDuplicateSelection/index.js new file mode 100644 index 0000000..e1895f1 --- /dev/null +++ b/hooks/useIsExistenceDuplicateSelection/index.js @@ -0,0 +1,24 @@ +import { uniqBy } from "lodash-es"; +import { ElMessage } from "element-plus"; + +/** + * @param {Array} list - 需要检查重复项的目标数组 + * @param {string} key - 用于去重判断的对象属性名 + * @param {string} [message="存在重复项,请勿重复选择"] - 可选的错误提示信息 + * @returns {Promise} 如果无重复项则 resolve,存在重复项时 reject 并提示错误 + */ +export default function useIsExistenceDuplicateSelection( + list, + key, + message = "存在重复项,请勿重复选择" +) { + return new Promise((resolve, reject) => { + const newList = uniqBy(list, key); + if (newList.length !== list.length) { + ElMessage.error(message); + reject(new Error(message)); + } else { + resolve(); + } + }); +} diff --git a/hooks/useListData/index.js b/hooks/useListData/index.js new file mode 100644 index 0000000..3c2f2c6 --- /dev/null +++ b/hooks/useListData/index.js @@ -0,0 +1,177 @@ +import { nextTick, ref } from "vue"; +import { getDataType } from "../../utils/index.js"; +import { + getQueryCriteria, + setQueryCriteria, +} from "../useQueryCriteria/index.js"; + +const verificationParameter = (api, options) => { + if (getDataType(api) !== "Function") throw new Error("api必须是一个函数"); + if (getDataType(options) !== "Object") + throw new Error("options必须是一个对象"); + if (options.immediate && getDataType(options.immediate) !== "Boolean") + throw new Error("options.immediate必须是一个布尔值"); + if (options.usePagination && getDataType(options.usePagination) !== "Boolean") + throw new Error("options.usePagination必须是一个布尔值"); + if (options.key && getDataType(options.key) !== "String") + throw new Error("options.key必须是一个字符串"); + if ( + options.callback && + getDataType(options.callback) !== "Function" && + getDataType(options.callback) !== "AsyncFunction" + ) + throw new Error("options.callback必须是一个函数"); + if ( + options.before && + getDataType(options.before) !== "Function" && + getDataType(options.before) !== "AsyncFunction" + ) + throw new Error("options.before必须是一个函数"); + if ( + options.defaultSearchForm && + getDataType(options.defaultSearchForm) !== "Object" + ) + throw new Error("options.defaultSearchForm必须是一个对象"); + if ( + options.clearSelection && + getDataType(options.clearSelection) !== "Boolean" + ) + throw new Error("options.clearSelection必须是一个布尔值"); + if ( + options.params && + getDataType(options.params) !== "Object" && + getDataType(options.params) !== "Function" + ) + throw new Error("options.params必须是一个对象或者一个函数"); + if ( + options.isStorageQueryCriteria && + getDataType(options.isStorageQueryCriteria) !== "Boolean" + ) + throw new Error("options.isStorageQueryCriteria必须是一个布尔值"); + if ( + options.tabsActiveName && + getDataType(options.tabsActiveName) !== "String" + ) + throw new Error("options.tabsActiveName必须是一个字符串"); +}; + +const getOptionParams = (params) => { + if (params) { + if (getDataType(params) === "Object") return params; + const paramsValue = params(); + if (getDataType(paramsValue) !== "Object") + throw new Error("options.params为函数时必须存在返回值并且必须是一个对象"); + else return paramsValue; + } +}; + +const getBeforeParams = (before, params) => { + if (before) { + const paramsValue = before(JSON.parse(JSON.stringify(params))); + if (getDataType(paramsValue) !== "Object") + throw new Error("options.before必须存在返回值并且必须是一个对象"); + else return paramsValue; + } +}; + +/** + * @param {Function} api - 接口请求函数,用于获取列表数据 + * @param {Object} [options] - 配置项(可选) + * @param {Function} [options.callback] - 数据获取完成后的回调函数,第一个参数为列表数据,第二个参数为接口返回数据 + * @param {Function} [options.before] - 请求前执行的钩子函数,接收当前查询参数作为参数,必须返回一个对象 + * @param {Object|Function} [options.params] - 额外的请求参数,可以是对象或返回对象的函数 + * @param {Object} [options.defaultSearchForm] - 搜索表单默认值 + * @param {boolean} [options.immediate=true] - 是否立即执行接口请求 + * @param {boolean} [options.usePagination=true] - 是否使用分页功能 + * @param {string} [options.key="list"] - 响应数据中存放列表数据的字段名 + * @param {boolean} [options.clearSelection=true] - 调用 resetPagination 时是否清空表格选择项 + * @param {boolean} [options.isStorageQueryCriteria=true] - 是否缓存当前查询条件 + * @param {string} [options.tabsActiveName] - 当存在 Tabs 组件时,当前激活的 tab 名称,用于区分查询缓存 + * @return {Object} 返回对象包含以下属性:list 表格数据,pagination 分页数据,searchForm 搜索表单数据,tableRef 表格实例,getData 获取数据函数,resetPagination 重置分页函数 + * @returns {Object} 返回对象包含以下属性: + * - [list] {Ref} 表格数据,使用 Vue 的 ref 包裹的数组 + * - [pagination] {Ref<{currentPage:number,pageSize:number,total:number}>} 分页信息对象,包含 currentPage、pageSize、total 字段,使用 ref 包裹 + * - [searchForm] {Ref} 搜索表单数据对象,使用 Vue 的 ref 包裹 + * - [tableRef] {Ref} 表格实例ref,可用于调用表格方法 + * - [getData] {Function} 获取或刷新列表数据的异步函数 + * - [resetPagination] {Function} 重置分页并刷新数据的异步函数 + */ + +export default function useListData(api, options = {}) { + verificationParameter(api, options); + const immediate = options.immediate ?? true; + const usePagination = options.usePagination ?? true; + const key = options.key ?? "list"; + const defaultSearchForm = options.defaultSearchForm ?? {}; + const clearSelection = options.clearSelection ?? true; + const isStorageQueryCriteria = options.isStorageQueryCriteria ?? true; + const defaultPagination = { currentPage: 1, pageSize: 20, total: 0 }; + const list = ref([]); + const queryCriteria = getQueryCriteria(options.tabsActiveName); + const pagination = ref(queryCriteria.pagination || defaultPagination); + const searchForm = ref(JSON.parse(JSON.stringify(defaultSearchForm))); + const tableRef = ref(null); + const getData = async () => { + const resData = await api({ + ...(usePagination + ? { + curPage: pagination.value.currentPage, + limit: pagination.value.pageSize, + } + : {}), + ...searchForm.value, + ...(queryCriteria.searchForm || {}), + ...(getBeforeParams( + options.before, + queryCriteria.searchForm || searchForm.value + ) || {}), + ...(getOptionParams(options.params) || {}), + }); + if (usePagination) { + if (resData.page[key]) list.value = resData.page[key]; + else list.value = resData[key] || resData.varList; + } else list.value = resData[key] || resData.varList; + if (usePagination) + pagination.value.total = + resData.page.totalCount || resData.page.totalResult; + options.callback && options.callback(list.value, resData); + !usePagination && + clearSelection && + tableRef.value && + tableRef.value.clearSelection(); + if (isStorageQueryCriteria) { + setQueryCriteria( + { + searchForm: { + ...searchForm.value, + ...(queryCriteria.searchForm || {}), + }, + pagination: pagination.value, + }, + options.tabsActiveName + ); + await nextTick(); + searchForm.value = queryCriteria.searchForm || searchForm.value; + } + }; + immediate && getData().then(); + const resetPagination = async () => { + list.value = []; + pagination.value = defaultPagination; + await nextTick(); + await getData(); + clearSelection && tableRef.value && tableRef.value.clearSelection(); + const cloneSearchForm = searchForm.value; + searchForm.value = {}; + await nextTick(); + searchForm.value = cloneSearchForm; + }; + return { + list, + pagination, + searchForm, + tableRef, + getData: async () => await getData(), + resetPagination: async () => await resetPagination(), + }; +} diff --git a/hooks/useQueryCriteria/index.js b/hooks/useQueryCriteria/index.js new file mode 100644 index 0000000..d502971 --- /dev/null +++ b/hooks/useQueryCriteria/index.js @@ -0,0 +1,43 @@ +import { useQueryCriteriaStore } from "../../pinia/queryCriteria/index.js"; + +function getCriteriaKey(tabsActiveName) { + const key = window.location.href; + return tabsActiveName ? `${key}/${tabsActiveName}` : key; +} + +export const getQueryCriteria = (tabsActiveName) => { + const miscellaneousStore = useQueryCriteriaStore(); + const criteriaKey = getCriteriaKey(tabsActiveName); + const queryCriteria = miscellaneousStore.getQueryCriteria[criteriaKey] || {}; + return { + pagination: queryCriteria.pagination, + searchForm: queryCriteria.searchForm, + tabsActiveName, + }; +}; +export const setQueryCriteria = (data, tabsActiveName) => { + const miscellaneousStore = useQueryCriteriaStore(); + const criteriaKey = getCriteriaKey(tabsActiveName); + miscellaneousStore.setQueryCriteria({ + ...miscellaneousStore.getQueryCriteria, + [criteriaKey]: { + ...miscellaneousStore.getQueryCriteria[criteriaKey], + ...data, + }, + }); +}; + +export const resetQueryCriteria = () => { + const miscellaneousStore = useQueryCriteriaStore(); + miscellaneousStore.resetQueryCriteria(); +}; + +export const getTabsActiveName = () => { + const miscellaneousStore = useQueryCriteriaStore(); + return miscellaneousStore.getQueryCriteriaTabsActiveName; +}; + +export const setTabsActiveName = (name) => { + const miscellaneousStore = useQueryCriteriaStore(); + miscellaneousStore.setQueryCriteriaTabsActiveName(name); +}; diff --git a/hooks/useRequestLoading/index.js b/hooks/useRequestLoading/index.js new file mode 100644 index 0000000..1ca8e34 --- /dev/null +++ b/hooks/useRequestLoading/index.js @@ -0,0 +1,20 @@ +import { customRef } from "vue"; + +const loading = customRef((track, trigger) => { + let loadingCount = 0; + return { + get() { + track(); + return loadingCount > 0; + }, + set(value) { + loadingCount += value ? 1 : -1; + loadingCount = Math.max(0, loadingCount); + trigger(); + }, + }; +}); + +export default function useRequestLoading() { + return loading; +} diff --git a/hooks/useUploadFile/index.js b/hooks/useUploadFile/index.js new file mode 100644 index 0000000..9ffd54c --- /dev/null +++ b/hooks/useUploadFile/index.js @@ -0,0 +1,41 @@ +import { unref } from "vue"; +import {uploadRequest} from "../../axios/index.js"; + +/** + * @description 用于处理单个或多个文件上传,传入数组是批量上传,传入对象是单个上传 + * @param {File|File[]} params - 要上传的文件或文件数组,可以是响应式对象 + * @param {string|number} type - 文件类型标识 + * @param {Object} [api] - 可选的自定义 API 对象 + * @param {Function} [api.batch="/img-files/batch"] - 批量上传文件的 API 函数,接受 formData 参数 + * @param {Function} [api.single="/img-files"] - 单个文件上传的 API 函数,接受 formData 参数 + * @returns {Promise} 上传成功后返回包含文件信息的对象或数组,传入数组会返回数组,传入对象会返回对象 + * */ +export default async function useUploadFile( + params, + type, + api = { + batch: (formData) => uploadRequest('/img-files/batch', formData), + single: (formData) => uploadRequest('/img-files', formData), + } +) { + const file = unref(params); + const isArray = Array.isArray(file); + const formData = new FormData(); + if (isArray) { + file.forEach((f) => { + f.raw && formData.append("files", f.raw); + }); + } else { + file.raw && formData.append("files", file.raw); + } + formData.append("type", type); + const filesLength = formData.getAll("files").length; + if (filesLength === 0) return file; + if (isArray) { + const { data } = await api.batch(formData); + return [...data, ...file.filter((f) => !f.raw)]; + } else { + const { data } = await api.single(formData); + return data; + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..fc1d8b9 --- /dev/null +++ b/index.js @@ -0,0 +1,97 @@ +// 组件库主入口文件 + +// ============================================================================= +// 导入所有组件 +// ============================================================================= + +import AppFormBuilder from './components/form_builder/index.vue'; +import AppTable from './components/table/index.vue'; +import AppSearch from './components/search/index.vue'; +import AppUpload from './components/upload/index.vue'; +import AppPagination from './components/pagination/index.vue'; +import AppEditor from './components/editor/index.vue'; +import AppQrCode from './components/qr_code/index.vue'; +import AppInfoBuilder from './components/info_builder/index.vue'; +import AppPreviewImg from './components/preview_img/index.vue'; +import AppTooltipImg from './components/tooltip_img/index.vue'; +import AppPdf from './components/pdf/index.vue'; +import AppPreviewPdf from './components/preview_pdf/index.vue'; +import AppAliPlayer from './components/ali-player/index.vue'; +import AppVideo from './components/video/index.vue'; +import AppSign from './components/sign/index.vue'; +import AppVerification from './components/verification/index.vue'; +import AppVerificationCode from './components/verification_code/index.vue'; +import AppTxt from './components/txt/index.vue'; +import AppViewTree from './components/view_tree/index.vue'; +import AppMap from './components/map/index.vue'; +import AppMapSelector from './components/map/map.vue'; +import AppImportFile from './components/import_file/index.vue'; +import AppCascader from './components/cascader/index.vue'; +import AppLayout from './layout/index.vue'; + +// ============================================================================= +// 导入所有工具函数和hooks +// ============================================================================= + +// Axios +export { configureAxios, postRequest, getRequest, putRequest, deleteRequest, patchRequest, uploadRequest } from './axios/index.js'; + +// Hooks 相关 +export { default as useDataDictionary } from './hooks/useDataDictionary/index.js'; +export { default as useDownloadBlob } from './hooks/useDownloadBlob/index.js'; +export { default as useDownloadFile } from './hooks/useDownloadFile/index.js'; +export { default as useForm } from './hooks/useForm/index.js'; +export { default as useIsExistenceDuplicateSelection } from './hooks/useIsExistenceDuplicateSelection/index.js'; +export { default as useListData } from './hooks/useListData/index.js'; +export { default as useRequestLoading } from './hooks/useRequestLoading/index.js'; +export { default as useUploadFile } from './hooks/useUploadFile/index.js'; +export * from './hooks/useQueryCriteria/index.js'; + +// 工具函数 +export * from './utils/index.js'; +export * from './regular/index.js'; +export { conversionRouterMeta, conversionNavMeta } from './conversionRouterMeta/index.js'; +export { default as formItemTypeEnum } from './enum/formItemType/index.js'; + +// 指令 +export { default as permissionDirective } from './directives/permission/index.js'; + +// Pinia Store +export * from './pinia/queryCriteria/index.js'; + +// 动态路由 +export { configureDynamicRouter, resetDynamicRouter, getStorageRouter } from './dynamicRouter/index.js'; + +// AES加密服务 +export { configureAesSecret, aesEncrypt, aesDecrypt } from './aesSecret/index.js'; + +// ============================================================================= +// 按需导出组件 +// ============================================================================= +export { + AppFormBuilder, + AppTable, + AppSearch, + AppUpload, + AppPagination, + AppEditor, + AppQrCode, + AppInfoBuilder, + AppPreviewImg, + AppTooltipImg, + AppPdf, + AppPreviewPdf, + AppAliPlayer, + AppVideo, + AppSign, + AppVerification, + AppVerificationCode, + AppTxt, + AppViewTree, + AppMap, + AppMapSelector, + AppImportFile, + AppCascader, + AppLayout, +}; + diff --git a/layout/breadcrumb/index.vue b/layout/breadcrumb/index.vue new file mode 100644 index 0000000..76f3143 --- /dev/null +++ b/layout/breadcrumb/index.vue @@ -0,0 +1,62 @@ + + + + + + + + {{ item.meta.title }} + + + {{ item.meta.title }} + + {{ item.meta.title }} + + + + + + + + + + diff --git a/layout/header/components/change_password.vue b/layout/header/components/change_password.vue new file mode 100644 index 0000000..b582aca --- /dev/null +++ b/layout/header/components/change_password.vue @@ -0,0 +1,72 @@ + + + + + 确认修改 + 关闭 + + + + + + + diff --git a/layout/header/index.vue b/layout/header/index.vue new file mode 100644 index 0000000..a145542 --- /dev/null +++ b/layout/header/index.vue @@ -0,0 +1,206 @@ + + + + {{ logoValueTitle }} + + + + + + + + + + + 更多导航栏 + + + + + + {{ item.title }} + + + + + + + {{ item.title }} + + + + + + + + + + {{ userName }} + + + + + + 修改密码 + 退出 + + + + + + + + + + + + diff --git a/layout/header/tx.png b/layout/header/tx.png new file mode 100644 index 0000000..3f55e30 Binary files /dev/null and b/layout/header/tx.png differ diff --git a/layout/index.vue b/layout/index.vue new file mode 100644 index 0000000..3ca4165 --- /dev/null +++ b/layout/index.vue @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{route.matched.filter((item) => item.meta?.title).at(-1).meta.title }} + + + + + + + + + + + + + + + diff --git a/layout/menu/index.vue b/layout/menu/index.vue new file mode 100644 index 0000000..95251f5 --- /dev/null +++ b/layout/menu/index.vue @@ -0,0 +1,83 @@ + + + + + + {{ menu.meta?.title }} + + + + + + {{ menu.meta?.title }} + + + + + + + + + + diff --git a/npm b/npm new file mode 100644 index 0000000..be4bbfa --- /dev/null +++ b/npm @@ -0,0 +1,6 @@ +# npm地址 +https://www.npmjs.com/package/zy-vue-library + +# npm账号 +liujianan15703339975 +Ljn15703339975. diff --git a/package.json b/package.json new file mode 100644 index 0000000..25b9107 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "zy-vue-library", + "private": false, + "version": "1.1.5", + "type": "module", + "description": "", + "author": "LiuJiaNan", + "license": "MIT", + "module": "index.js", + "files": [ + "aesSecret", + "axios", + "components", + "conversionRouterMeta", + "css", + "directives", + "dynamicRouter", + "enum", + "hooks", + "layout", + "pinia", + "regular", + "utils", + "index.js", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "element-plus": "^2.11.2", + "vue": "^3.5.0", + "vue-router": "^4.5.0" + }, + "scripts": { + "postinstall": "echo 'Thanks for using our component library!'" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@vue-office/pdf": "^2.0.10", + "@vueuse/core": "^12.8.2", + "@vueuse/integrations": "^12.8.2", + "@wangeditor/editor-for-vue": "^5.1.12", + "axios": "^1.12.2", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.18", + "lodash-es": "^4.17.21", + "pinia": "^3.0.3", + "qrcode": "^1.5.4", + "throttle-debounce": "^5.0.2", + "v-viewer": "^3.0.22", + "vue-esign": "^1.1.4", + "vue3-puzzle-vcode": "^1.1.7" + } +} diff --git a/pinia/queryCriteria/index.js b/pinia/queryCriteria/index.js new file mode 100644 index 0000000..d7173b3 --- /dev/null +++ b/pinia/queryCriteria/index.js @@ -0,0 +1,31 @@ +import { defineStore } from "pinia"; + +export const useQueryCriteriaStore = defineStore("queryCriteriaStore", { + state: () => ({ + queryCriteria: {}, + queryCriteriaTabsActiveName: "", + }), + getters: { + getQueryCriteria() { + return this.queryCriteria; + }, + getQueryCriteriaTabsActiveName() { + return this.queryCriteriaTabsActiveName; + }, + }, + actions: { + setQueryCriteria(data) { + this.queryCriteria = data; + }, + setQueryCriteriaTabsActiveName(data) { + this.queryCriteriaTabsActiveName = data; + }, + resetQueryCriteria() { + this.queryCriteria = {}; + this.queryCriteriaTabsActiveName = ""; + }, + }, + persist: { + storage: window.sessionStorage, + }, +}); diff --git a/regular/index.js b/regular/index.js new file mode 100644 index 0000000..1d32046 --- /dev/null +++ b/regular/index.js @@ -0,0 +1,56 @@ +/** + * 匹配中国手机号码,可包含国家代码86,支持各种运营商号段。 + */ +export const PHONE = + /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-7|9])|(?:5[0-3|5-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1|8|9]))\d{8}$/; + +/** + * 匹配中国大陆的统一社会信用代码。 + */ +export const UNIFIED_SOCIAL_CREDIT_CODE = + /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/; + +/** + * 匹配中国大陆的身份证号码,包括15位和18位号码,并验证最后一位校验码。 + */ +export const ID_NUMBER = + /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/; + +/** + * 匹配中国大陆的移动电话号码,不包含国家代码。 + */ +export const MOBILE_PHONE = + /^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\d{8}$/; + +/** + * 匹配浮点数,允许整数、一位或两位小数,以及零的情况。 + */ +export const FLOATING_POINT_NUMBER = + /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/; + +/** + * 两位小数。 + */ +export const TWO_DECIMAL_PLACES = /^\d+\.\d{2}$/; + +/** + * 一位小数(非必须)。 + */ +export const ONE_DECIMAL_PLACES = /^\d+(\.\d{0,1})?$/; + +/** + * 匹配中国大陆的车牌号码。 + */ +export const LICENSE_PLATE_NUMBER = + /^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1})$/; + +/** + * 匹配强密码,要求至少8个字符,包含大小写字母、数字和特殊字符。 + */ +export const STRONG_PASSWORD = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{8,}$/; + +/** + * 匹配完整的HTML标签,包括开始标签和结束标签。 + */ +export const HTML_TAG = /<\/?[^>]*>/g; diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..2c7ac38 --- /dev/null +++ b/utils/index.js @@ -0,0 +1,520 @@ +import { ElMessage } from "element-plus"; + +/** + * @description 计算序号 + * @param {Object} pagination 分页数据对象 + * @param {number | string} pagination.currentPage 当前页 + * @param {number | string} pagination.pageSize 每页条数 + * @param {number} index 当页数据的索引值 + * @return {number} 序号 + **/ +export function serialNumber(pagination, index) { + return (pagination.currentPage - 1) * pagination.pageSize + (index + 1); +} + +/** + * @description 字符串数组转数组 + * @param {string} value 转换的字符串数组 + * @return {Array} 转换后的数组 + **/ +export function toArrayString(value) { + // eslint-disable-next-line no-eval + return value ? eval(value).map(String) : []; +} + +/** + * @description 判断文件后缀名是否符合 + * @param {string} name 文件名字 + * @param {string} suffix 文件后缀 + * @return {boolean} 是否符合 + **/ +export function interceptTheSuffix(name, suffix) { + return ( + name.substring(name.lastIndexOf("."), name.length).toLowerCase() === + suffix.toLowerCase() + ); +} + +/** + * @description 图片转base64 + * @param {string} imgUrl 图片地址 + * @return {Promise} Promise实例,then包含base64编码 + **/ +export function image2Base64(imgUrl) { + return new Promise((resolve) => { + const img = new Image(); + img.src = imgUrl; + img.crossOrigin = "Anonymous"; + img.onload = function () { + const canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, img.width, img.height); + const ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase(); + resolve(canvas.toDataURL("image/" + ext)); + }; + }); +} +export function image2Base642(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = (e) => { + resolve(e.target.result); // 返回 base64 + }; + reader.onerror = (error) => { + reject(error); // 处理错误 + }; + }); +} +/** + * @description 判断图片是否可访问成功 + * @param {string} imgUrl 图片地址 + * @return {Promise} Promise实例 + **/ +export function checkImgExists(imgUrl) { + return new Promise((resolve, reject) => { + const ImgObj = new Image(); + ImgObj.src = imgUrl; + ImgObj.onload = function (res) { + resolve(res); + }; + ImgObj.onerror = function (err) { + reject(err); + }; + }); +} + +/** + * @description 获取数据类型 + * @param {any} data 数据 + * @return {string} 数据类型 + **/ +export function getDataType(data) { + return Object.prototype.toString.call(data).slice(8, -1); +} + +/** + * @description 数组去重 + * @param {Array} arr 去重的数组 + * @return {Array} 去重后的数组 + **/ +export function ArrayDeduplication(arr) { + return [...new Set(arr)]; +} + +/** + * @description 数组对象去重 + * @param {Array} arr 去重的数组 + * @param {string} name 去重的key + * @return {Array} 去重后的数组 + **/ +export function arrayObjectDeduplication(arr, name) { + const obj = {}; + arr = arr.reduce(function (previousValue, currentValue) { + if (!obj[currentValue[name]]) { + obj[currentValue[name]] = true; + previousValue.push(currentValue); + } + return previousValue; + }, []); + return arr; +} + +/** + * @description 查找字符串中指定的值第几次出现的位置 + * @param {Array} str 查找的字符串数组 + * @param {string} char 查找的值 + * @param {number} num 第几次出现 + * @return {number} 出现的位置 + **/ +export function findCharIndex(str, char, num) { + let index = str.indexOf(char); + if (index === -1) return -1; + for (let i = 0; i < num - 1; i++) { + index = str.indexOf(char, index + 1); + if (index === -1) return -1; + } + return index; +} + +/** + * @description 生成指定两个值之间的随机数 + * @param {number} min 最小值 + * @param {number} max 最大值 + * @return {number} 随机数 + **/ +export function randoms(min, max) { + return Math.random() * (max - min + 1) + min; +} + +/** + * @description 千位分隔符 + * @param {number | string} num 转换的值 + * @return {string} 转换后的值 + **/ +export function numFormat(num) { + if (num) { + const numArr = num.toString().split("."); + const arr = numArr[0].split("").reverse(); + let res = []; + for (let i = 0; i < arr.length; i++) { + if (i % 3 === 0 && i !== 0) { + res.push(","); + } + res.push(arr[i]); + } + res.reverse(); + if (numArr[1]) { + res = res.join("").concat("." + numArr[1]); + } else { + res = res.join(""); + } + return res; + } +} + +/** + * @description 验证是否为空 + * @param {any} value 验证的值 + * @return {boolean} 是否为空 + **/ +export function isEmpty(value) { + return ( + value === undefined || + value === null || + (typeof value === "object" && Object.keys(value).length === 0) || + (typeof value === "string" && value.trim().length === 0) + ); +} + +/** + * @description 获取url参数 + * @param {string} name 获取的key + * @return {string} 获取的值 + **/ +export function getUrlParam(name) { + const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); + const r = window.location.search.substr(1).match(reg); + if (r != null) return decodeURI(r[2]); + return ""; +} + +/** + * @description 数据分页 + * @param {Array} list 分页的数组 + * @param {number | string} currentPage 当前页 + * @param {number | string} pageSize 每页条数 + * @return {Array} 分页后的数组 + **/ +export function paging(list, currentPage, pageSize) { + return list.filter((item, index) => { + return ( + index < +currentPage * +pageSize && + index >= (+currentPage - 1) * +pageSize + ); + }); +} + +/** + * @description 获取文件后缀 + * @param {string} name 文件名 + * @return {string} 文件后缀 + **/ +export function getFileSuffix(name) { + return name.substring(name.lastIndexOf(".") + 1); +} + +/** + * @description 获取文件名称 + * @param {string} name 文件地址 + * @return {string} 文件名称 + **/ +export function getFileName(name) { + if (!name) return ""; + return name.substring(name.lastIndexOf("/") + 1); +} + +/** + * @description 读取txt文档 + * @param {string} filePah 文档路径 + * @return {resolve,string} 读取后的内容 + **/ +export function readTxtDocument(filePah) { + return new Promise((resolve) => { + const FILE_URL = getFileUrl(); + const file_url = FILE_URL + filePah; + const xhr = new XMLHttpRequest(); + xhr.open("get", file_url, true); + xhr.responseType = "blob"; + xhr.onload = function (event) { + const reader = new FileReader(); + reader.readAsText(event.target.response, "GB2312"); + reader.onload = function () { + resolve(reader.result); + }; + }; + xhr.send(); + }); +} + +/** + * @description 将秒转换成时分秒 + * @param {string,number} second 需要转换的秒数 + * @return {string} 转换后的时间 + **/ +export function secondConversion(second) { + if (!second) return 0; + const h = parseInt(second / 60 / 60, 10); + const m = parseInt((second / 60) % 60, 10); + const s = parseInt(second % 60, 10); + if (h) { + return h + "小时" + m + "分钟" + s + "秒"; + } else { + if (m) { + return m + "分钟" + s + "秒"; + } else { + return s + "秒"; + } + } +} + +/** + * @description 附件添加前缀 + * @param {Array} list 附件数组 + * @param {Object} options 配置选项 + * @param {string} [options.pathKey="filePath"] 附件路径字段名 + * @param {string} [options.nameKey="fileName"] 附件名称字段名 + * @param {string} [options.idKey="imgFilesId"] 附件id字段名 + * @return {Array} 添加完前缀后的数组 + **/ +export function addingPrefixToFile(list, options = {}) { + if (!list) return []; + const { + pathKey = "filePath", + nameKey = "fileName", + idKey = "imgFilesId", + } = options; + const FILE_URL = getFileUrl(); + for (let i = 0; i < list.length; i++) { + list[i].url = FILE_URL + list[i][pathKey]; + list[i].name = list[i][nameKey] || getFileName(list[i][pathKey]); + list[i].imgFilesId = list[i][idKey]; + } + return list; +} + +/** + * @description 验证重复选择 + * @param {Array} list 验证的数组 + * @param {number} index 选择的索引 + * @param {string} key 验证的字段 + * @param {string} id 验证的值 + **/ +export async function verifyDuplicateSelection(list, index, key, id) { + return new Promise((resolve, reject) => { + if (list.some((item) => item[key] === id)) { + ElMessage.warning("不能重复选择"); + reject(new Error("不能重复选择")); + } else { + list[index][key] = id; + resolve(); + } + }); +} + +/** + * @description 翻译状态 + * @param {number | string} status 状态 + * @param {Array} list 翻译的数组 + * @param {String} idKey + * @param {String} nameKey + * @return {string} 翻译后的状态 + **/ +export function getLabelName(status, list, idKey = "id", nameKey = "name") { + for (let i = 0; i < list.length; i++) { + if (status?.toString() === list[i][idKey]?.toString()) { + return list[i][nameKey]; + } + } +} + +/** + * @description 计算文件大小 + * @param {number | string} size 文件kb + * @return {string} 计算后的文件大小 + **/ +export function calculateFileSize(size) { + return size > 1024 + ? (size / 1024 + "").substring(0, (size / 1024 + "").lastIndexOf(".") + 3) + + "MB" + : size + "KB"; +} + +/** + * @description 根据身份证号获取出生日期和性别 + * @param {String} idCard 身份证号 + * @return {Object} 出生日期和性别 date sex + **/ +export function idCardGetDateAndGender(idCard) { + const reg = + /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/; + let sex = ""; + let date = ""; + if (reg.test(idCard)) { + const org_birthday = idCard.substring(6, 14); + const org_gender = idCard.substring(16, 17); + const birthday = + org_birthday.substring(0, 4) + + "-" + + org_birthday.substring(4, 6) + + "-" + + org_birthday.substring(6, 8); + const birthdays = new Date(birthday.replace(/-/g, "/")); + const Month = birthdays.getMonth() + 1; + let MonthDate; + const DayDate = birthdays.getDate(); + let Day; + if (Month < 10) MonthDate = "0" + Month; + else MonthDate = Month; + if (DayDate < 10) Day = "0" + DayDate; + else Day = DayDate; + sex = org_gender % 2 === 1 ? "1" : "0"; + date = birthdays.getFullYear() + "-" + MonthDate + "-" + Day; + } + return { sex, date }; +} + +/** + * @description 获取select中指定项组成的数组 + * @param {Array} list 获取的数组 + * @param {Array} value 获取的值 + * @param {string?} idKey 获取的id + * @return {Array} list中指定项组成的数组 + **/ +export function getSelectAppointItemList(list, value, idKey = "id") { + return list.filter((item) => value.includes(item[idKey])); +} + +/** + * @description json转换为树形结构 + * @param {Array} json 需要转换的json + * @param {string} idStr id字段 + * @param {string} pidStr 父级id字段 + * @param {string} childrenStr 子级字段 + * @return {Array} 转换完的树形结构 + **/ +export function listTransTree(json, idStr, pidStr, childrenStr) { + const r = []; + const hash = {}; + const id = idStr; + const pid = pidStr; + const children = childrenStr; + let i = 0; + let j = 0; + const len = json.length; + for (; i < len; i++) { + hash[json[i][id]] = json[i]; + } + for (; j < len; j++) { + const aVal = json[j]; + const hashVP = hash[aVal[pid]]; + if (hashVP) { + !hashVP[children] && (hashVP[children] = []); + hashVP[children].push(aVal); + } else { + r.push(aVal); + } + } + return r; +} + +/** + * @description 将值转换为"是"/"否"显示文本 + * @param {any} value 需要转换的值 + * @param {Object} options 配置选项 + * @param {string} options.yesText 真值时显示的文本,默认为"是" + * @param {string} options.noText 假值时显示的文本,默认为"否" + * @param {string|number} options.yesValue 判断为真的值,默认为"1" + * @return {string} 转换后的显示文本 + **/ +export function isEmptyToWhether(value, options = {}) { + const { yesText = "是", noText = "否", yesValue = "1" } = options; + return !isEmpty(value) + ? value.toString() === yesValue.toString() + ? yesText + : noText + : ""; +} + +/** + * @description 计算表格中需要合并的行信息 + * @param {Array} data 表格数据数组 + * @param {string} field 用于比较的字段名,相同值的行需要合并 + * @param {Number} rowIndex 当前行索引 + * @returns {Object} 包含rowspan和colspan属性的对象,用于表格单元格合并 + * - rowspan {number} 合并行数 + * - colspan {number} 合并列数 + */ + +export function getRowSpans(data, field, rowIndex) { + if (!Array.isArray(data) || data.length === 0 || rowIndex < 0) { + return { rowspan: 1, colspan: 1 }; + } + if (data.length === 1) { + return { rowspan: 1, colspan: 1 }; + } + let currentSpanCount = 1; + let currentSpanIndex = 0; + for (let i = 1; i < data.length; i++) { + const currentValue = data[i][field]; + const previousValue = data[i - 1][field]; + if (currentValue === previousValue) { + currentSpanCount++; + if (i === rowIndex) { + return { rowspan: 0, colspan: 0 }; + } + } else { + if (currentSpanIndex === rowIndex) { + return { rowspan: currentSpanCount, colspan: 1 }; + } + currentSpanIndex = i; + currentSpanCount = 1; + } + } + if (currentSpanIndex === rowIndex) { + return { rowspan: currentSpanCount, colspan: 1 }; + } + return { rowspan: 1, colspan: 1 }; +} + +/** + * @description 生成指定长度的guid + * @param {number} len 生成的guid长度 + * @return {string} 生成的guid + **/ +export function createGuid(len = 32) { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < len; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +/** + * @description 获取文件路径前缀地址 + * @return {string} 文件路径前缀地址 + **/ +export function getFileUrl() { + return import.meta.env.VITE_FILE_URL; +} + +export function getBaseUrl() { + return import.meta.env[import.meta.env.DEV ? "VITE_PROXY" : "VITE_BASE_URL"]; +} + +export function getWebUrl() { + return window.location.origin + window.location.pathname + "#"; +}