diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4c7f8a8e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..e4a07bd0 --- /dev/null +++ b/.env.development @@ -0,0 +1,17 @@ +# 历史路径-哈希带井号标识 +VITE_HISTORY_HASH = false + +# 历史路径-前缀URL如:/h5 +VITE_HISTORY_BASE_URL = / + +# 应用名称 +VITE_APP_NAME = Mask管理系统 + +# 应用标识 +VITE_APP_CODE = maskAntd + +# 应用版本 +VITE_APP_VERSION = '0.2.1' + +# 接口基础URL地址-不带/后缀 +VITE_API_BASE_URL = /dev-api diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..cb4095cd --- /dev/null +++ b/.env.production @@ -0,0 +1,19 @@ +# 历史路径-哈希带井号标识 +VITE_HISTORY_HASH = true + +# 历史路径-前缀URL如:/h5 +VITE_HISTORY_BASE_URL = /mask-antd + +# 应用名称 +VITE_APP_NAME = Mask管理系统 + +# 应用标识 +VITE_APP_CODE = maskAntd + +# 应用版本 +VITE_APP_VERSION = '0.2.1' + +# 接口基础URL地址-不带/后缀 +# VITE_API_BASE_URL = https://mock.apifox.cn/m1/1551143-0-default +VITE_API_BASE_URL = http://124.223.91.248:8102/prod-api +# VITE_API_BASE_URL = http://192.168.56.1:6275 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a2583a80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +package-lock.json +yarn.lock diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..41e79eac --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "semi": true, + "trailingComma": "es5", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "avoid" +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..59631a78 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# 基于 Ant-Design-Vue + Vue3 的管理系统 + +[![star](https://gitee.com/TsMask/mask_antd_vue3/badge/star.svg?theme=dark)](https://gitee.com/TsMask/mask_antd_vue3/stargazers) +![Build Vite](https://img.shields.io/badge/Build-Vite-green.svg) +![Build Vue3](https://img.shields.io/badge/Build-Vue3-green.svg) +![Build MaskApi](https://img.shields.io/badge/Build-MaskApi-orange.svg) +![Release V0.2.1](https://img.shields.io/badge/Release-V0.2.1-orange.svg) + +## 简介 + +该项目选择 [RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3) 进行功能适配。 + +- 系统布局使用 [@ant-design-vue/pro-layout](https://github.com/vueComponent/pro-components) +- 图标来源 [@ant-design/icons-vue](https://ant.design/components/icon) +- 菜单图标使用自定义iconfont `font_8d5l8fzk5b87iudi.js`图标文件 + +> 有任何问题或者建议,可以在 [_Issues_](https://gitee.com/TsMask/mask_api_midwayjs/issues) 或通过QQ群:[_57242844_](https://jq.qq.com/?_wv=1027&k=z6Y4YQcB) 提出想法。 +> 如果觉得项目对您有帮助,可以来个Star ⭐ diff --git a/index.html b/index.html new file mode 100644 index 00000000..1369a698 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +
+ + + diff --git a/init.md b/init.md deleted file mode 100644 index b5754e20..00000000 --- a/init.md +++ /dev/null @@ -1 +0,0 @@ -ok \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..28145b4b --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "ems_frontend_vue3", + "type": "module", + "description": "核心网管理系统", + "author": "TsMask", + "engines": { + "node": ">=18.0.0" + }, + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design-vue/pro-layout": "^3.2.4", + "@ant-design/icons-vue": "^6.1.0", + "ant-design-vue": "^3.2.20", + "dayjs": "^1.11.8", + "echarts": "^5.4.2", + "file-saver": "^2.0.5", + "js-base64": "^3.7.5", + "js-cookie": "^3.0.5", + "nprogress": "^0.2.0", + "pinia": "^2.1.4", + "vue": "^3.3.4", + "vue-router": "^4.2.2" + }, + "devDependencies": { + "@types/file-saver": "^2.0.5", + "@types/js-cookie": "^3.0.3", + "@types/node": "^18.0.0", + "@types/nprogress": "^0.2.0", + "@vitejs/plugin-vue": "^4.2.3", + "less": "^4.1.3", + "typescript": "^5.1.3", + "unplugin-vue-components": "^0.25.1", + "vite": "^4.3.9", + "vite-plugin-compression": "^0.5.1", + "vue-i18n": "^9.3.0-beta.27", + "vue-tsc": "^1.8.0" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..4814a3f3 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/font_8d5l8fzk5b87iudi.js b/public/font_8d5l8fzk5b87iudi.js new file mode 100644 index 00000000..699f245c --- /dev/null +++ b/public/font_8d5l8fzk5b87iudi.js @@ -0,0 +1 @@ +(function(window){var svgSprite=""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+"";var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window) \ No newline at end of file diff --git a/public/loading.js b/public/loading.js new file mode 100644 index 00000000..36664037 --- /dev/null +++ b/public/loading.js @@ -0,0 +1,205 @@ +/** + * loading 占位 + * 解决首次加载时白屏的问题 + */ +(function () { + const _app = document.querySelector('#app'); + if (_app && _app.innerHTML === '') { + const styleStr = ` + `; + + let loadInfo = { + title: '正在加载资源', + titleSub: '初次加载资源可能需要较多时间', + msg: '请耐心等待', + }; + document.title = "管理系统"; + + // 判断选择语言 + const lang = localStorage.getItem('cache:local:i18n') || 'zh_CN'; + if (lang === 'en_US') { + loadInfo = { + title: 'Loading Resources', + titleSub: 'Loading resources for the first time may take a lot of time', + msg: 'Please be patient', + }; + document.title = "Managerial System"; + } + + const divStr = ` +
+
+
+ + + + + + +
+
+
+ ${loadInfo.title} +
+
+ ${loadInfo.titleSub}
${loadInfo.msg} +
+
`; + + _app.innerHTML = styleStr + divStr; + } +})(); diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 00000000..98d5fd18 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 00000000..225c9327 --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,59 @@ +import { request } from '@/plugins/http-fetch'; + +// 登录方法 +export function login(data: Record) { + return request({ + url: '/login', + method: 'post', + data: data, + whithToken: false, + }); +} + +/** + * 注册方法 + * @param data 注册对象 + * @returns object + */ +export function register(data: Record) { + return request({ + url: '/register', + method: 'post', + data: data, + whithToken: false, + }); +} + +/** + * 获取用户详细信息 + * @returns object + */ +export function getInfo() { + return request({ + url: '/getInfo', + method: 'get', + }); +} + +/** + * 退出方法 + * @returns object + */ +export function logout() { + return request({ + url: '/logout', + method: 'post', + }); +} + +/** + * 获取验证码 + * @returns object + */ +export function getCaptchaImage() { + return request({ + url: '/captchaImage', + method: 'get', + whithToken: false, + }); +} diff --git a/src/api/monitor/cache.ts b/src/api/monitor/cache.ts new file mode 100644 index 00000000..b5fc7479 --- /dev/null +++ b/src/api/monitor/cache.ts @@ -0,0 +1,86 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 查询缓存详细 + * @returns object + */ +export function getCache() { + return request({ + url: '/monitor/cache', + method: 'get', + }); +} + +/** + * 查询缓存名称列表 + * @returns object + */ +export function listCacheName() { + return request({ + url: '/monitor/cache/getNames', + method: 'get', + }); +} + +/** + * 查询缓存名称下键名列表 + * @param cacheName 缓存名称列表中得到的缓存名称 + * @returns object + */ +export function listCacheKey(cacheName: string) { + return request({ + url: `/monitor/cache/getKeys/${cacheName}`, + method: 'get', + }); +} + +/** + * 查询缓存内容 + * @param cacheName 键名列表中得到的缓存名称 + * @param cacheKey 键名列表中得到的缓存键名 + * @returns object + */ +export function getCacheValue(cacheName: string, cacheKey: string) { + return request({ + url: `/monitor/cache/getValue/${cacheName}/${cacheKey}`, + method: 'get', + }); +} + +/** + * 删除缓存名称下键名列表 + * @param cacheName 缓存名称列表中得到的缓存名称 + * @returns object + */ +export function clearCacheName(cacheName: string) { + return request({ + url: `/monitor/cache/clearCacheName/${cacheName}`, + method: 'delete', + }); +} + +/** + * 删除缓存键名 + * @param cacheName 键名列表中得到的缓存名称 + * @param cacheKey 键名列表中得到的缓存键名 + * @returns object + */ +export function clearCacheKey(cacheName: string, cacheKey: string) { + return request({ + url: `/monitor/cache/clearCacheKey/${cacheName}/${cacheKey}`, + method: 'delete', + }); +} + +/** + * 安全清理缓存名称 + * + * 指定可清理的缓存key + * @returns object + */ +export function clearCacheSafe() { + return request({ + url: '/monitor/cache/clearCacheSafe', + method: 'delete', + }); +} diff --git a/src/api/monitor/job.ts b/src/api/monitor/job.ts new file mode 100644 index 00000000..09ec7d5b --- /dev/null +++ b/src/api/monitor/job.ts @@ -0,0 +1,121 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 定时任务调度列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportJob(query: Record) { + return request({ + url: '/monitor/job/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询定时任务调度列表 + * @param query 查询参数 + * @returns object + */ +export function listJob(query: Record) { + return request({ + url: '/monitor/job/list', + method: 'get', + params: query, + }); +} + +/** + * 查询定时任务调度详细 + * @param jobId 任务ID + * @returns object + */ +export function getJob(jobId: string | number) { + return request({ + url: `/monitor/job/${jobId}`, + method: 'get', + }); +} + +/** + * 新增定时任务调度 + * @param data 任务对象 + * @returns object + */ +export function addJob(data: Record) { + return request({ + url: '/monitor/job', + method: 'post', + data: data, + }); +} + +/** + * 修改定时任务调度 + * @param data 任务对象 + * @returns object + */ +export function updateJob(data: Record) { + return request({ + url: '/monitor/job', + method: 'put', + data: data, + }); +} + +/** + * 删除定时任务调度 + * @param jobId 任务ID + * @returns object + */ +export function delJob(jobId: string | number) { + return request({ + url: `/monitor/job/${jobId}`, + method: 'delete', + }); +} + +/** + * 任务状态修改 + * @param jobId 任务ID + * @param status 变更状态值 + * @returns + */ +export function changeJobStatus( + jobId: string | number, + status: string | number +) { + return request({ + url: '/monitor/job/changeStatus', + method: 'put', + data: { + jobId, + status, + }, + }); +} + +/** + * 定时任务立即执行一次 + * @param jobId 任务ID + * @returns object + */ +export function runJob(jobId: string) { + return request({ + url: `/monitor/job/run/${jobId}`, + method: 'put', + }); +} + +/** + * 重置刷新队列 + * @returns object + */ +export function resetQueueJob() { + return request({ + url: '/monitor/job/resetQueueJob', + method: 'put', + }); +} diff --git a/src/api/monitor/jobLog.ts b/src/api/monitor/jobLog.ts new file mode 100644 index 00000000..ade7b771 --- /dev/null +++ b/src/api/monitor/jobLog.ts @@ -0,0 +1,53 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 定时任务调度日志列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportJobLog( + query: Record +) { + return request({ + url: '/monitor/jobLog/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询调度日志列表 + * @param query 查询参数 + * @returns object + */ +export function listJobLog(query: Record) { + return request({ + url: '/monitor/jobLog/list', + method: 'get', + params: query, + }); +} + +/** + * 删除调度日志 + * @param jobLogId 任务日志Id + * @returns object + */ +export function delJobLog(jobLogId: string) { + return request({ + url: `/monitor/jobLog/${jobLogId}`, + method: 'delete', + }); +} + +/** + * 清空调度日志 + * @returns object + */ +export function cleanJobLog() { + return request({ + url: '/monitor/jobLog/clean', + method: 'delete', + }); +} diff --git a/src/api/monitor/logininfor.ts b/src/api/monitor/logininfor.ts new file mode 100644 index 00000000..8e0388e5 --- /dev/null +++ b/src/api/monitor/logininfor.ts @@ -0,0 +1,67 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 登录日志列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportLogininfor( + query: Record +) { + return request({ + url: '/monitor/logininfor/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询登录日志列表 + * @param query 查询参数 + * @returns object + */ +export function listLogininfor( + query: Record +) { + return request({ + url: '/monitor/logininfor/list', + method: 'get', + params: query, + }); +} + +/** + * 删除登录日志 + * @param infoId 登录日志Id + * @returns object + */ +export function delLogininfor(infoId: string) { + return request({ + url: `/monitor/logininfor/${infoId}`, + method: 'delete', + }); +} + +/** + * 清空登录日志 + * @returns object + */ +export function cleanLogininfor() { + return request({ + url: '/monitor/logininfor/clean', + method: 'delete', + }); +} + +/** + * 解锁用户登录状态 + * @param userName 登录账号 + * @returns object + */ +export function unlockLogininfor(userName: string) { + return request({ + url: `/monitor/logininfor/unlock/${userName}`, + method: 'put', + }); +} diff --git a/src/api/monitor/online.ts b/src/api/monitor/online.ts new file mode 100644 index 00000000..4faa13ea --- /dev/null +++ b/src/api/monitor/online.ts @@ -0,0 +1,26 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 查询在线用户列表 + * @param query 查询参数 + * @returns object + */ +export function listOnline(query: Record) { + return request({ + url: '/monitor/online/list', + method: 'get', + params: query, + }); +} + +/** + * 强退用户 + * @param tokenId 授权标识 + * @returns object + */ +export function forceLogout(tokenId: string) { + return request({ + url: `/monitor/online/${tokenId}`, + method: 'delete', + }); +} diff --git a/src/api/monitor/operlog.ts b/src/api/monitor/operlog.ts new file mode 100644 index 00000000..04bfb101 --- /dev/null +++ b/src/api/monitor/operlog.ts @@ -0,0 +1,55 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 操作日志列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportOperlog( + query: Record +) { + return request({ + url: '/monitor/operlog/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询操作日志列表 + * @param query 查询参数 + * @returns object + */ +export function listOperlog( + query: Record +) { + return request({ + url: '/monitor/operlog/list', + method: 'get', + params: query, + }); +} + +/** + * 删除操作日志 + * @param operId 操作日志ID + * @returns object + */ +export function delOperlog(operId: string) { + return request({ + url: `/monitor/operlog/${operId}`, + method: 'delete', + }); +} + +/** + * 清空操作日志 + * @returns object + */ +export function cleanOperlog() { + return request({ + url: '/monitor/operlog/clean', + method: 'delete', + }); +} diff --git a/src/api/monitor/server.ts b/src/api/monitor/server.ts new file mode 100644 index 00000000..eea90e35 --- /dev/null +++ b/src/api/monitor/server.ts @@ -0,0 +1,9 @@ +import { request } from '@/plugins/http-fetch'; + +/**获取服务信息 */ +export function getServer() { + return request({ + url: '/monitor/server', + method: 'get', + }); +} diff --git a/src/api/profile.ts b/src/api/profile.ts new file mode 100644 index 00000000..be793283 --- /dev/null +++ b/src/api/profile.ts @@ -0,0 +1,56 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 查询用户个人信息 + * @returns object + */ +export function getUserProfile() { + return request({ + url: '/system/user/profile', + method: 'get', + }); +} + +/** + * 修改用户个人信息 + * @param data 用户对象 + * @returns object + */ +export function updateUserProfile(data: Record) { + return request({ + url: '/system/user/profile', + method: 'put', + data: data, + }); +} + +/** + * 用户密码重置 + * @param userId 用户ID + * @param status 变更状态值 + * @returns object + */ +export function updateUserPwd(oldPassword: string, newPassword: string) { + return request({ + url: '/system/user/profile/updatePwd', + method: 'put', + data: { + oldPassword, + newPassword, + }, + }); +} + +/** + * 用户头像上传 + * @param data 表单数据对象 + * @returns object + */ +export function uploadAvatar(data: FormData) { + return request({ + url: '/system/user/profile/avatar', + method: 'post', + data, + dataType: 'form-data', + }); +} diff --git a/src/api/router.ts b/src/api/router.ts new file mode 100644 index 00000000..a0b810cf --- /dev/null +++ b/src/api/router.ts @@ -0,0 +1,12 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 获取路由 + * @returns object + */ +export const getRouters = () => { + return request({ + url: '/getRouters', + method: 'get', + }); +}; diff --git a/src/api/system/config.ts b/src/api/system/config.ts new file mode 100644 index 00000000..2358eb7c --- /dev/null +++ b/src/api/system/config.ts @@ -0,0 +1,103 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 参数配置列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportConfig( + query: Record +) { + return request({ + url: '/system/config/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询参数配置列表 + * @param query 查询参数 + * @returns object + */ +export function listConfig(query: Record) { + return request({ + url: '/system/config/list', + method: 'get', + params: query, + }); +} + +/** + * 查询参数详细 + * @param configId 参数配置ID + * @returns object + */ +export function getConfig(configId: string | number) { + return request({ + url: `/system/config/${configId}`, + method: 'get', + }); +} + +/** + * 根据参数键名查询参数值 + * @param configKey 参数键名 + * @returns object + */ +export function getConfigKey(configKey: string) { + return request({ + url: `/system/config/configKey/${configKey}`, + method: 'get', + }); +} + +/** + * 新增参数配置 + * @param data 参数配置对象 + * @returns object + */ +export function addConfig(data: Record) { + return request({ + url: '/system/config', + method: 'post', + data: data, + }); +} + +/** + * 修改参数配置 + * @param data 参数配置对象 + * @returns object + */ +export function updateConfig(data: Record) { + return request({ + url: '/system/config', + method: 'put', + data: data, + }); +} + +/** + * 删除参数配置 + * @param configId 参数配置ID + * @returns object + */ +export function delConfig(configId: string | number) { + return request({ + url: `/system/config/${configId}`, + method: 'delete', + }); +} + +/** + * 刷新参数缓存 + * @returns object + */ +export function refreshCache() { + return request({ + url: '/system/config/refreshCache', + method: 'put', + }); +} diff --git a/src/api/system/dept.ts b/src/api/system/dept.ts new file mode 100644 index 00000000..288a16ef --- /dev/null +++ b/src/api/system/dept.ts @@ -0,0 +1,99 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 查询部门列表 + * @param query 查询参数 + * @returns object + */ +export function listDept(query: Record) { + return request({ + url: '/system/dept/list', + method: 'get', + params: query, + }); +} + +/** + * 查询部门列表(排除节点) + * @param deptId 部门ID + * @returns object + */ +export function listDeptExcludeChild(deptId: string | number) { + return request({ + url: `/system/dept/list/exclude/${deptId}`, + method: 'get', + }); +} + +/** + * 查询部门详细 + * @param deptId 部门ID + * @returns object + */ +export function getDept(deptId: string | number) { + return request({ + url: `/system/dept/${deptId}`, + method: 'get', + }); +} + +/** + * 新增部门 + * @param data 部门对象 + * @returns object + */ +export function addDept(data: Record) { + return request({ + url: '/system/dept', + method: 'post', + data: data, + }); +} + +/** + * 修改部门 + * @param data 部门对象 + * @returns object + */ +export function updateDept(data: Record) { + return request({ + url: '/system/dept', + method: 'put', + data: data, + }); +} + +/** + * 删除部门 + * @param deptId 部门ID + * @returns object + */ +export function delDept(deptId: string | number) { + return request({ + url: `/system/dept/${deptId}`, + method: 'delete', + }); +} + +/** + * 查询部门下拉树结构 + * @returns object + */ +export function deptTreeSelect() { + return request({ + url: '/system/dept/treeSelect', + method: 'get', + }); +} + +/** + * 部门树结构列表(指定角色) + * @param roleId 角色ID + * @returns object + */ +export function roleDeptTreeSelect(roleId: string | number) { + return request({ + url: `/system/dept/roleDeptTreeSelect/${roleId}`, + method: 'get', + }); +} diff --git a/src/api/system/dict/data.ts b/src/api/system/dict/data.ts new file mode 100644 index 00000000..f4fc8df4 --- /dev/null +++ b/src/api/system/dict/data.ts @@ -0,0 +1,90 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 字典数据列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportData(query: Record) { + return request({ + url: '/system/dict/data/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询字典数据列表 + * @param query 查询值 + * @returns + */ +export function listData(query: Record) { + return request({ + url: '/system/dict/data/list', + method: 'get', + params: query, + }); +} + +/** + * 查询字典数据详细 + * @param dictCode 字典代码值 + * @returns object + */ +export function getData(dictCode: string | number) { + return request({ + url: `/system/dict/data/${dictCode}`, + method: 'get', + }); +} + +/** + * 新增字典数据 + * @param data 字典数据对象 + * @returns object + */ +export function addData(data: Record) { + return request({ + url: '/system/dict/data', + method: 'post', + data: data, + }); +} + +/** + * 修改字典数据 + * @param data 字典数据对象 + * @returns object + */ +export function updateData(data: Record) { + return request({ + url: '/system/dict/data', + method: 'put', + data: data, + }); +} + +/** + * 删除字典数据 + * @param dictCode 字典代码值 + * @returns object + */ +export function delData(dictCode: string | number) { + return request({ + url: `/system/dict/data/${dictCode}`, + method: 'delete', + }); +} + +/** + * 字典数据列表(指定字典类型) + * @param dictType 字典类型 + * @returns object + */ +export function getDictDataType(dictType: string) { + return request({ + url: `/system/dict/data/type/${dictType}`, + method: 'get', + }); +} diff --git a/src/api/system/dict/type.ts b/src/api/system/dict/type.ts new file mode 100644 index 00000000..040b0329 --- /dev/null +++ b/src/api/system/dict/type.ts @@ -0,0 +1,102 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 字典类型列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportType(query: Record) { + return request({ + url: '/system/dict/type/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询字典类型列表 + * @param query 查询值 + * @returns + */ +export function listType(query: Record) { + return request({ + url: '/system/dict/type/list', + method: 'get', + params: query, + }); +} + +/** + * 查询字典类型详细 + * @param dictId 字典编号 + * @returns object + */ +export function getType(dictId: string | number) { + return request({ + url: `/system/dict/type/${dictId}`, + method: 'get', + }); +} + +/** + * 新增字典类型 + * @param data 字典数据对象 + * @returns object + */ +export function addType(data: Record) { + return request({ + url: '/system/dict/type', + method: 'post', + data: data, + }); +} + +/** + * 修改字典类型 + * @param data 字典数据对象 + * @returns object + */ +export function updateType(data: Record) { + return request({ + url: '/system/dict/type', + method: 'put', + data: data, + }); +} + +/** + * 删除字典类型 + * @param dictCode 字典代码值 + * @returns object + */ +export function delType(dictId: string | number) { + return request({ + url: `/system/dict/type/${dictId}`, + method: 'delete', + }); +} + +/** + * 刷新字典缓存 + * @param data 字典数据对象 + * @returns object + */ +export function refreshCache() { + return request({ + url: '/system/dict/type/refreshCache', + method: 'put', + }); +} + +/** + * 获取字典选择框列表 + * @param data 字典数据对象 + * @returns object + */ +export function getDictOptionselect() { + return request({ + url: '/system/dict/type/getDictOptionselect', + method: 'get', + }); +} diff --git a/src/api/system/menu.ts b/src/api/system/menu.ts new file mode 100644 index 00000000..4827d770 --- /dev/null +++ b/src/api/system/menu.ts @@ -0,0 +1,87 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 查询菜单列表 + * @param query 查询参数 + * @returns object + */ +export function listMenu(query?: Record) { + return request({ + url: '/system/menu/list', + method: 'get', + params: query, + }); +} + +/** + * 查询菜单详细 + * @param menuId 菜单ID + * @returns object + */ +export function getMenu(menuId: string | number) { + return request({ + url: `/system/menu/${menuId}`, + method: 'get', + }); +} + +/** + * 查询菜单下拉树结构 + * @returns object + */ +export function menuTreeSelect() { + return request({ + url: '/system/menu/treeSelect', + method: 'get', + }); +} + +/** + * 根据角色ID查询菜单下拉树结构 + * @param roleId 角色ID + * @returns object + */ +export function roleMenuTreeSelect(roleId: string | number) { + return request({ + url: `/system/menu/roleMenuTreeSelect/${roleId}`, + method: 'get', + }); +} + +/** + * 新增菜单 + * @param data 菜单对象 + * @returns object + */ +export function addMenu(data: Record) { + return request({ + url: '/system/menu', + method: 'post', + data: data, + }); +} + +/** + * 修改菜单 + * @param data 菜单对象 + * @returns object + */ +export function updateMenu(data: Record) { + return request({ + url: '/system/menu', + method: 'put', + data: data, + }); +} + +/** + * 删除菜单 + * @param menuId 菜单ID + * @returns object + */ +export function delMenu(menuId: string | number) { + return request({ + url: `/system/menu/${menuId}`, + method: 'delete', + }); +} diff --git a/src/api/system/notice.ts b/src/api/system/notice.ts new file mode 100644 index 00000000..d3046fce --- /dev/null +++ b/src/api/system/notice.ts @@ -0,0 +1,64 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 查询公告列表 + * @param query 查询参数 + * @returns object + */ +export function listNotice(query: Record) { + return request({ + url: '/system/notice/list', + method: 'get', + params: query, + }); +} + +/** + * 查询公告详细 + * @param menuId 公告ID + * @returns object + */ +export function getNotice(noticeId: string | number) { + return request({ + url: `/system/notice/${noticeId}`, + method: 'get', + }); +} + +/** + * 新增公告 + * @param data 公告对象 + * @returns object + */ +export function addNotice(data: Record) { + return request({ + url: '/system/notice', + method: 'post', + data: data, + }); +} + +/** + * 修改公告 + * @param data 公告对象 + * @returns object + */ +export function updateNotice(data: Record) { + return request({ + url: '/system/notice', + method: 'put', + data: data, + }); +} + +/** + * 删除公告 + * @param noticeId 公告ID + * @returns object + */ +export function delNotice(noticeId: string | number) { + return request({ + url: `/system/notice/${noticeId}`, + method: 'delete', + }); +} diff --git a/src/api/system/post.ts b/src/api/system/post.ts new file mode 100644 index 00000000..da63bc92 --- /dev/null +++ b/src/api/system/post.ts @@ -0,0 +1,78 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 岗位列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportPost(query: Record) { + return request({ + url: '/system/post/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询岗位列表 + * @param query 查询参数 + * @returns object + */ +export function listPost(query: Record) { + return request({ + url: '/system/post/list', + method: 'get', + params: query, + }); +} + +/** + * 查询岗位详细 + * @param postId 岗位ID + * @returns object + */ +export function getPost(postId: string | number) { + return request({ + url: `/system/post/${postId}`, + method: 'get', + }); +} + +/** + * 新增岗位 + * @param data 岗位对象 + * @returns object + */ +export function addPost(data: Record) { + return request({ + url: '/system/post', + method: 'post', + data: data, + }); +} + +/** + * 修改岗位 + * @param data 岗位对象 + * @returns object + */ +export function updatePost(data: Record) { + return request({ + url: '/system/post', + method: 'put', + data: data, + }); +} + +/** + * 删除岗位 + * @param postId 岗位ID + * @returns object + */ +export function delPost(postId: string | number) { + return request({ + url: `/system/post/${postId}`, + method: 'delete', + }); +} diff --git a/src/api/system/role.ts b/src/api/system/role.ts new file mode 100644 index 00000000..2f426836 --- /dev/null +++ b/src/api/system/role.ts @@ -0,0 +1,134 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 角色列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportRole(query: Record) { + return request({ + url: '/system/role/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询角色列表 + * @param query 查询参数 + * @returns object + */ +export function listRole(query: Record) { + return request({ + url: '/system/role/list', + method: 'get', + params: query, + }); +} + +/** + * 查询角色详细 + * @param roleId 角色ID + * @returns object + */ +export function getRole(roleId: string | number) { + return request({ + url: `/system/role/${roleId}`, + method: 'get', + }); +} + +/** + * 新增角色 + * @param data 角色对象 + * @returns object + */ +export function addRole(data: Record) { + return request({ + url: '/system/role', + method: 'post', + data: data, + }); +} + +/** + * 修改角色 + * @param data 角色对象 + * @returns object + */ +export function updateRole(data: Record) { + return request({ + url: '/system/role', + method: 'put', + data: data, + }); +} + +/** + * 删除角色 + * @param roleId 角色ID + * @returns object + */ +export function delRole(roleId: string | number) { + return request({ + url: `/system/role/${roleId}`, + method: 'delete', + }); +} + +/** + * 角色状态修改 + * @param roleId 角色ID + * @param status 角色状态 + * @returns object + */ +export function changeRoleStatus(roleId: string, status: string | number) { + return request({ + url: '/system/role/changeStatus', + method: 'put', + data: { + roleId, + status, + }, + }); +} + +/** + * 修改角色数据权限 + * @param data 角色对象 + * @returns object + */ +export function dataScope(data: Record) { + return request({ + url: '/system/role/dataScope', + method: 'put', + data: data, + }); +} + +/** + * 角色分配用户列表 + * @param query 查询参数 + * @returns object + */ +export function authUserAllocatedList(query: Record) { + return request({ + url: '/system/role/authUser/allocatedList', + method: 'get', + params: query, + }); +} + +/** + * 角色分配选择授权 + * @param data 角色对象 + * @returns object + */ +export function authUserChecked(data: Record) { + return request({ + url: '/system/role/authUser/checked', + method: 'put', + data: data, + }); +} diff --git a/src/api/system/user.ts b/src/api/system/user.ts new file mode 100644 index 00000000..febcce9e --- /dev/null +++ b/src/api/system/user.ts @@ -0,0 +1,141 @@ +import { request } from '@/plugins/http-fetch'; + +/** + * 导入用户模板数据 + * @param data 表单数据对象 + * @returns object + */ +export function importData(data: FormData) { + return request({ + url: '/system/user/importData', + method: 'post', + data, + dataType: 'form-data', + }); +} + +/** + * 导入用户模板下载 + * @returns bolb + */ +export function importTemplate() { + return request({ + url: '/system/user/importTemplate', + method: 'get', + responseType: 'blob', + }); +} + +/** + * 用户列表导出 + * @param query 查询参数 + * @returns bolb + */ +export function exportUser(query: Record) { + return request({ + url: '/system/user/export', + method: 'post', + data: query, + responseType: 'blob', + }); +} + +/** + * 查询用户列表 + * @param query 查询参数 + * @returns object + */ +export function listUser(query: Record) { + return request({ + url: '/system/user/list', + method: 'get', + params: query, + }); +} + +/** + * 查询用户详细 + * @param userId 用户ID,新增0 + * @returns object + */ +export function getUser(userId: string | number = '0') { + return request({ + url: `/system/user/${userId}`, + method: 'get', + }); +} + +/** + * 新增用户 + * @param data 用户对象 + * @returns object + */ +export function addUser(data: Record) { + return request({ + url: '/system/user', + method: 'post', + data: data, + }); +} + +/** + * 修改用户 + * @param data 用户对象 + * @returns object + */ +export function updateUser(data: Record) { + return request({ + url: '/system/user', + method: 'put', + data: data, + }); +} + +/** + * 删除用户 + * @param userId 用户ID + * @returns object + */ +export function delUser(userId: string | number) { + return request({ + url: `/system/user/${userId}`, + method: 'delete', + }); +} + +/** + * 用户密码重置 + * @param userId 用户ID + * @param password 密码 + * @returns object + */ +export function resetUserPwd(userId: string | number, password: string) { + return request({ + url: '/system/user/resetPwd', + method: 'put', + data: { + userId, + password, + }, + }); +} + +/** + * 用户状态修改 + * @param userId 用户ID + * @param status 变更状态值 + * @returns object + */ +export function changeUserStatus( + userId: string | number, + status: string | number +) { + return request({ + url: '/system/user/changeStatus', + method: 'put', + data: { + userId, + status, + }, + }); +} diff --git a/src/api/tool/file.ts b/src/api/tool/file.ts new file mode 100644 index 00000000..6d454b17 --- /dev/null +++ b/src/api/tool/file.ts @@ -0,0 +1,195 @@ +import { request } from '@/plugins/http-fetch'; +import { encode } from 'js-base64'; + +/** + * 下载文件 + * @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png + * @param range 断点续传标识,填入字符串 `bytes=${startByte}-${endByte}` + * @returns object + */ +export async function downloadFile(filePath: string, range?: string) { + return request({ + url: `/file/download/${encode(filePath)}`, + method: 'get', + headers: range ? { range } : {}, + responseType: 'blob', + }); +} + +/** + * 下载文件切片 + * @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png + * @param chunkSize 数据块大小MB,默认1MB + * @returns bolb + */ +export async function downloadFileChunk( + filePath: string, + chunkSize: number = 1 +): Promise { + chunkSize = chunkSize * 1024 * 1024; + let start = 0; // 文件块的起始字节 + let end = chunkSize - 1; // 文件块的结束字节 + let totalSize = 0; // 文件总大小 + let downloadedSize = 0; // 已下载的文件大小 + let filePart: Blob[] = []; // 文件数据块内容 + + // 发送带有 Range 请求头的 HTTP 请求 + async function sendRequest() { + const range = `bytes=${start}-${end}`; + const res = await downloadFile(filePath, range); + if (res.code === 200 && res.status === 206) { + // 总大小 + const contentRange = res.headers.get('content-range') || '0/0'; + totalSize = parseInt(contentRange.split('/')[1]); + // 已下载大小 + const contentLength = res.headers.get('content-length') || '0'; + const chunkSize = parseInt(contentLength); + // 下一段数据块区间 + start += chunkSize; + end = Math.min(start + chunkSize - 1, totalSize - 1); + // 记录下载结果 + filePart.push(res.data); + downloadedSize += chunkSize; + // 小于总大小继续下载后续数据 + if (downloadedSize < totalSize) { + await sendRequest(); + } + } else { + return res; + } + } + + await sendRequest(); + return new Blob(filePart, { type: 'application/octet-stream' }); +} + +/** + * 上传文件 + * @param data 表单数据对象 + * @returns object + */ +export function uploadFile(data: FormData) { + return request({ + url: '/file/upload', + method: 'post', + data, + dataType: 'form-data', + }); +} + +/** + * 上传切片文件 + * @param file 文件对象 + * @param chunkSize 数据块大小MB,默认1MB + * @param subPath 归属子路径, 默认default + * @returns + */ +export async function uploadFileChunk( + fileData: File, + chunkSize: number = 1, + subPath: string = 'default' +) { + const { name, size } = fileData; + const chunkSizeInBytes = chunkSize * 1024 * 1024; + // 文件标识使用唯一编码 MD5(文件名+文件大小) + const fileIdentifier = `${name}-${size}`; + // 文件切分为多少份进行上传 + const chunksCount = Math.ceil(size / chunkSizeInBytes); + // 切块的数据数据用于上传 + const fileChunks: { index: number; chunk: Blob }[] = []; + + for (let i = 0; i < chunksCount; i++) { + const start = i * chunkSizeInBytes; + const end = Math.min(start + chunkSizeInBytes, size); + fileChunks.push({ + index: i, + chunk: fileData.slice(start, end), + }); + } + + // 检查是否已上传部分数据块 + const resCheck = await chunkCheck(fileIdentifier, name); + if (resCheck.code !== 200) { + return resCheck; + } + + let uploadedSize = 0; + let uploadProgress = 0; + + for (const { index, chunk } of fileChunks) { + const chunksIndex = `${index}`; + // 跳过已上传块 + if (resCheck.data.includes(chunksIndex)) { + continue; + } + + // 上传数据块 + const formData = new FormData(); + formData.append('file', chunk, name); + formData.append('index', chunksIndex); + formData.append('identifier', fileIdentifier); + + const resUpload = await chunkUpload(formData); + if (resUpload.code === 200) { + uploadedSize += chunk.size; + uploadProgress = (uploadedSize / size) * 100; + console.log(`上传进度:${uploadProgress}%`); + } else { + // 上传失败处理 + break; + } + } + + // 上传数据完整后合并数据块 + if (uploadedSize === size) { + return await chunkMerge(fileIdentifier, name, subPath); + } + return { code: 500, msg: '上传出错,请重试' }; +} + +/** + * 切片文件检查 + * @param identifier 文件标识 + * @param fileName 原文件名称 + * @returns object + */ +export function chunkCheck(identifier: string, fileName: string) { + return request({ + url: '/file/chunkCheck', + method: 'post', + data: { identifier, fileName }, + }); +} + +/** + * 切片文件合并 + * @param identifier 文件标识 + * @param fileName 原文件名称 + * @param subPath 文件归属 + * @returns object + */ +export function chunkMerge( + identifier: string, + fileName: string, + subPath: string = 'default' +) { + return request({ + url: '/file/chunkMerge', + method: 'post', + data: { identifier, fileName, subPath }, + }); +} + +/** + * 切片文件上传 + * @param data 表单数据对象 + * @returns object + */ +export function chunkUpload(data: FormData) { + return request({ + url: '/file/chunkUpload', + method: 'post', + data, + dataType: 'form-data', + }); +} diff --git a/src/assets/background.svg b/src/assets/background.svg new file mode 100644 index 00000000..43adc8a6 --- /dev/null +++ b/src/assets/background.svg @@ -0,0 +1,69 @@ + + + + Ant-Design-Pro + mask-and-vue3 By TsMask + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/donate.jpg b/src/assets/donate.jpg new file mode 100644 index 00000000..6d62f6c4 Binary files /dev/null and b/src/assets/donate.jpg differ diff --git a/src/assets/images/default_avatar.png b/src/assets/images/default_avatar.png new file mode 100644 index 00000000..89ae8d75 Binary files /dev/null and b/src/assets/images/default_avatar.png differ diff --git a/src/assets/js/icon_font_8d5l8fzk5b87iudi.ts b/src/assets/js/icon_font_8d5l8fzk5b87iudi.ts new file mode 100644 index 00000000..794233df --- /dev/null +++ b/src/assets/js/icon_font_8d5l8fzk5b87iudi.ts @@ -0,0 +1,130 @@ +/** + * 字体图标文件-静态资源文件路径 + */ +const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL; +export const scriptUrl = `${ + baseUrl.length === 1 && baseUrl.indexOf('/') === 0 + ? '' + : baseUrl.indexOf('/') === -1 + ? '/' + baseUrl + : baseUrl +}/font_8d5l8fzk5b87iudi.js`; + +/** + * 读取 font_8d5l8fzk5b87iudi.js 文件内svg图标名称 + * + * JSON.stringify(txt.match(/icon-(\S*)"/gi).map(i=>i.slice(0,-1))) + */ +export const iconFonts = [ + '#', + 'icon-alibaba', + 'icon-alimama', + 'icon-aliyun', + 'icon-anzhuo', + 'icon-biaoqing', + 'icon-chexiao', + 'icon-chexiao2', + 'icon-daimayingyong', + 'icon-daishenhe', + 'icon-dashang', + 'icon-dianzan', + 'icon-dianzan1', + 'icon-facebook', + 'icon-fangda', + 'icon-fangda2', + 'icon-fanhui', + 'icon-fanhui1', + 'icon-fankui1', + 'icon-fenxiang', + 'icon-fuzhichenggong', + 'icon-fuzhidaima', + 'icon-fuzhidaima1', + 'icon-gengduo', + 'icon-gerenzhanghu', + 'icon-github', + 'icon-gonggao', + 'icon-gonggaodayi', + 'icon-gongnengjieshao', + 'icon-gouwuche', + 'icon-gouwuche2', + 'icon-guanbi', + 'icon-huidingbu', + 'icon-huifu', + 'icon-huizhiguize', + 'icon-iconfont1', + 'icon-ios', + 'icon-jieshi', + 'icon-jinggao', + 'icon-lishi', + 'icon-morentouxiang', + 'icon-paixu', + 'icon-pcduan', + 'icon-piliang', + 'icon-qingchu', + 'icon-qq', + 'icon-qunzhu', + 'icon-right', + 'icon-saoyisao', + 'icon-shanchu', + 'icon-shang', + 'icon-shang1', + 'icon-shang2', + 'icon-shangchuan', + 'icon-shenhejujue', + 'icon-shenhetongguo', + 'icon-shijian', + 'icon-shuoming', + 'icon-souren', + 'icon-sousuo', + 'icon-soutubiao', + 'icon-suofang', + 'icon-suoxiao', + 'icon-suoxiao2', + 'icon-taobaowang', + 'icon-tengxunwang', + 'icon-tianjiachengyuan', + 'icon-tianmao', + 'icon-tubiaohuizhi', + 'icon-tubiaoku', + 'icon-tuichu', + 'icon-twitter', + 'icon-weibo', + 'icon-weibo1', + 'icon-weibo2', + 'icon-weijiaru', + 'icon-weitijiao', + 'icon-weixin', + 'icon-wenjian', + 'icon-wocanyu', + 'icon-wofaqi', + 'icon-xia', + 'icon-xia2', + 'icon-xiangmu', + 'icon-xiangmuchengyuan', + 'icon-xiangxia', + 'icon-xiangxia1', + 'icon-xiangxia2', + 'icon-xiangyou', + 'icon-xiaomi', + 'icon-xiazai', + 'icon-xinjiantubiaoku', + 'icon-yingwen', + 'icon-you', + 'icon-you1', + 'icon-you2', + 'icon-youxiang', + 'icon-youxuan', + 'icon-youxuan2', + 'icon-yuzhanghao', + 'icon-yuzhanghao1', + 'icon-zhifubao', + 'icon-zhizuoliucheng', + 'icon-zhongguodianxin', + 'icon-zhuanrang', + 'icon-zhubajie', + 'icon-zuo', + 'icon-zuo1', + 'icon-zuo2', + 'icon-zuoxuan', + 'icon-zuoxuan2', +]; diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 00000000..89ae8d75 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/CronModal/components/Day.vue b/src/components/CronModal/components/Day.vue new file mode 100644 index 00000000..c176ad25 --- /dev/null +++ b/src/components/CronModal/components/Day.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/components/CronModal/components/Hour.vue b/src/components/CronModal/components/Hour.vue new file mode 100644 index 00000000..9555d3d2 --- /dev/null +++ b/src/components/CronModal/components/Hour.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/components/CronModal/components/Minute.vue b/src/components/CronModal/components/Minute.vue new file mode 100644 index 00000000..17a6e8c9 --- /dev/null +++ b/src/components/CronModal/components/Minute.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/components/CronModal/components/Month.vue b/src/components/CronModal/components/Month.vue new file mode 100644 index 00000000..9a782bf3 --- /dev/null +++ b/src/components/CronModal/components/Month.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/components/CronModal/components/Second.vue b/src/components/CronModal/components/Second.vue new file mode 100644 index 00000000..e4efdf3b --- /dev/null +++ b/src/components/CronModal/components/Second.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/src/components/CronModal/index.vue b/src/components/CronModal/index.vue new file mode 100644 index 00000000..8d6a723e --- /dev/null +++ b/src/components/CronModal/index.vue @@ -0,0 +1,112 @@ + + + + diff --git a/src/components/DictTag/index.vue b/src/components/DictTag/index.vue new file mode 100644 index 00000000..a8e8ae03 --- /dev/null +++ b/src/components/DictTag/index.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/IconFont/index.vue b/src/components/IconFont/index.vue new file mode 100644 index 00000000..912cd286 --- /dev/null +++ b/src/components/IconFont/index.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/components/LinkiFrame/index.vue b/src/components/LinkiFrame/index.vue new file mode 100644 index 00000000..483d1cce --- /dev/null +++ b/src/components/LinkiFrame/index.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/src/constants/admin-constants.ts b/src/constants/admin-constants.ts new file mode 100644 index 00000000..eeca5ec2 --- /dev/null +++ b/src/constants/admin-constants.ts @@ -0,0 +1,5 @@ +/**管理员-系统指定角色KEY */ +export const ADMIN_ROLE_KEY = 'admin'; + +/**管理员-系统指定权限 */ +export const ADMIN_PERMISSION = '*:*:*'; diff --git a/src/constants/app-constants.ts b/src/constants/app-constants.ts new file mode 100644 index 00000000..cea6637a --- /dev/null +++ b/src/constants/app-constants.ts @@ -0,0 +1,5 @@ +/**应用-请求头-系统标识 */ +export const APP_REQUEST_HEADER_CODE = 'X-App-Code'; + +/**应用-请求头-系统版本 */ +export const APP_REQUEST_HEADER_VERSION = 'X-App-Version'; diff --git a/src/constants/cache-keys-constants.ts b/src/constants/cache-keys-constants.ts new file mode 100644 index 00000000..22d7e9a5 --- /dev/null +++ b/src/constants/cache-keys-constants.ts @@ -0,0 +1,11 @@ +/**会话缓存-网络请求 */ +export const CACHE_SESSION_FATCH = 'cache:session:fatch'; + +/**本地缓存-布局设置 */ +export const CACHE_LOCAL_PROCONFIG = 'cache:local:proConfig'; + +/**本地缓存-主题色 */ +export const CACHE_LOCAL_PRIMARY_COLOR = 'cache:local:primaryColor'; + +/**本地缓存-多语言 */ +export const CACHE_LOCAL_I18N = 'cache:local:i18n'; diff --git a/src/constants/menu-constants.ts b/src/constants/menu-constants.ts new file mode 100644 index 00000000..a753ef18 --- /dev/null +++ b/src/constants/menu-constants.ts @@ -0,0 +1,20 @@ +/**组件布局类型-基础布局组件标识 */ +export const MENU_COMPONENT_LAYOUT_BASIC = 'BasicLayout'; + +/**组件布局类型-空白布局组件标识 */ +export const MENU_COMPONENT_LAYOUT_BLANK = 'BlankLayout'; + +/**组件布局类型-内链接布局组件标识 */ +export const MENU_COMPONENT_LAYOUT_LINK = 'LinkLayout'; + +/**菜单类型-目录 */ +export const MENU_TYPE_DIR = 'D'; + +/**菜单类型-菜单 */ +export const MENU_TYPE_MENU = 'M'; + +/**菜单类型-按钮 */ +export const MENU_TYPE_BUTTON = 'B'; + +/**菜单内嵌地址标识-带/前缀 */ +export const MENU_PATH_INLINE = '/inline'; diff --git a/src/constants/token-constants.ts b/src/constants/token-constants.ts new file mode 100644 index 00000000..043273ff --- /dev/null +++ b/src/constants/token-constants.ts @@ -0,0 +1,11 @@ +/**令牌-数据响应字段 */ +export const TOKEN_RESPONSE_FIELD = 'access_token'; + +/**令牌-请求头标识前缀 */ +export const TOKEN_KEY_PREFIX = 'Bearer '; + +/**令牌-请求头标识 */ +export const TOKEN_KEY = 'Authorization'; + +/**令牌-存放Cookie标识 */ +export const TOKEN_COOKIE = 'AuthAntdv'; diff --git a/src/directive/index.ts b/src/directive/index.ts new file mode 100644 index 00000000..37c3d042 --- /dev/null +++ b/src/directive/index.ts @@ -0,0 +1,10 @@ +import { App } from 'vue'; +import permsDirective from './perms-directive'; +import rolesDirective from './roles-directive'; + +export default { + install: (app: App) => { + app.directive('perms', permsDirective); + app.directive('roles', rolesDirective); + }, +}; diff --git a/src/directive/perms-directive.ts b/src/directive/perms-directive.ts new file mode 100644 index 00000000..04a00384 --- /dev/null +++ b/src/directive/perms-directive.ts @@ -0,0 +1,38 @@ +import { hasPermissions, matchPermissions } from '@/plugins/auth-user'; +import { DirectiveBinding } from 'vue'; + +/** + * perms-权限标识 + * + * 指令值:字符串数组 + * + * 指令的参数:has/math,默认has + * + * v-perms="['monitor:server:query']" + * 等同 + * v-perms:has="['monitor:server:query']" + * + * v-perms:math="['style:user:query', 'style:user:edit']" + * + * @param el 指令绑定到的元素 + * @param binding 一个对象 + */ +export default function (el: any, binding: DirectiveBinding) { + const value = binding.value; + let arg = binding.arg; + let ok: boolean = false; + if (Array.isArray(value) && value.length > 0) { + // 匹配 + if (arg === 'math') { + ok = matchPermissions(value); + } + // 含有 + if (!arg || arg === 'has') { + ok = hasPermissions(value); + } + } + // 没有权限就移除节点 + if (!ok) { + el.parentNode && el.parentNode.removeChild(el); + } +} diff --git a/src/directive/roles-directive.ts b/src/directive/roles-directive.ts new file mode 100644 index 00000000..8ee00980 --- /dev/null +++ b/src/directive/roles-directive.ts @@ -0,0 +1,38 @@ +import { hasRoles, matchRoles } from '@/plugins/auth-user'; +import { DirectiveBinding } from 'vue'; + +/** + * roles-权限标识 + * + * 指令值:字符串数组 + * + * 指令的参数:has/math,默认has + * + * v-roles="['admin']" + * 等同 + * v-roles:has="['admin']" + * + * v-roles:math="['common', 'user']" + * + * @param el 指令绑定到的元素 + * @param binding 一个对象 + */ +export default function (el: any, binding: DirectiveBinding) { + const value = binding.value; + let arg = binding.arg; + let ok: boolean = false; + if (Array.isArray(value) && value.length > 0) { + // 匹配 + if (arg === 'math') { + ok = matchRoles(value); + } + // 含有 + if (!arg || arg === 'has') { + ok = hasRoles(value); + } + } + // 没有权限就移除节点 + if (!ok) { + el.parentNode && el.parentNode.removeChild(el); + } +} diff --git a/src/hooks/useI18n.ts b/src/hooks/useI18n.ts new file mode 100644 index 00000000..fc76eba4 --- /dev/null +++ b/src/hooks/useI18n.ts @@ -0,0 +1,26 @@ +import { computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { localSet } from '@/utils/cache-local-utils'; +import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants'; + +export default function useLocale() { + //实例化i18n + const i18n = useI18n(); + + // 获取当前语言设置 + const currentLocale = computed(() => { + return i18n.locale.value; + }); + + // 切换语言 + const changeLocale = (value: string) => { + i18n.locale.value = value; + localSet(CACHE_LOCAL_I18N, value); + }; + + return { + currentLocale, + changeLocale, + t: i18n.t, + }; +} diff --git a/src/hooks/useLoading.ts b/src/hooks/useLoading.ts new file mode 100644 index 00000000..3750f69f --- /dev/null +++ b/src/hooks/useLoading.ts @@ -0,0 +1,16 @@ +import { ref, inject, provide } from 'vue'; + +const INJECT_LOADING_KEY = Symbol('loading_store'); + +export const createLoading = (v = false) => { + const loading = ref(v); + const change = (bool: boolean) => { + loading.value = bool; + }; + provide(INJECT_LOADING_KEY, loading); + return [loading, change]; +}; + +export const useLoading = () => { + return inject(INJECT_LOADING_KEY); +}; diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 00000000..8745b805 --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,58 @@ +import { onBeforeMount } from 'vue'; +import { ConfigProvider } from 'ant-design-vue/lib'; +import { CACHE_LOCAL_PRIMARY_COLOR } from '@/constants/cache-keys-constants'; +import { localGet, localSet } from '@/utils/cache-local-utils'; + +/** + * 初始主题色 + */ +export const usePrimaryColor = () => { + onBeforeMount(() => { + changePrimaryColor(getLocalColor()); + }); +}; + +/** + * 改变主题色 + * @param color 颜色 + */ +export function changePrimaryColor(color?: string) { + if (!color) { + color = getRandomColor(); + } + ConfigProvider.config({ + theme: { + primaryColor: color, + }, + }); + localSet(CACHE_LOCAL_PRIMARY_COLOR, color); +} + +/** + * 获取主题色-本地缓存 + * @returns 颜色 + */ +export function getLocalColor() { + return localGet(CACHE_LOCAL_PRIMARY_COLOR) || '#1890ff'; +} + +/** + * 获取随机颜色范围 + * @returns 颜色 + */ +function getRandomColor(): string { + const colors: string[] = [ + '#f5222d', + '#fa541c', + '#fa8c16', + '#a0d911', + '#13c2c2', + '#1890ff', + '#722ed1', + '#eb2f96', + '#faad14', + '#52c41a', + ]; + const i = Math.floor(Math.random() * 10); + return colors[i]; +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 00000000..aa344943 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,16 @@ +import { createI18n } from 'vue-i18n'; +import { localGet } from '@/utils/cache-local-utils'; +import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants'; +import zhCN from './locales/zh-CN'; +import enUS from './locales/en-US'; + +const i18n = createI18n({ + legacy: false, // 使用 Composition API 的方式创建 i18n 实例 + locale: localGet(CACHE_LOCAL_I18N) || 'zh_CN', // 默认显示语言 + messages: { + zh_CN: zhCN, + en_US: enUS, + }, +}); + +export default i18n; diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts new file mode 100644 index 00000000..0df53e9c --- /dev/null +++ b/src/i18n/locales/en-US.ts @@ -0,0 +1,62 @@ +export default { + // 语言 + i18n: 'English', + hello: 'Hello', + + // 通用 + common: { + title: 'Mask Antd Vue3', + desc: 'Management system based on ant design vue+vue3', + loading: 'Please wait...', + tipTitle: 'Prompt', + }, + + // 全局页脚 + globalFooter: { + help: 'Help', + privacy: 'Privacy', + term: 'Term', + }, + + // 校验 + valid: { + userNameReg: + 'The account cannot start with a number and can contain uppercase and lowercase letters, numbers, and no less than 5 digits.', + userNamePlease: 'Please enter the correct login account', + userNameHit: 'Login account', + passwordReg: + 'The password should contain at least uppercase and lowercase letters, numbers, special symbols, and no less than 6 digits.', + passwordPlease: 'Please enter the correct login password', + passwordHit: 'Login password', + passwordConfirmHit: 'Confirm login password', + phoneReg: 'Incorrect phone number', + phonePlease: 'Please enter the correct phone number', + phoneHit: 'Mobile number', + codePlease: 'Please enter the correct verification code', + codeHit: 'Verification code', + codeText: 'Obtain verification code', + codeSmsSend: 'Successfully sent, please pay attention to checking the SMS', + }, + + // 页面 + views: { + login: { + tabPane1: 'Account password login', + tabPane2: 'Login with phone number', + registerBtn: 'Register an account', + loginBtn: 'Login', + loginSuccess: 'Login successful', + loginMethod: 'Other login methods:', + loginMethodWX: 'WeChat Scan Login', + loginMethodQQ: 'QQ Scan Code Login', + }, + register: { + registerBtn: 'Register', + loginBtn: 'Log in with an existing account', + passwordErr: 'Please enter the correct confirmation password', + passwordConfirmErr: 'The two passwords entered do not match', + tipContent: 'Congratulations, {username} account registration succeeded!', + tipBtn: 'Go to login', + }, + }, +}; diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts new file mode 100644 index 00000000..86d95237 --- /dev/null +++ b/src/i18n/locales/zh-CN.ts @@ -0,0 +1,60 @@ +export default { + // 语言 + i18n: '中文', + hello: '你好', + + // 通用 + common: { + title: 'Mask Antd Vue3', + desc: '基于 ant-design-vue + vue3 的管理系统', + loading: '请稍等...', + tipTitle: '提示', + }, + + // 全局页脚 + globalFooter: { + help: '帮助', + privacy: '隐私', + term: '条款', + }, + + // 校验 + valid: { + userNameReg: '账号不能以数字开头,可包含大写小写字母,数字,且不少于5位', + userNamePlease: '请输入正确登录账号', + userNameHit: '登录账号', + passwordReg: '密码至少包含大小写字母、数字、特殊符号,且不少于6位', + passwordPlease: '请输入正确登录密码', + passwordHit: '登录密码', + passwordConfirmHit: '确认登录密码', + phoneReg: '手机号码不正确', + phonePlease: '请输入正确的手机号码', + phoneHit: '手机号码', + codePlease: '请输入正确的验证码', + codeHit: '验证码', + codeText: '获取验证码', + codeSmsSend: '发送成功,请注意查看短信', + }, + + // 页面 + views: { + login: { + tabPane1: '账号密码登录', + tabPane2: '手机号登录', + registerBtn: '注册账号', + loginBtn: '登录', + loginSuccess: '登录成功', + loginMethod: '其他登录方式:', + loginMethodWX: '微信扫一扫登录', + loginMethodQQ: 'QQ扫码登录', + }, + register: { + registerBtn: '注册', + loginBtn: '使用已有账号登录', + passwordErr: '请正确输入确认密码', + passwordConfirmErr: '两次输入的密码不一致', + tipContent: '恭喜您,{username} 账号注册成功!', + tipBtn: '前往登录', + }, + }, +}; diff --git a/src/layouts/BasicLayout.vue b/src/layouts/BasicLayout.vue new file mode 100644 index 00000000..2b290f03 --- /dev/null +++ b/src/layouts/BasicLayout.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/src/layouts/BlankLayout.vue b/src/layouts/BlankLayout.vue new file mode 100644 index 00000000..0c0d640e --- /dev/null +++ b/src/layouts/BlankLayout.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/layouts/LinkLayout.vue b/src/layouts/LinkLayout.vue new file mode 100644 index 00000000..7c322a81 --- /dev/null +++ b/src/layouts/LinkLayout.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/layouts/components/RightContent.vue b/src/layouts/components/RightContent.vue new file mode 100644 index 00000000..e0a1decd --- /dev/null +++ b/src/layouts/components/RightContent.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/src/layouts/components/Tabs.vue b/src/layouts/components/Tabs.vue new file mode 100644 index 00000000..4c2507c4 --- /dev/null +++ b/src/layouts/components/Tabs.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..de1255a8 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,16 @@ +import { createApp } from 'vue'; +import store from './store'; +import App from './App.vue'; +import router from './router'; +import directive from './directive'; +import i18n from './i18n'; +import '@ant-design-vue/pro-layout/dist/style.css'; +import 'ant-design-vue/dist/antd.variable.min.css'; + +const app = createApp(App); +app.use(store); +app.use(router); +app.use(directive); +app.use(i18n); + +app.mount('#app'); diff --git a/src/plugins/auth-token.ts b/src/plugins/auth-token.ts new file mode 100644 index 00000000..2c5bd953 --- /dev/null +++ b/src/plugins/auth-token.ts @@ -0,0 +1,17 @@ +import Cookies from 'js-cookie'; +import { TOKEN_COOKIE } from '@/constants/token-constants'; + +/**获取cookis中Token字符串 */ +export function getToken(): string { + return Cookies.get(TOKEN_COOKIE) || ''; +} + +/**设置cookis中Token字符串 */ +export function setToken(token: string): void { + Cookies.set(TOKEN_COOKIE, token); +} + +/**移除cookis中Token字符串 */ +export function removeToken(): void { + Cookies.remove(TOKEN_COOKIE); +} diff --git a/src/plugins/auth-user.ts b/src/plugins/auth-user.ts new file mode 100644 index 00000000..f736c6d3 --- /dev/null +++ b/src/plugins/auth-user.ts @@ -0,0 +1,54 @@ +import { ADMIN_PERMISSION, ADMIN_ROLE_KEY } from '@/constants/admin-constants'; +import useUserStore from '@/store/modules/user'; + +/** + * 只需含有其中权限 + * @param role 权限字符数组 + * @returns true | false + */ +export function hasPermissions(permissions: string[]): boolean { + if (!permissions || permissions.length === 0) return false; + const userPermissions = useUserStore().permissions; + if (!userPermissions || userPermissions.length === 0) return false; + if (userPermissions.includes(ADMIN_PERMISSION)) return true; + return permissions.some(p => userPermissions.some(up => up === p)); +} + +/** + * 同时匹配其中权限 + * @param role 权限字符数组 + * @returns true | false + */ +export function matchPermissions(permissions: string[]): boolean { + if (!permissions || permissions.length === 0) return false; + const userPermissions = useUserStore().permissions; + if (!userPermissions || userPermissions.length === 0) return false; + if (userPermissions.includes(ADMIN_PERMISSION)) return true; + return permissions.every(p => userPermissions.some(up => up === p)); +} + +/** + * 只需含有其中角色 + * @param role 角色字符数组 + * @returns true | false + */ +export function hasRoles(roles: string[]): boolean { + if (!roles || roles.length === 0) return false; + const userRoles = useUserStore().roles; + if (!userRoles || userRoles.length === 0) return false; + if (userRoles.includes(ADMIN_ROLE_KEY)) return true; + return roles.some(r => userRoles.some(ur => ur === r)); +} + +/** + * 同时匹配其中角色 + * @param role 角色字符数组 + * @returns true | false + */ +export function matchRoles(roles: string[]): boolean { + if (!roles || roles.length === 0) return false; + const userRoles = useUserStore().roles; + if (!userRoles || userRoles.length === 0) return false; + if (userRoles.includes(ADMIN_ROLE_KEY)) return true; + return roles.every(r => userRoles.some(ur => ur === r)); +} diff --git a/src/plugins/http-fetch.ts b/src/plugins/http-fetch.ts new file mode 100644 index 00000000..24985d4d --- /dev/null +++ b/src/plugins/http-fetch.ts @@ -0,0 +1,270 @@ +import { getToken, removeToken } from '@/plugins/auth-token'; +import { sessionGetJSON, sessionSetJSON } from '@/utils/cache-session-utils'; +import { TOKEN_KEY, TOKEN_KEY_PREFIX } from '@/constants/token-constants'; +import { CACHE_SESSION_FATCH } from '@/constants/cache-keys-constants'; +import { + APP_REQUEST_HEADER_CODE, + APP_REQUEST_HEADER_VERSION, +} from '@/constants/app-constants'; + +/**响应结果类型 */ +export type ResultType = { + /**响应码 */ + code: number | 200 | 500; + /**信息 */ + msg: string; + /**数据 */ + data?: any; + /**未知属性 */ + [key: string]: any; +}; + +/**防止重复提交类型 */ +type RepeatSubmitType = { + /**请求地址 */ + url: string; + /**请求数据 */ + data: string; + /**请求时间 */ + time: number; +}; + +/**请求参数类型 */ +type OptionsType = { + /**请求地址 */ + url: string; + /**请求方法 */ + method: 'get' | 'post' | 'put' | 'delete'; + /**请求头 */ + headers?: HeadersInit; + /**地址栏参数 */ + params?: Record; + /**发送数据 */ + data?: Record | FormData | object; + /**请求数据类型 */ + dataType?: 'form-data' | 'json'; + /**响应数据类型 */ + responseType?: 'text' | 'json' | 'blob' | 'arrayBuffer'; + /**请求缓存策略 */ + cache?: RequestCache; + /**请求的凭证,如 omit、same-origin、include */ + credentials?: RequestCredentials; + /**请求体 */ + body?: BodyInit; + /**防止数据重复提交 */ + repeatSubmit?: boolean; + /**携带授权Token请求头 */ + whithToken?: boolean; +}; + +/**全局配置类型 */ +type ConfigType = { + /**请求的根域名地址-不带/后缀 */ + baseUrl: string; + /**超时时间,毫秒 */ + timeout: number; +}; + +/**默认配置 */ +const FATCH_CONFIG: ConfigType = { + baseUrl: import.meta.env.VITE_API_BASE_URL, + timeout: 10 * 1000, +}; + +/**默认请求参数 */ +const FATCH_OPTIONS: OptionsType = { + url: '', + method: 'get', + headers: { + [APP_REQUEST_HEADER_CODE]: import.meta.env.VITE_APP_CODE, + [APP_REQUEST_HEADER_VERSION]: import.meta.env.VITE_APP_VERSION, + // 使用mock.apifox.cn时开启 + // apifoxToken: '8zCzh3vipdEwd1ukv9lQEuTekdWIH7xN', + }, + dataType: 'json', + responseType: 'json', + cache: 'no-cache', + credentials: undefined, + repeatSubmit: true, + whithToken: true, +}; + +/**请求前的拦截 */ +function beforeRequest(options: OptionsType): OptionsType | Promise { + options.headers = Object.assign({}, options.headers); + //console.log('请求前的拦截', options); + + // 给发送数据类型设置请求头 + if (options.dataType === 'json') { + Reflect.set( + options.headers, + 'content-type', + 'application/json;charset=utf-8' + ); + } + + // 是否需要设置 token + const token = getToken(); + if (options.whithToken && token) { + Reflect.set(options.headers, TOKEN_KEY, TOKEN_KEY_PREFIX + token); + } + // 是否需要防止数据重复提交 + if ( + options.repeatSubmit && + options.dataType === 'json' && + ['post', 'put'].includes(options.method) + ) { + const requestObj: RepeatSubmitType = { + url: options.url, + data: JSON.stringify(options.data), + time: Date.now(), + }; + const sessionObj: RepeatSubmitType = sessionGetJSON(CACHE_SESSION_FATCH); + if (sessionObj) { + const { url, data, time } = sessionObj; + const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 + if ( + requestObj.url === url && + requestObj.data === data && + requestObj.time - time < interval + ) { + const message = '数据正在处理,请勿重复提交'; + console.warn(`[${url}]: ${message}`); + return Promise.reject(message); + } else { + sessionSetJSON(CACHE_SESSION_FATCH, requestObj); + } + } else { + sessionSetJSON(CACHE_SESSION_FATCH, requestObj); + } + } + + // get请求拼接地址栏参数 + if (options.method === 'get' && options.params) { + let paramStr = ''; + const params = options.params; + for (const key in params) { + const value = params[key]; + // 空字符或未定义的值不作为参数发送 + if (value === '' || value === undefined) continue; + paramStr += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } + if (paramStr && paramStr.startsWith('&')) { + options.url = `${options.url}?${paramStr.substring(1)}`; + } + } + + // 非get参数提交 + if (options.data instanceof FormData) { + options.body = options.data; + } else { + options.body = JSON.stringify(options.data); + } + return options; +} + +/**请求后的拦截 */ +function interceptorResponse(res: ResultType): ResultType | Promise { + //console.log('请求后的拦截', res); + // 登录失效时,移除授权令牌并重新刷新页面 + if (res.code === 401) { + removeToken(); + window.location.reload(); + } + return res; +} + +/** + * 请求http + * + * @param options 请求参数 + * + * responseType改变响应结果类型 + * @returns 返回 Promise + */ +export async function request(options: OptionsType): Promise { + // 请求超时控制请求终止 + const controller = new AbortController(); + const { signal } = controller; + const timeoutId = setTimeout(() => { + controller.abort(); // 终止请求 + }, FATCH_CONFIG.timeout); + + options = Object.assign({ signal }, FATCH_OPTIONS, options); + + // 检查请求拦截 + const beforeReq = beforeRequest(options); + if (beforeReq instanceof Promise) { + return await beforeReq; + } + options = beforeReq; + + // 判断用户传递的URL是否http或/开头 + if (!options.url.startsWith('http')) { + const uri = options.url.startsWith('/') ? options.url : `/${options.url}`; + options.url = FATCH_CONFIG.baseUrl + uri; + } + + try { + const res = await fetch(options.url, options); + // console.log('请求结果:', res); + if (res.status === 500) { + return { + code: 500, + msg: '服务器连接出错!', + }; + } + + // 根据响应数据类型返回 + switch (options.responseType) { + case 'text': // 文本数据 + const str = await res.text(); + return { + code: 200, + msg: str, + }; + case 'json': // json格式数据 + const result = await res.json(); + // 请求后的拦截 + const beforeRes = interceptorResponse(result); + if (beforeRes instanceof Promise) { + return await beforeRes; + } + return result; + case 'blob': // 二进制数据则直接返回 + case 'arrayBuffer': + const contentType = res.headers.get('content-type') || ''; + if (contentType.startsWith('application/json')) { + const result = await res.json(); + return result as ResultType; + } + const data = + options.responseType === 'blob' + ? await res.blob() + : await res.arrayBuffer(); + return { + code: 200, + msg: '成功', + data: data, + status: res.status, + headers: res.headers, + }; + default: + return { + code: 500, + msg: '未知响应数据类型', + }; + } + } catch (error: any) { + // 请求被终止时 + if (error.name === 'AbortError') { + return { + code: 500, + msg: '网络连接超时!', + }; + } + throw error; + } finally { + clearTimeout(timeoutId); // 请求成功,清除超时计时器 + } +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 00000000..0915ead7 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,289 @@ +import { + createRouter, + createWebHistory, + createWebHashHistory, + RouteRecordRaw, +} from 'vue-router'; +import NProgress from 'nprogress'; +import 'nprogress/nprogress.css'; +import BasicLayout from '../layouts/BasicLayout.vue'; +import BlankLayout from '../layouts/BlankLayout.vue'; +import LinkLayout from '../layouts/LinkLayout.vue'; +import { encode } from 'js-base64'; +import { getToken } from '@/plugins/auth-token'; +import { validHttp } from '@/utils/regular-utils'; +import useUserStore from '@/store/modules/user'; +import useAppStore from '@/store/modules/app'; +import useRouterStore from '@/store/modules/router'; + +// NProgress Configuration +NProgress.configure({ showSpinner: false }); + +// import { MetaRecord, MenuDataItem } from '@ant-design-vue/pro-layout'; +// mate数据类型 MetaRecord +// 根据/路径构建菜单列表,列表项类型 MenuDataItem +// https://github.com/vueComponent/pro-components/blob/a19279f3a28190bf11e8c36f316c92dbd3387a6d/packages/pro-layout/src/typings.ts#L16 +// 菜单图标来源 https://ant.design/components/icon 自定义iconfont + +/**公共路由 */ +const constantRoutes: RouteRecordRaw[] = [ + { + path: '/', + name: 'Root', + meta: { title: '根节点' }, + component: BasicLayout, + redirect: '/index', + children: [ + { + path: '/index', + name: 'Index', + meta: { title: '首页', icon: 'icon-pcduan', cache: true }, + component: () => import('@/views/index.vue'), + }, + { + path: '/dome1', + name: 'Dome1', + meta: { title: '示例一', icon: 'icon-ios' }, + component: () => import('@/views/dome/dome1.vue'), + }, + { + path: '/dome2', + name: 'Dome2', + meta: { title: '示例二', icon: 'icon-anzhuo' }, + component: () => import('@/views/dome/dome2.vue'), + }, + { + path: '/dome3', + name: 'Dome3', + meta: { title: '示例三', icon: 'icon-qunzhu' }, + component: () => import('@/views/dome/dome3.vue'), + }, + { + path: '/domes', + name: 'Domes', + meta: { + title: '示例目录', + icon: 'icon-zhizuoliucheng', + }, + component: BlankLayout, + redirect: () => ({ name: 'PageInfo' }), + children: [ + { + path: 'page-info', + name: 'PageInfo', + meta: { title: '页面信息', icon: 'icon-huifu' }, + component: () => import('../views/domes/page-info.vue'), + }, + { + path: 'page-typography', + name: 'PageTypography', + meta: { title: '文本信息', icon: 'icon-huizhiguize' }, + component: () => import('../views/domes/page-typography.vue'), + }, + { + path: 'dynamic-match/:id(\\d+)', + name: 'DynamicMatch', + // 路由 path 默认参数再 meta.params 里 + meta: { title: '动态参数页面', params: { id: 1 }, cache: true }, + component: () => import('../views/domes/dynamic-match.vue'), + }, + { + path: 'disabled', + name: 'Disabled', + meta: { title: '禁止点击', disabled: true }, + component: () => {}, + }, + { + path: 'https://github.com/TsMask', + name: 'BlankGithubTsMask', + meta: { + title: 'TsMask-打开新窗', + icon: 'icon-github', + target: '_blank', + }, + component: () => {}, + }, + { + path: encode('https://www.antdv.com/components/comment-cn'), + name: 'HttpsAntDesignVue', + meta: { + title: 'Antdv-内嵌窗口', + icon: 'icon-morentouxiang', + target: null, + }, + component: LinkLayout, + }, + ], + }, + { + path: 'https://github.com/', + name: 'BlankGithub', + meta: { + title: 'Github-打开新窗', + icon: 'icon-github', + target: '_blank', + }, + component: () => {}, + }, + { + path: 'https://www.antdv.com/components/comment-cn?sdf=12321&id=12&sdnf', + name: 'SelfAnt Design Vue', + meta: { + title: 'Antdv-当前窗口', + icon: 'icon-morentouxiang', + target: '_self', + }, + component: LinkLayout, + }, + { + path: '/account', + name: 'Account', + meta: { + title: '个人中心', + }, + component: BlankLayout, + redirect: '/account/profile', + children: [ + { + path: 'profile', + name: 'Profile', + meta: { title: '个人信息', cache: true }, + component: () => import('@/views/account/profile.vue'), + }, + { + path: 'settings', + name: 'Settings', + meta: { title: '个人设置', cache: true }, + component: () => import('@/views/account/settings.vue'), + }, + ], + }, + ], + }, + { + path: '/login', + name: 'Login', + meta: { title: '登录' }, + component: () => import('@/views/login.vue'), + }, + { + path: '/register', + name: 'Register', + meta: { title: '注册' }, + component: () => import('@/views/register.vue'), + }, + { + path: '/403', + name: 'NotPermission', + meta: { title: '没有访问权限' }, + component: () => import('@/views/error/403.vue'), + }, + { + path: '/redirect', + name: 'Redirect', + meta: { title: '重定向' }, + component: BasicLayout, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/views/redirect/index.vue'), + }, + ], + }, + { + path: '/:pathMatch(.*)*', + meta: { title: '找不到匹配页面' }, + component: () => import('@/views/error/404.vue'), + }, +]; + +// 根据.env配置获取是否带井号和基础路径 +const hasHash = import.meta.env.VITE_HISTORY_HASH; +const bashUrl = import.meta.env.VITE_HISTORY_BASE_URL; + +/**全局路由 */ +const router = createRouter({ + history: + hasHash === 'true' + ? createWebHashHistory(bashUrl) + : createWebHistory(bashUrl), + routes: constantRoutes, + scrollBehavior(to, from, savedPosition) { + if (savedPosition) { + return savedPosition; + } else { + return { top: 0 }; + } + }, +}); + +/**全局路由-后置守卫 */ +router.afterEach((to, from, failure) => { + NProgress.done(); + // 设置标题 + if (to.meta?.title) { + useAppStore().setTitle(to.meta.title); + } +}); + +/**无Token可访问页面地址白名单 */ +const WHITE_LIST: string[] = ['/login', '/auth-redirect', '/bind', '/register']; + +/**全局路由-前置守卫 */ +router.beforeEach((to, from, next) => { + NProgress.start(); + const token = getToken(); + + // 没有token + if (!token) { + if (WHITE_LIST.includes(to.path)) { + // 在免登录白名单,直接进入 + next(); + } else { + // 否则全部重定向到登录页 + next(`/login?redirect=${to.fullPath}`); + } + } + + // 有Token + if (token) { + // 防止重复访问登录页面 + if (to.path === '/login') { + next({ name: 'Index' }); + } else { + // 判断当前用户是否有角色信息 + const user = useUserStore(); + if (user.roles && user.roles.length === 0) { + // 获取用户信息 + user + .fnGetInfo() + .then(() => { + return useRouterStore().generateRoutes(); + }) + .then(accessRoutes => { + // 根据后台配置生成可访问的路由表 + if (accessRoutes && accessRoutes.length !== 0) { + for (const route of accessRoutes) { + // 动态添加可访问路由表,http开头会异常 + if (!validHttp(route.path)) { + router.addRoute(route); + } + } + } + // 刷新替换原先路由,确保addRoutes已完成 + next({ ...to, replace: true }); + }) + .catch(e => { + console.error(`[${to.path}]: ${e.message}`); + user.fnLogOut().finally(() => { + next({ name: 'Login' }); + }); + }); + } else { + next(); + } + } + } +}); + +export default router; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 00000000..02ae96a9 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,5 @@ +import { createPinia } from 'pinia'; + +const store = createPinia(); + +export default store; diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts new file mode 100644 index 00000000..eb6d7b2a --- /dev/null +++ b/src/store/modules/app.ts @@ -0,0 +1,31 @@ +import { defineStore } from 'pinia'; + +/**应用参数类型 */ +type AppStore = { + /**应用名称 */ + appName: string; + /**应用标识 */ + appCode: string; + /**应用版本 */ + appVersion: string; +}; + +const useAppStore = defineStore('app', { + state: (): AppStore => ({ + appName: import.meta.env.VITE_APP_NAME, + appCode: import.meta.env.VITE_APP_CODE, + appVersion: import.meta.env.VITE_APP_VERSION, + }), + actions: { + /**设置网页标题 */ + setTitle(title?: string) { + if (title) { + document.title = `${title} - ${this.appName}`; + } else { + document.title = this.appName; + } + }, + }, +}); + +export default useAppStore; diff --git a/src/store/modules/dict.ts b/src/store/modules/dict.ts new file mode 100644 index 00000000..ec183b09 --- /dev/null +++ b/src/store/modules/dict.ts @@ -0,0 +1,63 @@ +import { defineStore } from 'pinia'; +import { getDictDataType } from '@/api/system/dict/data'; + +/**字典参数类型 */ +type DictStore = { + /**字典数据 */ + dicts: Map; +}; + +const useDictStore = defineStore('dict', { + state: (): DictStore => ({ + dicts: new Map(), + }), + actions: { + /**清空字典 */ + clearDict() { + this.dicts.clear(); + }, + /**删除字典 */ + removeDict(key: string) { + if (!key) return; + return this.dicts.delete(key); + }, + /** + * 处理字典数据对象用于回显标签 + * @param data 字典数据项 + * @returns + */ + parseDataDict(data: Record) { + return [ + { + label: data.dictLabel, + value: data.dictValue, + elTagType: data.tagType, + elTagClass: data.tagClass, + }, + ]; + }, + /**获取字典 */ + async getDict(key: string) { + if (!key) return []; + let disct = this.dicts.get(key); + if (disct === undefined || disct.length === 0) { + const res = await getDictDataType(key); + if (res.code === 200 && Array.isArray(res.data)) { + const dictData: DictType[] = res.data.map(d => ({ + label: d.dictLabel, + value: d.dictValue, + elTagType: d.tagType, + elTagClass: d.tagClass, + })); + this.dicts.set(key, dictData); + disct = dictData; + } else { + disct = []; + } + } + return disct; + }, + }, +}); + +export default useDictStore; diff --git a/src/store/modules/layout.ts b/src/store/modules/layout.ts new file mode 100644 index 00000000..0fb8fa0f --- /dev/null +++ b/src/store/modules/layout.ts @@ -0,0 +1,91 @@ +import { CACHE_LOCAL_PROCONFIG } from '@/constants/cache-keys-constants'; +import { localGetJSON, localSetJSON } from '@/utils/cache-local-utils'; +import { defineStore } from 'pinia'; + +/**布局参数类型 */ +type LayoutStore = { + /**布局设置抽屉显示 */ + visible: boolean; + /**布局配置 */ + proConfig: { + /**导航布局 */ + layout: 'side' | 'top' | 'mix'; + /**导航菜单主题色 */ + navTheme: 'dark' | 'light'; + /**顶部导航主题,仅导航布局为mix时生效 */ + headerTheme: 'dark' | 'light'; + /**固定顶部栏 */ + fixedHeader: boolean; + /**固定菜单栏 */ + fixSiderbar: boolean; + /**自动分割菜单 */ + splitMenus: boolean; + /**内容区域-顶栏 */ + headerRender: any | boolean | undefined; + /**内容区域-页脚 */ + footerRender: any | boolean | undefined; + /**内容区域-菜单头 */ + menuHeaderRender: any | boolean | undefined; + /**内容区域-导航标签项 */ + tabRender: any | boolean | undefined; + }; + /**水印内容 */ + waterMarkContent: string; +}; + +/**判断是否关闭内容区域 */ +const proRender = (render: any) => (render === false ? false : undefined); + +/**本地缓存-布局配置设置 */ +const proConfigLocal: LayoutStore['proConfig'] = localGetJSON( + CACHE_LOCAL_PROCONFIG +) || { + layout: 'side', + headerTheme: 'light', + navTheme: 'light', + fixSiderbar: true, + fixedHeader: true, + splitMenus: true, +}; + +const useLayoutStore = defineStore('layout', { + state: (): LayoutStore => ({ + visible: false, + proConfig: { + layout: proConfigLocal.layout, + navTheme: proConfigLocal.navTheme, + headerTheme: proConfigLocal.headerTheme, + fixedHeader: Boolean(proConfigLocal.fixedHeader), + fixSiderbar: Boolean(proConfigLocal.fixSiderbar), + splitMenus: Boolean(proConfigLocal.splitMenus), + headerRender: proRender(proConfigLocal.headerRender), + footerRender: proRender(proConfigLocal.footerRender), + menuHeaderRender: proRender(proConfigLocal.menuHeaderRender), + tabRender: proRender(proConfigLocal.tabRender), + }, + waterMarkContent: import.meta.env.VITE_APP_NAME, + }), + actions: { + /**改变显示状态 */ + changeVisibleLayoutSetting() { + this.visible = !this.visible; + }, + /**修改水印文字 */ + changeWaterMark(text: string) { + this.waterMarkContent = text; + }, + /**修改布局设置 */ + changeConf(key: string, value: boolean | string | number | undefined) { + if (Reflect.has(this.proConfig, key)) { + // 同时修改mix混合菜单的导航主题 + if (key === 'navTheme') { + Reflect.set(this.proConfig, 'headerTheme', value); + } + Reflect.set(this.proConfig, key, value); + localSetJSON(CACHE_LOCAL_PROCONFIG, this.proConfig); + } + }, + }, +}); + +export default useLayoutStore; diff --git a/src/store/modules/router.ts b/src/store/modules/router.ts new file mode 100644 index 00000000..41f9ff87 --- /dev/null +++ b/src/store/modules/router.ts @@ -0,0 +1,152 @@ +import { defineStore } from 'pinia'; +import { + RouteComponent, + RouteLocationRaw, + RouteMeta, + RouteRecordRaw, +} from 'vue-router'; +import { getRouters } from '@/api/router'; +import BasicLayout from '@/layouts/BasicLayout.vue'; +import BlankLayout from '@/layouts/BlankLayout.vue'; +import LinkLayout from '@/layouts/LinkLayout.vue'; +import { + MENU_COMPONENT_LAYOUT_BASIC, + MENU_COMPONENT_LAYOUT_BLANK, + MENU_COMPONENT_LAYOUT_LINK, +} from '@/constants/menu-constants'; + +/**路由构建参数类型 */ +type RouterStore = { + /**初始的根路由数据 */ + rootRouterData: RouteRecordRaw[]; + /**动态路由数据 */ + buildRouterData: RouteRecordRaw[]; +}; + +const useRouterStore = defineStore('router', { + state: (): RouterStore => ({ + rootRouterData: [], + buildRouterData: [], + }), + actions: { + /** + * 记录初始根节点菜单数据 + * @param data 初始数据 + * @returns 初始数据 + */ + setRootRouterData(data: RouteRecordRaw[]) { + if (this.rootRouterData.length <= 0) { + this.rootRouterData = data; + } + return this.rootRouterData; + }, + /** + * 动态路由列表数据生成 + * @returns 生成的路由菜单 + */ + async generateRoutes() { + const res = await getRouters(); + if (res.code === 200 && Array.isArray(res.data)) { + const buildRoutes = buildRouters(res.data.concat()); + this.buildRouterData = buildRoutes; + return buildRoutes; + } + return []; + }, + }, +}); + +/**异步路由类型 */ +type RecordRaws = { + path: string; + name: string; + meta: RouteMeta; + redirect: RouteLocationRaw; + component: string; + children: RecordRaws[]; +}; + +/** + * 构建动态路由 + * + * 遍历后台配置的路由菜单,转换为组件路由菜单 + * + * @param recordRaws 异步路由列表 + * @returns 可添加的路由列表 + */ +function buildRouters(recordRaws: RecordRaws[]): RouteRecordRaw[] { + const routers: RouteRecordRaw[] = []; + for (const item of recordRaws) { + // 路由页面组件 + let component: RouteComponent = {}; + if (item.component) { + const comp = item.component; + if (comp === MENU_COMPONENT_LAYOUT_BASIC) { + component = BasicLayout; + } else if (comp === MENU_COMPONENT_LAYOUT_BLANK) { + component = BlankLayout; + } else if (comp === MENU_COMPONENT_LAYOUT_LINK) { + component = LinkLayout; + } else { + // 指定页面视图,一般用于显示子菜单 + component = findView(comp); + } + } + + // 有子菜单进行递归 + let children: RouteRecordRaw[] = []; + if (item.children && item.children.length > 0) { + children = buildRouters(item.children); + } + + // 对元数据特殊参数进行处理 + let metaIcon = (item.meta?.icon as string) || ''; + if (!metaIcon.startsWith('icon-')) { + metaIcon = ''; + } + item.meta = Object.assign(item.meta, { + icon: metaIcon, + }); + + // 构建路由 + const router: RouteRecordRaw = { + path: item.path, + name: item.name, + meta: item.meta, + redirect: item.redirect, + component: component, + children: children, + }; + routers.push(router); + } + return routers; +} + +/**匹配views里面所有的.vue或.tsx文件 */ +const views = import.meta.glob('./../../views/**/*.{vue,tsx}'); + +/** + * 查找页面模块 + * + * 查找 `/views/system/menu/index.vue` 或 `/views/system/menu/index.tsx` + * + * 参数值为 `system/menu/index` + * + * @param dirName 组件路径 + * @returns 路由懒加载函数 + */ +function findView(dirName: string) { + for (const dir in views) { + let viewDirName = ''; + const component = dir.match(/views\/(.+)\.(vue|tsx)/); + if (component && component.length === 3) { + viewDirName = component[1]; + } + if (viewDirName === dirName) { + return () => views[dir](); + } + } + return () => import('@/views/error/404.vue'); +} + +export default useRouterStore; diff --git a/src/store/modules/tabs.ts b/src/store/modules/tabs.ts new file mode 100644 index 00000000..3ab22d82 --- /dev/null +++ b/src/store/modules/tabs.ts @@ -0,0 +1,189 @@ +import { defineStore } from 'pinia'; +import type { LocationQuery, RouteLocationNormalizedLoaded } from 'vue-router'; + +/**导航标签栏类型 */ +type TabsStore = { + /**标签列表 */ + tabs: TabType[]; + /**激活标签项 */ + activePath: string; + /**缓存页面路由名称 */ + caches: Set; +}; + +/**标签信息类型 */ +type TabType = { + path: string; + query: LocationQuery; + name: string; + title: string; + icon?: any; + cache?: boolean; +}; + +const useTabsStore = defineStore('tabs', { + state: (): TabsStore => ({ + tabs: [], + activePath: '', + caches: new Set(), + }), + getters: { + /**获取导航标签栏列表 */ + getTabs(state) { + return state.tabs; + }, + /**获取缓存页面名 */ + getCaches(state) { + return [...state.caches]; + }, + }, + actions: { + /**清空标签项和缓存项列表 */ + clear() { + this.tabs = []; + this.caches.clear(); + }, + /** + * 删除标签项 + * @param path 当期标签路由地址 + * @returns 布尔 true/false + */ + remove(path: string) { + if (!path) return false; + const tabIndex = this.tabs.findIndex(tab => tab.path === path); + if (tabIndex === -1) return false; + // 同名称标签只剩一个时,才移除缓存 + const name = this.tabs[tabIndex].name; + const tabs = this.tabs.filter(tab => tab.name === name); + if (tabs.length <= 1) { + this.cacheDelete(name); + } + this.tabs.splice(tabIndex, 1); + return true; + }, + /** + * 添加标签项 + * @param tab 标签信息对象 + * @param index 插入指定位置,默认加到最后 + * @returns 布尔 true/false + */ + add(tab: TabType, index?: number) { + const { path, query, name, title, icon, cache } = tab; + // 是否缓存 + if (cache) { + this.cacheAdd(name); + } + // 获取没有才添加 + let tabIndex = this.tabs.findIndex(tab => tab.path === path); + if (tabIndex >= 0) return false; + const idx = index ? index : this.tabs.length; + this.tabs.splice(idx, 0, { path, query, name, title, icon }); + return true; + }, + /**添加缓存项 + * @param name 路由名称 + * @returns 布尔 true/false + */ + cacheAdd(name: string) { + if (!name) return; + const has = this.caches.has(name); + if (has) return; + this.caches.add(name); + }, + /** + * 删除缓存项 + * @param name 路由名称 + * @returns 布尔 true/false + */ + cacheDelete(name: string) { + if (!name) return false; + const has = this.caches.has(name); + if (!has) return false; + return this.caches.delete(name); + }, + + /** + * 打开标签 + * + * 动态参数会开新标签,这是考虑多信息查看才没用同一个标签打开。 + * @param raw 跳转的路由信息 + * @returns 无 + */ + tabOpen(raw: RouteLocationNormalizedLoaded) { + // 刷新是重定向不记录 + if (raw.path.startsWith('/redirect')) return; + // 标签缓存使用路由名称 + const name = (raw.name && raw.name.toString()) || '-'; + // 新增到当期标签后面打开,获取当期标签下标 + const tabIndex = this.tabs.findIndex(tab => tab.path === this.activePath); + this.add( + { + path: raw.path, + query: raw.query, + name: name, + title: raw.meta.title || '-', + icon: raw.meta.icon || '#', + cache: Boolean(raw.meta.cache), + }, + tabIndex + 1 + ); + // 设置激活项 + this.activePath = raw.path; + }, + /** + * 关闭标签 + * @param path 当期标签路由地址 + * @returns 新跳转push路由参数 + */ + tabClose(path: string) { + if (!path) return null; + // 获取当前项和最后项下标 + const tabIndex = this.tabs.findIndex(tab => tab.path === path); + if (tabIndex === -1) return null; + const lastIndex = this.tabs.length - 1; + let to = null; + // 只有一项默认跳首页 + if (lastIndex === 0) { + to = { + path: '/index', + query: {}, + }; + } + // 关闭当期标签,操作第一项跳后一项 + else if (path === this.activePath && tabIndex === 0) { + const tab = this.tabs[tabIndex + 1]; + to = { + path: tab.path, + query: tab.query, + }; + } + // 关闭当期标签,默认跳前一项 + else if (path === this.activePath && tabIndex <= lastIndex) { + const tab = this.tabs[tabIndex - 1]; + to = { + path: tab.path, + query: tab.query, + }; + } + // 移除标签 + this.remove(path); + return to; + }, + /** + * 跳转标签 + * @param path 当期标签路由地址 + * @returns 新跳转push路由参数 + */ + tabGoto(path: string) { + if (!path) return null; + const tab = this.tabs.find(tab => tab.path === path); + if (!tab) return null; + return { + path: tab.path, + query: tab.query, + }; + }, + }, +}); + +export default useTabsStore; diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts new file mode 100644 index 00000000..6930cf58 --- /dev/null +++ b/src/store/modules/user.ts @@ -0,0 +1,171 @@ +import defaultAvatar from '@/assets/images/default_avatar.png'; +import useLayoutStore from './layout'; +import { login, logout, getInfo } from '@/api/login'; +import { getToken, setToken, removeToken } from '@/plugins/auth-token'; +import { defineStore } from 'pinia'; +import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants'; +import { validHttp } from '@/utils/regular-utils'; + +/**用户信息类型 */ +type UserInfo = { + /**授权凭证 */ + token: string; + /**登录账号 */ + userName: string; + /**用户角色 字符串数组 */ + roles: string[]; + /**用户权限 字符串数组 */ + permissions: string[]; + /**用户头像 */ + avatar: string; + /**用户昵称 */ + nickName: string; + /**用户手机号 */ + phonenumber: string; + /**用户邮箱 */ + email: string; + /**用户性别 */ + sex: string | undefined; +}; + +/** + * 格式解析头像地址 + * @param avatar 头像路径 + * @returns url地址 + */ +function parseAvatar(avatar: string): string { + if (!avatar) { + return defaultAvatar; + } + if (validHttp(avatar)) { + return avatar; + } + const baseApi = import.meta.env.VITE_API_BASE_URL; + return `${baseApi}${avatar}`; +} + +const useUserStore = defineStore('user', { + state: (): UserInfo => ({ + token: getToken(), + userName: '', + roles: [], + permissions: [], + avatar: '', + nickName: '', + phonenumber: '', + email: '', + sex: undefined, + }), + getters: { + /** + * 获取正确头像地址 + * @param state 内部属性不用传入 + * @returns 头像地址url + */ + getAvatar(state) { + return parseAvatar(state.avatar); + }, + /** + * 获取基础信息属性 + * @param state 内部属性不用传入 + * @returns 基础信息 + */ + getBaseInfo(state) { + return { + nickName: state.nickName, + phonenumber: state.phonenumber, + email: state.email, + sex: state.sex, + }; + }, + }, + actions: { + /** + * 更新基础信息属性 + * @param data 变更信息 + */ + setBaseInfo(data: Record) { + this.nickName = data.nickName; + this.phonenumber = data.phonenumber; + this.email = data.email; + this.sex = data.sex; + }, + /** + * 更新头像 + * @param avatar 上传后的地址 + */ + setAvatar(avatar: string) { + this.avatar = avatar; + }, + /** + * 获取正确头像地址 + * @param avatar + */ + fnAvatar(avatar: string) { + return parseAvatar(avatar); + }, + // 登录 + async fnLogin(loginBody: Record) { + const res = await login(loginBody); + if (res.code === 200 && res.data) { + const token = res.data[TOKEN_RESPONSE_FIELD]; + setToken(token); + this.token = token; + } + return res; + }, + // 获取用户信息 + async fnGetInfo() { + const res = await getInfo(); + if (res.code === 200 && res.data) { + const { user, roles, permissions } = res.data; + // 登录账号 + this.userName = user.userName; + // 用户头像 + this.avatar = user.avatar; + // 基础信息 + this.nickName = user.nickName; + this.phonenumber = user.phonenumber; + this.email = user.email; + this.sex = user.sex; + + // 验证返回的roles是否是一个非空数组 + if (Array.isArray(roles) && roles.length > 0) { + this.roles = roles; + this.permissions = permissions; + } else { + this.roles = ['ROLE_DEFAULT']; + this.permissions = []; + } + + // 水印文字信息=用户昵称 手机号 + let waterMarkContent = this.nickName; + if (this.phonenumber) { + waterMarkContent = `${this.nickName} ${this.phonenumber}`; + } + useLayoutStore().changeWaterMark(waterMarkContent); + } + // 网络错误时退出登录状态 + if (res.code === 500) { + removeToken(); + window.location.reload(); + } + return res; + }, + // 退出系统 + async fnLogOut() { + try { + await logout(); + } catch (error) { + throw error; + } finally { + this.token = ''; + this.roles = []; + this.permissions = []; + removeToken(); + } + }, + }, +}); + +export default useUserStore; diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts new file mode 100644 index 00000000..9c949291 --- /dev/null +++ b/src/typings/components.d.ts @@ -0,0 +1,54 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + AAvatar: typeof import('ant-design-vue/lib')['Avatar'] + ABadge: typeof import('ant-design-vue/lib')['Badge'] + AButton: typeof import('ant-design-vue/lib')['Button'] + ACol: typeof import('ant-design-vue/lib')['Col'] + ADropdown: typeof import('ant-design-vue/lib')['Dropdown'] + AForm: typeof import('ant-design-vue/lib')['Form'] + AFormItem: typeof import('ant-design-vue/lib')['FormItem'] + AImage: typeof import('ant-design-vue/lib')['Image'] + AInput: typeof import('ant-design-vue/lib')['Input'] + AInputPassword: typeof import('ant-design-vue/lib')['InputPassword'] + AMenu: typeof import('ant-design-vue/lib')['Menu'] + AMenuDivider: typeof import('ant-design-vue/lib')['MenuDivider'] + AMenuItem: typeof import('ant-design-vue/lib')['MenuItem'] + APopover: typeof import('ant-design-vue/lib')['Popover'] + ARow: typeof import('ant-design-vue/lib')['Row'] + ASpace: typeof import('ant-design-vue/lib')['Space'] + ATabPane: typeof import('ant-design-vue/lib')['TabPane'] + ATabs: typeof import('ant-design-vue/lib')['Tabs'] + ATooltip: typeof import('ant-design-vue/lib')['Tooltip'] + BellOutlined: typeof import('@ant-design/icons-vue')['BellOutlined'] + CronModal: typeof import('./../components/CronModal/index.vue')['default'] + Day: typeof import('./../components/CronModal/components/Day.vue')['default'] + DictTag: typeof import('./../components/DictTag/index.vue')['default'] + DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined'] + GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined'] + Hour: typeof import('./../components/CronModal/components/Hour.vue')['default'] + IconFont: typeof import('./../components/IconFont/index.vue')['default'] + LinkiFrame: typeof import('./../components/LinkiFrame/index.vue')['default'] + LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined'] + LogoutOutlined: typeof import('@ant-design/icons-vue')['LogoutOutlined'] + Minute: typeof import('./../components/CronModal/components/Minute.vue')['default'] + MobileOutlined: typeof import('@ant-design/icons-vue')['MobileOutlined'] + Month: typeof import('./../components/CronModal/components/Month.vue')['default'] + QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined'] + QuestionCircleOutlined: typeof import('@ant-design/icons-vue')['QuestionCircleOutlined'] + ReloadOutlined: typeof import('@ant-design/icons-vue')['ReloadOutlined'] + RobotOutlined: typeof import('@ant-design/icons-vue')['RobotOutlined'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + Second: typeof import('./../components/CronModal/components/Second.vue')['default'] + SettingOutlined: typeof import('@ant-design/icons-vue')['SettingOutlined'] + UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined'] + WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined'] + } +} diff --git a/src/typings/dict.d.ts b/src/typings/dict.d.ts new file mode 100644 index 00000000..38e52f7d --- /dev/null +++ b/src/typings/dict.d.ts @@ -0,0 +1,7 @@ +/**字段类型 */ +type DictType = { + label: string; + value: string; + elTagType: string; + elTagClass: string; +}; diff --git a/src/typings/router.d.ts b/src/typings/router.d.ts new file mode 100644 index 00000000..69983d0d --- /dev/null +++ b/src/typings/router.d.ts @@ -0,0 +1,13 @@ +import 'vue-router'; +import { MetaRecord, MenuDataItem } from '@ant-design-vue/pro-layout'; + +declare module 'vue-router' { + interface RouteMeta extends MetaRecord { + /**请求授权 */ + requiresAuth?: boolean; + /**权限 */ + permissions?: string[]; + /**角色 */ + roles?: string[]; + } +} diff --git a/src/typings/vite-env.d.ts b/src/typings/vite-env.d.ts new file mode 100644 index 00000000..a735ff23 --- /dev/null +++ b/src/typings/vite-env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.vue' { + import { DefineComponent } from 'vue'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/src/utils/cache-local-utils.ts b/src/utils/cache-local-utils.ts new file mode 100644 index 00000000..f3d51906 --- /dev/null +++ b/src/utils/cache-local-utils.ts @@ -0,0 +1,40 @@ +/**长期级缓存设置 */ +export function localSet(key: string, value: string) { + if (!localStorage || key == null || value == null) { + return; + } + localStorage.setItem(key, value); +} + +/**长期级缓存获取 */ +export function localGet(key: string) { + if (!localStorage || key == null) { + return null; + } + return localStorage.getItem(key); +} + +/**长期级缓存移除 */ +export function localRemove(key: string) { + if (!localStorage || key == null) { + return null; + } + return localStorage.removeItem(key); +} + +/**长期级缓存设置JSON */ +export function localSetJSON(key: string, jsonValue: object) { + if (key == null || jsonValue == null) { + return null; + } + localSet(key, JSON.stringify(jsonValue)); +} + +/**长期级缓存获取JSON */ +export function localGetJSON(key: string) { + const value = localGet(key); + if (value == null) { + return null; + } + return JSON.parse(value); +} diff --git a/src/utils/cache-session-utils.ts b/src/utils/cache-session-utils.ts new file mode 100644 index 00000000..f3fe5022 --- /dev/null +++ b/src/utils/cache-session-utils.ts @@ -0,0 +1,40 @@ +/**会话级缓存设置 */ +export function sessionSet(key: string, value: string) { + if (!sessionStorage || key == null || value == null) { + return; + } + sessionStorage.setItem(key, value); +} + +/**会话级缓存获取 */ +export function sessionGet(key: string) { + if (!sessionStorage || key == null) { + return null; + } + return sessionStorage.getItem(key); +} + +/**会话级缓存移除 */ +export function sessionRemove(key: string) { + if (!sessionStorage || key == null) { + return null; + } + return sessionStorage.removeItem(key); +} + +/**会话级缓存设置JSON */ +export function sessionSetJSON(key: string, jsonValue: object) { + if (key == null || jsonValue == null) { + return null; + } + sessionSet(key, JSON.stringify(jsonValue)); +} + +/**会话级缓存获取JSON */ +export function sessionGetJSON(key: string) { + const value = sessionGet(key); + if (value == null) { + return null; + } + return JSON.parse(value); +} diff --git a/src/utils/date-utils.ts b/src/utils/date-utils.ts new file mode 100644 index 00000000..166669e2 --- /dev/null +++ b/src/utils/date-utils.ts @@ -0,0 +1,72 @@ +// 依赖来源 https://github.com/iamkun/dayjs +import dayjs from 'dayjs'; + +// 导入本地化语言并设为默认使用 +import('dayjs/locale/zh-cn'); +dayjs.locale('zh-cn'); + +/**年 列如:2022 */ +export const YYYY = 'YYYY'; + +/**年-月 列如:2022-12 */ +export const YYYY_MM = 'YYYY-MM'; + +/**年-月-日 列如:2022-12-30 */ +export const YYYY_MM_DD = 'YYYY-MM-DD'; + +/**年月日时分秒 列如:20221230010159 */ +export const YYYYMMDDHHMMSS = 'YYYYMMDDHHmmss'; + +/**年-月-日 时:分:秒 列如:2022-12-30 01:01:59 */ +export const YYYY_MM_DD_HH_MM_SS = 'YYYY-MM-DD HH:mm:ss'; + +/** + * 格式时间字符串 + * @param dateStr 时间字符串 + * @param formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss + * @returns Date对象 + */ +export function parseStrToDate( + dateStr: string, + formatStr: string = YYYY_MM_DD_HH_MM_SS +): Date { + return dayjs(dateStr, formatStr).toDate(); +} + +/** + * 格式时间 + * @param date 可转的Date对象 + * @param formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss + * @returns 时间格式字符串 + */ +export function parseDateToStr( + date: string | number | Date, + formatStr: string = YYYY_MM_DD_HH_MM_SS +): string { + return dayjs(date).format(formatStr); +} + +/** + * 格式时间成日期路径 + * + * 年/月 列如:2022/12 + * @returns 时间格式字符串 YYYY/MM + */ +export function parseDatePath(date: number | Date = Date.now()): string { + return dayjs(date).format('YYYY/MM'); +} + +/** + * 判断两次时间差 + * @param endDate 结束时间 + * @param startDate 开始时间 + * @returns 单位秒 + */ +export function diffSeconds( + endDate: number | Date, + startDate: number | Date +): number { + const value = Math.ceil(dayjs(endDate).diff(startDate, 'seconds')); + if (Number.isNaN(value)) return 0; + return value; +} diff --git a/src/utils/parse-tree-utils.ts b/src/utils/parse-tree-utils.ts new file mode 100644 index 00000000..50866442 --- /dev/null +++ b/src/utils/parse-tree-utils.ts @@ -0,0 +1,188 @@ +/** + * 解析数据层级转树结构 + * + * @param data 数组数据 + * @param fieldId 读取节点字段 默认 'id' + * @param fieldParentId 读取节点父节点字段 默认 'parentId' + * @param fieldChildren 设置子节点字段 默认 'children' + * @returns 层级数组 + */ +export function parseDataToTree( + data: Record[], + fieldId: string = 'id', + fieldParentId: string = 'parentId', + fieldChildren: string = 'children' +) { + // 节点分组 + let map: Map[]> = new Map(); + // 节点id + let treeIds: string[] = []; + // 树节点 + let tree: Record[] = []; + + for (const item of data) { + let parentId = item[fieldParentId]; + // 分组 + let mapItem = map.get(parentId) ?? []; + mapItem.push(item); + map.set(parentId, mapItem); + // 记录节点id + treeIds.push(item[fieldId]); + } + + for (const [key, value] of map) { + // 选择不是节点id的作为树节点 + if (!treeIds.includes(key)) { + tree.push(...value); + } + } + + for (const iterator of tree) { + componet(iterator); + } + + /**闭包递归函数 */ + function componet(iterator: Record) { + let id = iterator[fieldId]; + let item = map.get(id); + if (item) { + iterator[fieldChildren] = item; + } + if (iterator[fieldChildren]) { + for (let i of iterator[fieldChildren]) { + componet(i); + } + } + } + return tree; +} + +/** + * 解析数据层级转树结构-排除节点 + * + * @param data 数组数据 + * @param excludeField 排除节点字段 默认 'type' + * @param excludeValue 排除节点值 默认 '0' + * @param fieldId 读取节点字段 默认 'id' + * @param fieldParentId 读取节点父节点字段 默认 'parentId' + * @param fieldChildren 设置子节点字段 默认 'children' + * @returns 层级数组 + */ +export function parseDataToTreeExclude( + data: Record[], + excludeField = 'type', + excludeValue = '0', + fieldId: string = 'id', + fieldParentId: string = 'parentId', + fieldChildren: string = 'children' +) { + // 节点分组 + let map: Map[]> = new Map(); + // 节点id + let treeIds: string[] = []; + // 树节点 + let tree: Record[] = []; + + for (const item of data) { + // 排除值跳过 + let exclude = item[excludeField]; + if (exclude && exclude === excludeValue) { + continue; + } + let parentId = item[fieldParentId]; + // 分组 + let mapItem = map.get(parentId) ?? []; + mapItem.push(item); + map.set(parentId, mapItem); + // 记录节点id + treeIds.push(item[fieldId]); + } + + for (const [key, value] of map) { + // 选择不是节点id的作为树节点 + if (!treeIds.includes(key)) { + tree.push(...value); + } + } + + for (const iterator of tree) { + componet(iterator); + } + + /**闭包递归函数 */ + function componet(iterator: Record) { + let id = iterator[fieldId]; + let item = map.get(id); + if (item) { + iterator[fieldChildren] = item; + } + if (iterator[fieldChildren]) { + for (let i of iterator[fieldChildren]) { + componet(i); + } + } + } + return tree; +} + +/** + * 解析树结构数据转出一维id数组 + * + * @param data 数组数据 + * @param fieldId 读取节点字段 默认 'id' + * @param fieldChildren 读取子节点字段 默认 'children' + * @returns 层级数组 + */ +export function parseTreeKeys( + data: Record[], + fieldId: string = 'id', + fieldChildren: string = 'children' +) { + // 节点id + let treeIds: string[] | number[] = []; + componet(data); + /**闭包递归函数 */ + function componet(data: Record[]) { + if (data.length <= 0) return; + for (const iterator of data) { + let id = iterator[fieldId]; + if (id) { + treeIds.push(id as never); + } + if (Array.isArray(iterator[fieldChildren])) { + componet(iterator[fieldChildren]); + } + } + } + return treeIds; +} + +/** + * 解析树结构数据转出含子节点的一维id数组 + * + * @param data 数组数据 + * @param fieldId 读取节点字段 默认 'id' + * @param fieldChildren 读取子节点字段 默认 'children' + * @returns 层级数组 + */ +export function parseTreeNodeKeys( + data: Record[], + fieldId: string = 'id', + fieldChildren: string = 'children' +) { + // 节点id + let treeIds: string[] | number[] = []; + componet(data); + /**闭包递归函数 */ + function componet(data: Record[]) { + if (data.length <= 0) return; + for (const iterator of data) { + let nodes = iterator[fieldChildren]; + if (Array.isArray(nodes) && nodes.length > 0) { + treeIds.push(iterator[fieldId] as never); + componet(iterator[fieldChildren]); + } + } + } + return treeIds; +} diff --git a/src/utils/regular-utils.ts b/src/utils/regular-utils.ts new file mode 100644 index 00000000..a7463473 --- /dev/null +++ b/src/utils/regular-utils.ts @@ -0,0 +1,67 @@ +/** + * 有效账号格式 + * + * 账号不能以数字开头,可包含大写小写字母,数字,且不少于5位 + */ +export const regExpUserName = /^[a-zA-Z][a-z0-9A-Z]{5,}$/; + +/** + * 有效密码格式 + * + * 密码至少包含大小写字母、数字、特殊符号,且不少于6位 + */ +export const regExpPasswd = + /^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$/; + +/** + * 有效手机号格式 + */ +export const regExpMobile = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/; + +/** + * 有效邮箱格式 + */ +export const regExpEmail = + /^(([^<>()\\.,;:\s@"]+(\.[^<>()\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/; + +/** + * 有效用户昵称格式 + * + * 用户昵称只能包含字母、数字、中文和下划线,且不少于2位 + */ +export const regExpNick = /^[\w\u4e00-\u9fa5-]{2,}$/; + +/** + * 是否为http(s)://开头 + */ +export const regExpHttp = /^http(s)?:\/\/+/; + +/** + * 判断是否为http(s)://开头 + * @param link 网络链接 + * @returns true | false + */ +export function validHttp(link: string): boolean { + if (!link) return false; + return regExpHttp.test(link); +} + +/** + * 判断是否为有效手机号格式 + * @param mobile 手机号字符串 + * @returns true | false + */ +export function validMobile(mobile: string): boolean { + if (!mobile) return false; + return regExpMobile.test(mobile); +} + +/** + * 判断是否为有效邮箱格式 + * @param email 邮箱字符串 + * @returns true | false + */ +export function validEmail(email: string): boolean { + if (!email) return false; + return regExpEmail.test(email); +} diff --git a/src/views/account/components/base-info.vue b/src/views/account/components/base-info.vue new file mode 100644 index 00000000..5a282759 --- /dev/null +++ b/src/views/account/components/base-info.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/src/views/account/components/reset-passwd.vue b/src/views/account/components/reset-passwd.vue new file mode 100644 index 00000000..a4657735 --- /dev/null +++ b/src/views/account/components/reset-passwd.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/src/views/account/components/style-layout.vue b/src/views/account/components/style-layout.vue new file mode 100644 index 00000000..c159050d --- /dev/null +++ b/src/views/account/components/style-layout.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/src/views/account/profile.vue b/src/views/account/profile.vue new file mode 100644 index 00000000..cc2696ce --- /dev/null +++ b/src/views/account/profile.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/src/views/account/settings.vue b/src/views/account/settings.vue new file mode 100644 index 00000000..6b12f7c2 --- /dev/null +++ b/src/views/account/settings.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/views/dome/dome1.vue b/src/views/dome/dome1.vue new file mode 100644 index 00000000..09380429 --- /dev/null +++ b/src/views/dome/dome1.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/views/dome/dome2.vue b/src/views/dome/dome2.vue new file mode 100644 index 00000000..f3fdd5c9 --- /dev/null +++ b/src/views/dome/dome2.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/views/dome/dome3.vue b/src/views/dome/dome3.vue new file mode 100644 index 00000000..3174c70d --- /dev/null +++ b/src/views/dome/dome3.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/views/domes/dynamic-match.vue b/src/views/domes/dynamic-match.vue new file mode 100644 index 00000000..5249bdca --- /dev/null +++ b/src/views/domes/dynamic-match.vue @@ -0,0 +1,87 @@ + + + diff --git a/src/views/domes/page-info.vue b/src/views/domes/page-info.vue new file mode 100644 index 00000000..82119502 --- /dev/null +++ b/src/views/domes/page-info.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/views/domes/page-typography.vue b/src/views/domes/page-typography.vue new file mode 100644 index 00000000..89f895aa --- /dev/null +++ b/src/views/domes/page-typography.vue @@ -0,0 +1,102 @@ + + diff --git a/src/views/error/403.vue b/src/views/error/403.vue new file mode 100644 index 00000000..13c215c4 --- /dev/null +++ b/src/views/error/403.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/src/views/error/404.vue b/src/views/error/404.vue new file mode 100644 index 00000000..14b1e60e --- /dev/null +++ b/src/views/error/404.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/views/index.vue b/src/views/index.vue new file mode 100644 index 00000000..a2ed7b40 --- /dev/null +++ b/src/views/index.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/views/login.vue b/src/views/login.vue new file mode 100644 index 00000000..307f8921 --- /dev/null +++ b/src/views/login.vue @@ -0,0 +1,469 @@ + + + + + diff --git a/src/views/monitor/cache/index.vue b/src/views/monitor/cache/index.vue new file mode 100644 index 00000000..4421250d --- /dev/null +++ b/src/views/monitor/cache/index.vue @@ -0,0 +1,498 @@ + + + + + diff --git a/src/views/monitor/cache/info.vue b/src/views/monitor/cache/info.vue new file mode 100644 index 00000000..18c78a34 --- /dev/null +++ b/src/views/monitor/cache/info.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/src/views/monitor/job/index.vue b/src/views/monitor/job/index.vue new file mode 100644 index 00000000..aa4c5094 --- /dev/null +++ b/src/views/monitor/job/index.vue @@ -0,0 +1,1087 @@ + + + + + diff --git a/src/views/monitor/job/log.vue b/src/views/monitor/job/log.vue new file mode 100644 index 00000000..15481346 --- /dev/null +++ b/src/views/monitor/job/log.vue @@ -0,0 +1,682 @@ + + + + + diff --git a/src/views/monitor/logininfor/index.vue b/src/views/monitor/logininfor/index.vue new file mode 100644 index 00000000..69b0a303 --- /dev/null +++ b/src/views/monitor/logininfor/index.vue @@ -0,0 +1,546 @@ + + + + + diff --git a/src/views/monitor/online/index.vue b/src/views/monitor/online/index.vue new file mode 100644 index 00000000..e151ff27 --- /dev/null +++ b/src/views/monitor/online/index.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/src/views/monitor/operlog/index.vue b/src/views/monitor/operlog/index.vue new file mode 100644 index 00000000..faf3e0e0 --- /dev/null +++ b/src/views/monitor/operlog/index.vue @@ -0,0 +1,692 @@ + + + + + diff --git a/src/views/monitor/server/info.vue b/src/views/monitor/server/info.vue new file mode 100644 index 00000000..16aa9721 --- /dev/null +++ b/src/views/monitor/server/info.vue @@ -0,0 +1,329 @@ + + + + + diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue new file mode 100644 index 00000000..0eaf77c1 --- /dev/null +++ b/src/views/redirect/index.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/views/register.vue b/src/views/register.vue new file mode 100644 index 00000000..fb66e29e --- /dev/null +++ b/src/views/register.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue new file mode 100644 index 00000000..5b72c333 --- /dev/null +++ b/src/views/system/config/index.vue @@ -0,0 +1,809 @@ + + + + + diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue new file mode 100644 index 00000000..da6fa754 --- /dev/null +++ b/src/views/system/dept/index.vue @@ -0,0 +1,800 @@ + + + + + diff --git a/src/views/system/dict/data.vue b/src/views/system/dict/data.vue new file mode 100644 index 00000000..4085c4e7 --- /dev/null +++ b/src/views/system/dict/data.vue @@ -0,0 +1,883 @@ + + + + + diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue new file mode 100644 index 00000000..a895ba30 --- /dev/null +++ b/src/views/system/dict/index.vue @@ -0,0 +1,809 @@ + + + + + diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue new file mode 100644 index 00000000..7d29d392 --- /dev/null +++ b/src/views/system/menu/index.vue @@ -0,0 +1,1103 @@ + + + + + diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue new file mode 100644 index 00000000..da1372ee --- /dev/null +++ b/src/views/system/notice/index.vue @@ -0,0 +1,734 @@ + + + + + diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue new file mode 100644 index 00000000..92e93a35 --- /dev/null +++ b/src/views/system/post/index.vue @@ -0,0 +1,759 @@ + + + + + diff --git a/src/views/system/role/auth-user.vue b/src/views/system/role/auth-user.vue new file mode 100644 index 00000000..722872f0 --- /dev/null +++ b/src/views/system/role/auth-user.vue @@ -0,0 +1,508 @@ + + + + + diff --git a/src/views/system/role/components/auth-user-select.vue b/src/views/system/role/components/auth-user-select.vue new file mode 100644 index 00000000..19746b36 --- /dev/null +++ b/src/views/system/role/components/auth-user-select.vue @@ -0,0 +1,301 @@ + + + + + diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue new file mode 100644 index 00000000..25a1e796 --- /dev/null +++ b/src/views/system/role/index.vue @@ -0,0 +1,1281 @@ + + + + + diff --git a/src/views/system/user/components/UploadXlsxImport.vue b/src/views/system/user/components/UploadXlsxImport.vue new file mode 100644 index 00000000..66164491 --- /dev/null +++ b/src/views/system/user/components/UploadXlsxImport.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue new file mode 100644 index 00000000..9d58f15a --- /dev/null +++ b/src/views/system/user/index.vue @@ -0,0 +1,1433 @@ + + + + + diff --git a/src/views/tool/build/index.vue b/src/views/tool/build/index.vue new file mode 100644 index 00000000..a66e68de --- /dev/null +++ b/src/views/tool/build/index.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/views/tool/swagger/index.vue b/src/views/tool/swagger/index.vue new file mode 100644 index 00000000..c7703370 --- /dev/null +++ b/src/views/tool/swagger/index.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/src/views/tool/upload/index.vue b/src/views/tool/upload/index.vue new file mode 100644 index 00000000..edd1ed9c --- /dev/null +++ b/src/views/tool/upload/index.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..11702116 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true, + "noEmit": true, + "typeRoots": ["src/typings", "./node_modules/@types"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "exclude": ["dist", "node_modules", "test", "script"], + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..9d31e2ae --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..ac6c2a93 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,76 @@ +import vue from '@vitejs/plugin-vue'; +import { defineConfig, loadEnv } from 'vite'; +import Components from 'unplugin-vue-components/vite'; +import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; +import Compression from 'vite-plugin-compression'; +import path from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + // 读取环境配置变量,指定前缀 + const env = loadEnv(mode, process.cwd(), 'VITE_'); + return { + // 访问基础路径 + base: env.VITE_HISTORY_BASE_URL, + // 本地开发服务配置 + server: { + port: 6269, // 端口 + host: true, // 暴露到网络地址 + open: false, // 完成后自动跳转浏览器打开 + proxy: { + // https://cn.vitejs.dev/config/#server-proxy + [env.VITE_API_BASE_URL]: { + target: 'http://192.168.56.1:6275', + changeOrigin: true, + rewrite: p => p.replace(/^\/dev-api/, ''), + }, + }, + }, + resolve: { + // 资源别名 + alias: { + '@': path.resolve(__dirname, './src'), + }, + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], + }, + css: { + preprocessorOptions: { + less: { + // DO NOT REMOVE THIS LINE + javascriptEnabled: true, + modifyVars: { + // hack: `true; @import 'ant-design-vue/dist/antd.variable.less'`, + // '@primary-color': '#eb2f96', // 全局主色 + }, + }, + }, + }, + optimizeDeps: { + include: ['@ant-design/icons-vue', 'ant-design-vue'], + }, + plugins: [ + vue(), + // Vue文件中自动导入组件,dirs目录和antd库 + Components({ + dts: 'src/typings/components.d.ts', + deep: true, + dirs: ['src/components'], + extensions: ['vue', 'tsx'], + resolvers: [ + AntDesignVueResolver({ + importStyle: false, + resolveIcons: true, + cjs: true, // 避免es模块打包缺失 + }), + ], + }), + // gzip静态压缩文件 + Compression({ + verbose: false, + algorithm: 'gzip', + ext: '.gz', + disable: false, // 是否禁用 + }), + ], + }; +});