14 Commits

Author SHA1 Message Date
lyt
d2d83fd70b 'admin-22.12.09:发布v2.4.2版本,具体更新内容查看CHANGELOG.md' 2022-12-09 23:55:16 +08:00
56139e72a7 !41 修复get请求传递嵌套对象或数组时无法正常编码问题
Merge pull request !41 from 随心/master
2022-12-07 10:51:38 +00:00
07e0f742d8 !40 开启TagsView缓存后,刷新后所有的路由都变成组件缓存了
Merge pull request !40 from mrjimin/master
2022-12-07 07:19:27 +00:00
c09b154a3f 修复get请求传递嵌套对象或数组时无法正常编码问题 2022-12-05 10:48:16 +08:00
6e59014357 update src/layout/routerView/parent.vue.
Signed-off-by: mrjimin <z8888788@163.com>
2022-12-03 08:22:44 +00:00
02e7c49750 update src/layout/routerView/parent.vue.
这里应该拿到的是已经设置开启组件缓存的路由,而不是全部,需要先判断item.meta.isKeepAlive

Signed-off-by: mrjimin <z8888788@163.com>
2022-12-03 07:31:01 +00:00
lyt
7b26cb21dd 'admin-22.11.30:发布v2.4.1版本,具体更新内容查看CHANGELOG.md' 2022-11-30 23:04:40 +08:00
lyt
bfecc6f6d2 Merge branch 'master' of https://gitee.com/lyt-top/vue-next-admin 2022-11-30 18:51:38 +08:00
lyt
14981044b9 'admin-22.11.30:删除v2.4.0版本不需要的依赖' 2022-11-30 18:51:14 +08:00
8ed7986a96 update src/views/error/404.vue.
Signed-off-by: lyt-Top <1105290566@qq.com>
2022-11-30 09:46:14 +00:00
852075ccfb update src/components/table/index.vue.
Signed-off-by: lyt-Top <1105290566@qq.com>
2022-11-30 09:39:44 +00:00
lyt
ba80b9bc76 'admin-22.11.30:修改v2.4.0文字说明' 2022-11-30 17:36:38 +08:00
lyt
4f8f13a722 'admin-22.11.30:修改v2.4.0文字说明' 2022-11-30 17:34:16 +08:00
lyt
1787f09bdc 'admin-22.11.29:发布v2.4.0版本,具体更新内容查看CHANGELOG.md' 2022-11-29 22:03:40 +08:00
340 changed files with 28796 additions and 12200 deletions

8
.env Normal file
View File

@ -0,0 +1,8 @@
# port 端口号
VITE_PORT = 8888
# open 运行 npm run dev 时自动打开浏览器
VITE_OPEN = false
# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
VITE_PUBLIC_PATH = /vue-next-admin-preview/

5
.env.development Normal file
View File

@ -0,0 +1,5 @@
# 本地环境
ENV = 'development'
# 本地环境接口地址
VITE_API_URL = 'http://localhost:8888/'

5
.env.production Normal file
View File

@ -0,0 +1,5 @@
# 线上环境
ENV = 'production'
# 线上环境接口地址
VITE_API_URL = 'https://lyt-top.gitee.io/vue-next-admin-preview/'

View File

@ -13,9 +13,18 @@ module.exports = {
},
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
rules: {
// http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
@ -26,6 +35,9 @@ module.exports = {
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2],
'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
@ -59,5 +71,6 @@ module.exports = {
'no-v-model-argument': 'off',
'no-case-declarations': 'off',
'no-console': 'error',
'no-redeclare': 'off',
},
};

32
.gitignore vendored
View File

@ -1,11 +1,23 @@
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
.DS_Store
node_modules
/dist
# Local History for Visual Studio Code
.history/
frontend/LICENSE
*.zip
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

425
CHANGELOG.md Normal file
View File

@ -0,0 +1,425 @@
# <a href="https://gitee.com/lyt-top/vue-next-admin" target="_blank">vue-next-admin 更新日志</a>
🎉🎉🔥 `vue-next-admin` 基于 vue3.x 、Typescript、vite、Element plus 等适配手机、平板、pc 的后台开源免费模板库vue2.x 请切换 vue-prev-admin 分支)
## 2.4.2
`2022.12.09`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 国际化自动导入文件功能,只需在 `/src/i18n/pages` 下新建文件夹定义即可
- 🎉 新增 `/make/tableDemo` 中 [搜索框展开,收缩功能,高级筛选组件 有计划做吗](https://gitee.com/lyt-top/vue-next-admin/issues/I6511L)
- 🐞 修复 [!40 开启 TagsView 缓存后,刷新后所有的路由都变成组件缓存了](https://gitee.com/lyt-top/vue-next-admin/pulls/40),感谢[@mrjimin](https://gitee.com/mrjimin)
- 🐞 修复 [!41 修复 get 请求传递嵌套对象或数组时无法正常编码问题](https://gitee.com/lyt-top/vue-next-admin/pulls/41),感谢[@随心](https://gitee.com/jiangqiang1996)
- 🐞 修复 组件 wangEditor 回显值的问题
- 🐞 修复 `/fun/echartsMap`(地理坐标/地图)、`visualizingDemo2`(数据可视化演示 2 演示报错问题
- 🎯 优化 版本升级提示
- 🎯 优化 无权限登录时增加提示信息,[BUG因前端加载路由(initFrontEndControlRoutes)中当前用户角色为一个陌生角色, 导致 router.beforeEach 会死循环 浏览器崩溃](https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO),感谢[@canroc](https://gitee.com/canroc)、[@随心](https://gitee.com/jiangqiang1996)
- 🌈 重构 `/views/system` 新增修改组件合并。[可以把新增修改组件合并成一个吧](https://gitee.com/lyt-top/vue-next-admin/issues/I64WES)
- 🌈 重构 图标选择器,[图标选择器没办法筛选,只能筛选 ali 的](https://gitee.com/lyt-top/vue-next-admin/issues/I64HZD),感谢[@随心](https://gitee.com/jiangqiang1996)
## 2.4.1
`2022.11.30`
- 🎉 新增 版本升级提示
- 🐞 修复 [先打开 F12 再登录进去,然后改变浏览器大小 js 报错](https://gitee.com/lyt-top/vue-next-admin/issues/I63ZZT),感谢[@Quber](https://gitee.com/quber)
## 2.4.0
`2022.11.29`
⚡⚡⚡ 此版为破坏性更新,应群友建议 `script lang="ts"``script lang="ts" setup 语法糖`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 表格封装演示,路径:`组件封装 -> 表格封装演示`
- 🎉 新增 master 分支 script lang="ts" 改成 script lang="ts" setup 语法糖,将同步基础分支
- 🐞 修复 [v2.3.0 版本报错问题处理](https://gitee.com/lyt-top/vue-next-admin/issues/I623RP)
- 🐞 修复 [el-backtop 滚动高度不触发(固定了 header](https://gitee.com/lyt-top/vue-next-admin/issues/I63N0D),感谢[@dejavuuuuu](https://gitee.com/zc19951010)
- 🎯 优化 完善 ts 类型,删除根目录 `plugins.d.ts、shim.d.ts、source.d.ts`,移入到 `/src/types/global.d.ts`
- 🎯 优化 代码 `watch` 移动到 `生命周期钩子` 最后,文字注释等
## 2.3.0
`2022.11.16`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 新版登录页
- 🎉 新增 tagsview 鼠标中键 `关闭当前 tagsview`
- 🎉 新增 `分栏菜单鼠标悬停预加载`。[分栏模式如何去掉鼠标悬浮父级菜单,分栏菜单自动加载的功能啊](https://gitee.com/lyt-top/vue-next-admin/issues/I5RUY7)。操作路径:`布局配置 -> 分栏设置`
- 🐞 修复 [vue-i18n](https://vue-i18n.intlify.dev/api/general.html#createi18n) 报错,[!39 修复 i18n 兼容性问题](https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/39/files),感谢[@随心](https://toscode.gitee.com/jiangqiang1996)
- 🐞 修复 顶栏搜索功能点击蒙蔽弹窗不关闭
- 🐞 修复 [!38 fix: bug refreshRouterViewKey 值为 null 导致路由缓存第一次无效](https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/38/files),感谢[@P)](https://toscode.gitee.com/foxp8y)
- 🐞 修复 `路由参数 -> 普通路由/动态路由` 国际化演示时,`tagsView``浏览器标题` 显示异常。[演示中:路由参数界面 -> 动态路由,国际化显示时面包屑、浏览器标题有 bug](https://gitee.com/lyt-top/vue-next-admin/issues/I5JRJG)
- 🐞 修复 `路由参数 -> 普通路由/动态路由` 动态设置 `tagsViewName` 时,`tagsView 右键菜单刷新` 功能失效也就是路由后面有参数时query、params。[普通或动态路由新建页面后点击 tagview 刷新无效](https://gitee.com/lyt-top/vue-next-admin/issues/I5K3YO),感谢[@dejavuuuuu](https://gitee.com/zc19951010)
- 🐞 修复 [表单el-form字体图标偏移问题](https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM)
- 🐞 修复 路由 `router.addRoute` 时,一直提示 `No match found for location with path 'xxx'`
- 🎯 优化 全局 `getCurrentInstance` 替换成 [`provide/inject`](https://cn.vuejs.org/api/application.html#app-provide) 或通过 `ref` 处理
- 🎯 优化 引入组件方式 `(import xxx from xxx)` 改成 `defineAsyncComponent(() => import(xxx))`
- 🎯 优化 页面高度 100% 问题,重写布局配置 `界面设置 -> 固定 Header` 多余的 `el-scrollbar` 逻辑、重写各界面需 `计算属性 computed` 设置动态高度问题(改为 css `flex` 设置自适应高度,具体查看文档:[设置可视区高度 100%](https://lyt-top.gitee.io/vue-next-admin-doc-preview/config/otherIssues/#%E8%AE%BE%E7%BD%AE%E5%8F%AF%E8%A7%86%E5%8C%BA%E9%AB%98%E5%BA%A6-100)。[!31 修复页面样式无法通过百分比设置的问题](https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/31),感谢[@LostDeer](https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/31/files)。`(改动较大,删除多余代码)`
- 🎯 优化 [wangeditor](https://www.wangeditor.com/) 组件,`@wangeditor/editor-for-vue`。可自行修改,组件位置:`/src/components/editor`。相关 Issues[wangeditor 编辑器多个菜单不能回弹](https://gitee.com/lyt-top/vue-next-admin/issues/I5M5H7)
- 🌈 重构 外链、内嵌 iframe 逻辑 + 美化iframe 支持缓存
## 2.2.0
`2022.07.10`
⚡⚡⚡ [/sec/stores/userInfo.ts](https://gitee.com/lyt-top/vue-next-admin/blob/master/src/stores/userInfo.ts) 下添加了 `getApiUserInfo` 接口模拟数据 `setTimeout` 为 3 秒
- 🌟 更新 依赖更新最新版本
- 🐞 修复 [主界面重新授权按钮点击卡死不跳转登录界面#I5C3JS](https://gitee.com/lyt-top/vue-next-admin/issues/I5C3JS),感谢[@Hero-Typ](https://gitee.com/tian_yu_peng)
- 🐞 修复 编译警告[#I5CVSB](https://gitee.com/lyt-top/vue-next-admin/issues/I5CVSB),全局替换成 `:deep(attr)`,感谢[@Linvas](https://gitee.com/linvas)。参考文档:[vue3 sfc-style](https://v3.cn.vuejs.org/api/sfc-style.html#style-scoped)。`node_modules\print-js\dist\print.js``print-js` 作者适配或去除 `package.json` 中的 `"print-js": "^1.6.0"`
- 🐞 修复 [vue-next-admin-template-js 版本前端控制路由userInfo.js 请求用户信息接口报错,加载不到路由 可以写个定时器模拟一下接口 一样的报错#I5F1HP](https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP),感谢[@白开水](https://gitee.com/libin951223)
## 2.1.1
`2022.05.27`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 深色模式下,`<el-button text></el-button>` 时,`:active` 样式
- 🎯 优化 [页面缓存在刷新之后失效 #I58U75](https://gitee.com/lyt-top/vue-next-admin/issues/I58U75)),感谢[@ls0428](https://gitee.com/ls0428)
- 🎯 优化 [SvgIcon 对下载的 Svg 图像设置颜色无效 #I59ND0](https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0)),感谢[@elus_z](https://gitee.com/elus_z)
- 🎯 优化 `/src/utils/toolsValidate.ts` 工具类
- 🐞 修复 [布局切换TagsView 显示的 tab 会多一个出来 #I58WGM](https://gitee.com/lyt-top/vue-next-admin/issues/I58WGM),感谢[@lg_boy](https://gitee.com/lg_boy)
- 🐞 修复 [如果设置顶部面包屑导航开启图标 isBreadcrumbIcon=true 后,样式有点问题 如果不开启就是正常的 #I58VB8](https://gitee.com/lyt-top/vue-next-admin/issues/I58VB8)
- 🐞 修复 地址栏路由地址输入错误时,返回首页后,再次输入路由地址错误时,不跳转 404 问题
- 🐞 修复 [2.1.0 版本的图标选择组件多次点击后功能失效 #I590TH](https://gitee.com/lyt-top/vue-next-admin/issues/I590TH),感谢[@quber](https://gitee.com/quber)
## 2.1.0
`2022.04.18`
⚡⚡⚡ 此版本为破环性更新,优化内容如下:(谨慎更新!谨慎更新!!谨慎更新!!!)。因为 `vuex` 替换成 `pinia`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 部分界面图片不显示问题(更换 gitee 在线图片地址源)
- 🎯 优化 各界面方法引入与逻辑之间添加一行空行,方便区分内容
- 🎯 优化 图标选择器 [#I4YAHB](https://gitee.com/lyt-top/vue-next-admin/issues/I4YAHB),感谢[@真有你的](https://gitee.com/sunliusen)
- 🎯 优化 图标选择器 icon type 类型为 all 时,类型 ali、ele、awe 回显问题
- 🎯 优化 去掉开发环境 i18n 控制台警告,页面代码:[i18n/index.ts](https://gitee.com/lyt-top/vue-next-admin/blob/master/src/i18n/index.ts)
- 🎯 优化 `NextLoading.start()` 方法,防止第一次进入界面时出现短暂空白
- 🎯 优化 地址栏有参数退出登录,再次登录不跳之前界面问题 `src/layout/navBars/breadcrumb/user.vue`
- 🎯 优化 `SvgIcon` 组件,防止 `开启 Tagsview 图标` 时,`tagsView 右键菜单关闭` 报错问题,工作流不可连线、全屏时关闭按钮消失问题
- 🎯 优化 [如果 url 中有中文等特殊字符,第一次切换该 tab 时 keep-alive 失效#I55JS7](https://gitee.com/lyt-top/vue-next-admin/issues/I55JS7),感谢[yuyong1566](https://gitee.com/yuyong1566)
- 🎯 优化 [wangEditor](https://www.wangeditor.com/) 更新到 v5[vue3 版本线上示例中 wangeditor 富文本编辑器 demo 实例,无法换行#I5565B](https://gitee.com/lyt-top/vue-next-admin/issues/I5565B),感谢@[jenchih](https://gitee.com/jenchih)
- 🎯 优化 [在关闭 tagview 时,高度刷新时会会变化,出现滚动条](https://gitee.com/lyt-top/vue-next-admin/issues/I55FHM),感谢[张松](https://gitee.com/zs310071113)
- 🎯 优化 [路由参数](https://lyt-top.gitee.io/vue-next-admin-preview/#/params/common)演示
- 🎉 新增 [vuex](https://vuex.vuejs.org/) 替换成 [pinia](https://pinia.vuejs.org/getting-started.html)
- 🎉 新增 tagsView 支持自定义 tagsView 名称(文章详情时有用),前往体验:[路由参数/普通路由](https://lyt-top.gitee.io/vue-next-admin-preview/#/params/common)。新增 tagsView 支持自定义名称国际化,感谢[@q7but](https://gitee.com/q7but)、[!22 add 添加自定义 tagVIewName 拓展,支持国际化](https://gitee.com/lyt-top/vue-next-admin/pulls/22/files)、感谢[@tony_tong_xin](https://gitee.com/tony_tong_xin)
- 🐞 修复 适配 `"element-plus": "^2.1.9"2.2.0` 版本
- 🐞 修复 [导航栏横向布局后,一级菜单显示问题#I4Z3M3](https://gitee.com/lyt-top/vue-next-admin/issues/I4Z3M3)
- 🐞 修复 横向布局三级及以上导航菜单高亮、导航高度不统一问题
- 🐞 修复 分栏模式下,选中的菜单是 primary 样式,鼠标移入字也变成 primary 色了,感谢群友@孤夜-流殇
- 🐞 修复 [vuex 里面改了颜色 但是不生效 #I4WFMA](https://gitee.com/lyt-top/vue-next-admin/issues/I4WFMA)
- 🐞 修复 全局主题 primary 清空颜色后报错,[#I4X0LG](https://gitee.com/lyt-top/vue-next-admin/issues/I4X0LG),感谢[面向 BUG 编程](https://gitee.com/fhtfy)
- 🐞 修复 [.eslintrc.js 文件 rules 标签名错误 #I53IPK](https://gitee.com/lyt-top/vue-next-admin/issues/I53IPK),感谢[yuyong1566](https://gitee.com/yuyong1566)
- 🐞 修复 `开启 Tagsview 图标` 时,`tagsView 右键菜单关闭` 报错问题
- 🐞 修复 `router.push` 路径找不到时报错问题,`404、401 界面` 已移入到 `main` 主布局里(之前全屏)
- 🐞 修复 [全局修改组件大小失效了](https://gitee.com/lyt-top/vue-next-admin/issues/I551RP),感谢[lg_boy](https://gitee.com/lg_boy)
- 🐞 修复 [修改一下配置时,需要每次都清理 `window.localStorage` 浏览器永久缓存,配置才会生效,问题解决#I567R1](https://gitee.com/lyt-top/vue-next-admin/issues/I567R1),感谢[@lanbao123](https://gitee.com/lanbao123)
- 🐞 修复 [标记为需要缓存的 tab 页后,再次从左侧菜单打开,还是显示被缓存的页面内容#I4UY3G](https://gitee.com/lyt-top/vue-next-admin/issues/I4UY3G),感谢@axcc1234、特别感谢群友@华仔
- 🌈 重构 路由(`/src/router/index.ts`)解决 No match found for location with path "xxx"(前端控制,后端控制未解决) 问题
## 2.0.2
`2022.03.04`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 Alert 提示添加边框
- 🎯 优化 功能 / 数字滚动 演示界面
- 🐞 修复 全局主题按钮颜色 :active 问题
- 🐞 修复 Dropdown 下拉菜单样式问题
- 🐞 修复 SvgIcon 图标组件动态切换时报警告问题,[SvgIcon 改变 name 时可能导致图像不显示](https://gitee.com/lyt-top/vue-next-admin/issues/I4VGE0),感谢@axcc1234
## 2.0.1
`2022.02.25`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 svgIcon 图标组件
- 🎯 优化 vite.config.ts 打包,感谢群友@YourObjec
- 🐞 修复 tagViews 开启图标不显示问题(风格 5感谢群友@坏人
- 🐞 修复 [Element Plus 1.2.0-beta.6 以后的版本 el-table 在移动端无法左右滑动](https://gitee.com/lyt-top/vue-next-admin/issues/I4UPTP),感谢@YGDada
## 2.0.0
`2022.02.21`
⚡⚡⚡ 此版本为破环性更新,优化内容如下:(谨慎更新!谨慎更新!!谨慎更新!!!)。演示界面建议直接覆盖文件。如需使用之前版本,请前往[gitee 发行版](https://gitee.com/lyt-top/vue-next-admin/releases) 进行对应版本下载。基础版会基于 `master` 分支进行修改
- 🌟 更新 依赖更新最新版本
- 🌟 更新 登录页、首页
- 💔 移除 vue-web-screen-shot
- 💔 移除 城市多级联动,完整 json 数据请去 [vue-next-admin-images/menu](https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu) 仓库查看
- 💔 移除 功能/echartsTree 树图
- 💔 移除 其它设置/Tagsview 风格 2、Tagsview 风格 3
- 💔 移除 功能/验证器
- 🚧 调整 src/api 编写方式
- 🚧 调整 自定义封装公用组件演示,更好的维护
- 🎉 新增 Volar 支持vs code 配置参考 [Vue Language Features (Volar)](https://lyt-top.gitee.io/vue-next-admin-doc-preview/home/vscode/)
- 🎉 新增 `SvgIcon` 支持本地 svg 图标使用
- 🎉 新增 表单表格验证演示
- 🎯 优化 全局主题(移除 success、info、warning、danger
- 🎯 优化 工作流(开源)
- 🎯 优化 element plus svg 图标,`elementXXX` 改成 `ele-XXX`
- 🌈 重构 深色模式
- 🌹 合并 [处理 parent 的 h100 由于外层有 min-height 导致失效的问题](https://gitee.com/lyt-top/vue-next-admin/pulls/20),感谢@MaxNull@21030442-mao
- 🐞 修复 element plus 升级 `^1.3.0-beta.5` 后 组件 size 大小问题(大改:涉及布局、演示界面)
- 🐞 修复 vs code 使用 Vue Language Features (Volar) 插件 代码报红问题(可以把公用的 ts 类型定义封装起来公用)
## 1.2.2
`2021.12.21`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 iframes 滚动条问题
- 🎯 优化 部署后每次都要强制刷新清浏览器缓存问题
- 🎉 新增 工具类百分比验证演示
- 🐞 修复 [tag-view 标签右键会超出浏览器 #I4KN78](https://gitee.com/lyt-top/vue-next-admin/issues/I4KN78)
## 1.2.1
`2021.12.12`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 cropper 裁剪时卡顿问题 [#I4M2VQ](https://gitee.com/lyt-top/vue-next-admin/issues/I4M2VQ)
- 🎯 优化 Wangeditor 富文本编辑器的问题 [#I4LPC1](https://gitee.com/lyt-top/vue-next-admin/issues/I4LPC1)、[#I4LM7I](https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I)
- 🐞 修复 浏览器标题问题
- 🐞 修复 element plus svg 图标引入
- 🐞 修复 工作流不可以拖线连接问题
## 1.2.0
`2021.11.28`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 深色模式
- 🎯 优化 `/@/utils` 文件夹,合并删除单一内容
- 🎯 优化 系统设置:菜单管理(新增、修改)、角色管理(新增菜单权限)、用户管理、部门管理、字典管理
- 🎯 优化 登录界面逻辑、权限管理逻辑
- 🎯 优化 同步 [vue-next-admin-images](https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu) 后端控制菜单模拟数据
- 🎉 新增 适配 Font Icon 向 SVG Icon 迁移(改动大,"element-plus": "^1.2.0-beta.4" 谨慎更新)
- 🐞 修复 热更新问题,感谢@甜蜜蜜
- 🐞 修复 页面/element 字体图标演示
- 🐞 修复 功能/图标选择器演示,新增高级功能 [issues #I4GJXQ](https://gitee.com/lyt-top/vue-next-admin/issues/I4GJXQ)
## 1.1.2
`2021.10.17`
- 🌟 更新 依赖更新最新版本
- 🐞 修复 开启全屏时,刷新界面被还原成未全屏的状态
- 🎯 优化 tagsView 右键菜单关闭逻辑
- 🎯 优化 wangeditor 富文本编辑器(增加双向绑定)
- 🎉 新增 工作流(暂不开源)
- 🎉 新增 基础版 ts不带国际化切换 `vue-next-admin-template` 分支
## 1.1.1
`2021.09.25`
- 🌟 更新 依赖更新最新版本(`"element-plus": "^1.1.0-beta.13"` 版本运行错误,`^1.1.0-beta.16`修复横向菜单卡死问题)
- 🐞 修复 Dialog 弹窗位置错误、Drawer 抽屉内边距、el-menu 菜单收起时背景色问题
- 🎯 优化 锁屏界面自动锁屏(s/秒)必须设置至少 1 秒
- 🎉 新增 分栏布局,鼠标移入当前项时,显示当前项菜单内容
- 🎉 新增 工作流(未完成)
## 1.1.0
`2021.09.10`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 小屏模式下登录页二维码遮挡标题问题
- 🎉 新增 图片验证器
- 🎉 新增 动态复杂表单
- 🎉 新增 工作流(未完成)
- 🎉 新增 深色主题(伪深色,样式变动大,谨慎更新)
## 1.0.18
`2021.08.29`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 权限组件去掉顶级 div`/src/components/auth`
- 🎉 新增 布局配置添加恢复默认按钮
- 🐞 修复 升级 <a href="https://element-plus.gitee.io/#/zh-CN/component/changelog" target="_blank">element plus 1.1.0-beta.7</a>后项目无法启动、el-menu 菜单
- 🐞 修复 表格固定列时的层级、设置了相对定位时,遮挡左侧导航菜单问题
## 1.0.17
`2021.08.22`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 去除设置布局切换重置主题样式initSetLayoutChange切换布局需手动设置样式设置的样式自动同步各布局
- 🎯 优化 Dropdown 下拉菜单用户账号靠边时换行问题
- 🎯 优化 左侧导航菜单,共用菜单树,防止 `布局配置` 设置 `菜单 / 顶栏` 时,样式丢失等问题
- 🐞 修复 固定 header 后没有回到顶部的 bug拉取项目后运行不起来的 bug。<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/14" target="_blank">!14</a>,感谢<a href="https://gitee.com/wjs0509" target="_blank">@wjs0509</a>
- 🐞 修复 tagView 右键全屏后,浏览器窗口大小发生任何变化都会导致左边菜单显示出来,并且可点击打开对应页面。<a href="https://gitee.com/lyt-top/vue-next-admin/issues/I46E6T" target="_blank">I46E6T</a>
- 🐞 修复 默认设置 `菜单 / 顶栏` 样式不生效问题(/@/src/store/modules/themeConfig.ts
## 1.0.16
`2021.08.14`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 菜单高亮(详情且详情设置了 meta.isHide 时,顶级菜单高亮),感谢群友@YourObject
- 🎯 优化 详情路径写法:如父级(/pages/filtering那么详情为/pages/filtering/details?id=1。这样写可实现详情时父级菜单高亮否则写成/pages/filteringDetails?id=1顶级菜单将不会高亮。可参考`页面/过滤筛选组件`,点击当前图片进行测试
- 🎯 优化 tagsView 右键菜单全屏时,打开的界面高度问题
- 🎯 优化 图表批量 resize 问题
- 🐞 修复 菜单收起时设置全局主题primary 且有二级菜单时),文字高亮颜色不对
- 🐞 修复 国际化 <a href="https://gitee.com/lyt-top/vue-next-admin/issues/I43NPE" target="_blank">#I43NPE</a>。可参考:`页面/过滤筛选组件`,点击顶部语言切换,进行底部分页国际化查看
## 1.0.15
`2021.08.06`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 tagsView 右键菜单点击时的字段名id 已修改成 contextMenuClickId与路由中返回的 id 名冲突问题,感谢群友@伯牙已遇钟子期
- 🎉 新增 多个 form 表单验证界面演示
## 1.0.14
`2021.07.29`
- 🌟 更新 依赖更新最新版本vue、vuex、vue-router,出现问题,请手动降级。版本查看:<a href="https://www.npmjs.com/" target="_blank">vnpm</a>
- 🎯 优化 数据可视化图表演示加载卡顿问题、优化有图表的演示界面
- 🎯 优化 路由参数演示界面
- 🎯 优化 tagsView 操作演示界面由于存在相同路由多标签必须要传全部参数值query 或者 params
- 🎉 新增 开启 TagsView 共用开启时多个路由菜单共用一个详情组件参数为后点击的覆盖前面点击的tagsView 中只会出现一个(不支持同时出现多个 tagsView 标签))。关闭时:(多个路由菜单共用一个详情组件,参数不同,会同时出现多个 tagsView 标签)
- 🐞 修复 tagsView 共用(单标签)时,右键菜单功能点击,参数不对的问题(第 2n+个参数未覆盖第一个参数值)
- 🐞 修复 多 tagsView 标签(参数不同)、单个 tagsView 标签公用(参数不同)所带来的刷新功能、横向自动滚动等问题
- 🐞 修复 处理全屏若干问题,<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/12" target="_blank">pr!12</a>,感谢群友@另一个前端
## 1.0.13
`2021.07.25`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 数据可视化演示界面(/visualizingDemo1、/visualizingDemo2
- 🎉 新增 登录页扫码登录
## 1.0.12
`2021.07.16`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 数据可视化演示空界面(待完善)
- 🎯 优化 tagsView 动态路由xxx/:id/:name时的右键菜单刷新、关闭其它时参数丢失问题2021.07.15 优化)
- 🐞 修复 路由带参数时,复制路径到登录页,跳转后参数消失的问题
- 🐞 修复 设置多个外链,点击后,页面内容停留在上一个内容(内容未改变)、国际化处理、打开新窗口 sessionStorage 共享等
## 1.0.11
`2021.07.14`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 路由参数、图片懒加载界面演示
- ⚠️ 警告 Form 表单 `binding value must be a string or number`,解决:加上 `label-position="top"` 不报警告(等待官方修复)
- 🎯 优化 锁屏界面动画效果、首页图表显示
- 🎯 优化 tagsView 右键菜单 `关闭` 功能逻辑
- 🐞 修复 开启 TagsView 拖拽报错及小于 `1000px` 时自动设置禁止拖拽(<a href="https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI" target="_blank">#I3ZRRI</a>
- 🐞 修复 `iframe 内嵌、外链` 高度问题,使用 computed 进行计算
- 🐞 修复 默认布局开启 `侧边栏 Logo` 与关闭 `菜单水平折叠`,切换到横向布局时,菜单看不见的问题
- 🐞 修复 切换不同布局时,再去开启 `经典布局分割菜单` 功能不生效问题
- 🐞 修复 浏览器窗口标题中/英文切换不实时生效的问题
- 🐞 修复 切换布局时,某些功能不可以使用。部分界面不需要取消事件监听(proxy.mittBus.off('xxx'))
- 🐞 修复 动态路由带参数router-link 跳转问题(<a href="hhttps://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G" target="_blank">#I3YX6G</a>
- 🐞 修复 横向菜单有二级菜单时,点击子级菜单不高亮问题
- 🐞 修复 功能 tagsView 操作演示不生效
## 1.0.10
`2021.07.07`
- 🌟 更新 依赖更新最新版本(字体图标无问题)
- 🎯 优化 内嵌 iframe、外链解决 tagsView 刷新问题
## 1.0.9
`2021.07.02`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 图标选择器设置宽度、v-model 等问题
- 🎯 优化 滚动通知栏在手机上的体验
- 🎯 优化 系统管理/新增菜单(编辑菜单),使用 `图标选择器` 进行模拟
- 🎯 优化 字体图标(自动载入) 逻辑
- 🐞 修复 screenfull 全屏时,按键盘 esc 键图标不改变问题,感谢群友@伯牙已遇钟子期
## 1.0.8
`2021.06.29`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 表单中英文切换演示
- 🎯 优化 登录页查看密码 icon 图标
- 🎯 优化 图标选择器
- 🎯 优化 拖动指令
- 🐞 修复 form 表单在页面小于 576px 时的排版问题
## 1.0.7
`2021.06.24`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 拖动指令及其演示界面
- 🎯 优化 锁屏界面,解锁提示
- 🎯 优化 登录页在手机上显示的效果
## 1.0.6
`2021.06.23`
- 🎯 优化 去掉内嵌 iframe 内边距padding
- 🎯 优化 城市多级联动组件
- 🎯 优化 Tree 树形控件改成表格组件
- 🐞 修复 Cascader 级联选择器高度问题
## 1.0.5
`2021.06.22`
- 🌟 更新 vite 降级为@vite2.3.7,降级方法 `cnpm install vite@2.3.7`,防止 element plus 字体图标消失
- 🐞 修复 开启后端控制路由isRequestRoutes = true内嵌 iframe、外链不可使用的问题
## 1.0.4
`2021.06.19`
- 🌟 更新 依赖更新最新版本("vite": "^2.3.7")热更新无问题
- 🎉 新增 深克隆工具,方便开发,感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/6" target="_blank">#6</a>)
- 🎯 优化 vuex 模块自动导入。感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/4" target="_blank">#4</a>),感谢群友@web 小学生-第五君
- 🎯 优化 类型定义提高编码体验修复不能将类型“string | undefined”分配给类型“string”的问题。感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/5" target="_blank">#5</a>)
- 🎯 优化 `layout` 文件夹移动到与 `views` 文件夹同级(改动较大,`/@/views/layout` 变成 `/@/layout`
- 🎯 优化 页面有 `console.log``eslint` 不生效问题
- 🎯 优化 页面、ts 中 `any` 类型问题(改动较大)
- 🎯 优化 登录页在手机上显示的效果
- 🎯 优化 多行注释信息,鼠标放到方法名即可查看,更加直观的知道方法参数等。引入方法时需去掉以 `.ts` 结尾的后缀(改动较大)
- 🎯 优化 移除 `utils/storage.ts` 下的旧写法(改动较大)
- 🎯 优化 拆分 `router` 下内容,路由、前端、后端控制分开写,方便理解
- 🐞 修复 鼠标移入顶部用户信息栏 `开/关全屏` 文字反向问题
- 🐞 修复 热更新时NextLoading界面 loading 不消失问题 `window.nextLoading === undefined`
- 🐞 修复 vuex 中不可以使用 `/@/api/xxx` 下的接口调用问题
## 1.0.3
`2021.06.02`
- ❄️ 删除 G6 思维导图界面
- 🌟 更新 手动更新 vue、vue-router、vuex 到最近最多人使用的版本,出现不可预测的问题请降低版本。版本查看:<a href="https://www.npmjs.com/package/vue" target="_blank">vue 版本查看</a>
- 🐞 修复 开启后端控制路由 `isRequestRoutes` 在非首页刷新页面后,回到首页的问题,感谢群友@伯牙已遇钟子期
## 1.0.2
`2021.06.01`
- 🌟 更新 依赖更新最新版本
- 🐞 修复 菜单搜索中文不可以搜索的问题,感谢群友@逍遥天意
## 1.0.1
`2021.05.31`
- 🎉 新增 更新日志文件 `CHANGELOG.md`,以后每次更新都会在这里显示对应内容
- 🌟 更新 依赖更新最新版本
- 🐞 修复 分栏、经典布局路由设置 `meta.isHide``true` 时报错问题,感谢群友@29@芭芭拉
- 🐞 修复 经典布局点击 `tagsView` 左侧菜单数据不变问题

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 倔强嘴角留下一抹殇
Copyright (c) 2021 lyt-Top
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

File diff suppressed because one or more lines are too long

172
README.md

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

37
backend/.gitignore vendored
View File

@ -1,37 +0,0 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
package-lock.json
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.lock

View File

@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -1,5 +0,0 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

View File

@ -1,90 +0,0 @@
{
"name": "tsplatform",
"version": "1.1.0",
"description": "nestjs backend",
"author": "SweetHoney",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@fastify/static": "^6.5.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/platform-fastify": "^9.2.0",
"@nestjs/swagger": "^6.1.3",
"@nestjs/typeorm": "^9.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"cookie-parser": "^1.4.6",
"csurf": "^1.11.0",
"express-rate-limit": "^6.7.0",
"fastify-swagger": "^5.2.0",
"fs": "^0.0.1-security",
"helmet": "^6.0.0",
"mysql2": "^2.3.3",
"passport-jwt": "^4.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"ts-md5": "^1.3.1",
"typeorm": "^0.3.10"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "28.1.8",
"@types/node": "^16.0.0",
"@types/passport-jwt": "^3.0.7",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.3",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.8",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.1.0",
"typescript": "^4.7.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -1,47 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 10:59:16
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:58:50
* @FilePath: \tsplatform\backend\src\Config\Index.ts
* @Description: 系统全局配置
*/
/**
* 系统配置
*/
export const AppConfig = {
debug: false,
port: 3000,
jwt: {
secret: "4vRk^ga52xVP$B2vYK$%r8a8hctLgbU9",
expiresIn: "60000s"
},
md5Key: `4vRk^ga52xVP$B2vYK$%r8a8hctLgbU9`
}
/**
* 数据库配置
*/
export const DataBaseConfig = {
DataBase: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'sweet_honey',
username: 'root',
password: 'root',
//是否自动迁移同步
synchronize: true,
},
DataBase1: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'test1',
username: 'root',
password: 'root',
//是否自动迁移同步
synchronize: true,
}
}

View File

@ -1,123 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:06:21
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 11:11:11
* @FilePath: \tsplatform\backend\src\Controller\AuthController.ts
* @Description: 系统授权控制器
*/
import { Body, Controller, Get, Post, Request, Req, UseGuards } from '@nestjs/common';
import { RestfulReturn } from 'src/Expand/RestfulReturn';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { UserService } from 'src/Service/UserService';
import { RestfulHttpCodeEnum } from 'src/Enum/Global';
import { JwtService } from '@nestjs/jwt';
import { LoginDto } from 'src/EntiyDto/LoginDto';
import { AppConfig } from 'src/Config/Index';
import { RoleService } from 'src/Service/RoleService';
import { MenuService } from 'src/Service/MenuService';
@ApiBearerAuth()
@ApiTags('系统授权管理')
@Controller('/auth')
export class AuthController {
constructor(
public readonly _restful: RestfulReturn,
public readonly _service: UserService,
public readonly _roleService: RoleService,
public readonly _menuService: MenuService,
public readonly _jwt: JwtService
) { }
@ApiOperation({
summary: '获取账号登录验证码',
description: '获取账号登录验证码'
})
@Get('/captcha')
async captcha() {
return await this._restful.toJson(
RestfulHttpCodeEnum.SUCCESS, ""
);
}
@ApiOperation({
summary: '账号密码登录',
description: '账号密码登录'
})
@Post('/login')
async login(@Body() dto: LoginDto) {
const result = await this._service.repository.findOne({ where: { UserName: dto.UserName, Password: dto.Password } });
if (result && result.Id > 0) {
const token = await this._jwt.signAsync({ uid: result.Id }, {
secret: AppConfig.jwt.secret,
expiresIn: AppConfig.jwt.expiresIn,
});
return await this._restful.toJson(
RestfulHttpCodeEnum.SUCCESS, { userinfo: result, token: token }
);
} else {
return await this._restful.toJson(
RestfulHttpCodeEnum.FAIL, "账号密码有误!"
);
}
}
@ApiOperation({
summary: '根据token获取用户信息',
description: '根据token获取用户信息'
})
@Get('/getUserInfo')
async getUserInfo(@Req() request: Request) {
try {
const token = request.headers["authorization"];
const jwtResult = await this._jwt.verifyAsync(token, { secret: AppConfig.jwt.secret, ignoreExpiration: false })
const uid = jwtResult.uid;
const result = await this._service.getById(uid);
const menuRole = await this._roleService.getMenusByRoleId(result.RoleId);
const menuIds: number[] = [];
for await (const info of menuRole) {
menuIds.push(info.MenuId);
}
const menus = await this._menuService.repository.createQueryBuilder().whereInIds(menuIds).getMany()
const permissions: string[] = [];
for await (const info of menus) {
permissions.push(info.Permission);
}
return await this._restful.toJson(
RestfulHttpCodeEnum.SUCCESS, { userInfo: result, permission: permissions }
);
}
catch (ex) {
return await this._restful.toJson(
RestfulHttpCodeEnum.FAIL, ex
);
}
}
@ApiOperation({
summary: '根据token退出登录',
description: '根据token退出登录'
})
@Get('/loginOut')
async loginOut(@Req() request: Request) {
try {
const token = request.headers["authorization"];
const jwtResult = await this._jwt.verifyAsync(token, { secret: AppConfig.jwt.secret, ignoreExpiration: false })
const uid = jwtResult.uid;
const result = await this._service.getById(uid);
return await this._restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
catch (ex) {
return await this._restful.toJson(
RestfulHttpCodeEnum.FAIL, ex
);
}
}
}

View File

@ -1,143 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:05:40
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 11:11:20
* @FilePath: \tsplatform\backend\src\Controller\RoleController.ts
* @Description: 省市区控制器
*/
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { RestfulReturn } from 'src/Expand/RestfulReturn';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { IdDto, IdsDto, ListDto, PageDto } from 'src/EntiyDto/CommonDto';
import { RestfulHttpCodeEnum } from 'src/Enum/Global';
import { CityService } from 'src/Service/CityService';
import { SysCity } from 'src/Entity/SysCity';
import { Equal, Not } from 'typeorm';
@ApiBearerAuth()
@ApiTags('省市区管理')
@Controller('/city')
export class CityController {
constructor(
public readonly restful: RestfulReturn,
public readonly service: CityService
) {
}
//#region 基础控制器
@ApiOperation({
summary: '分页查询省市区列表',
description: '分页查询省市区列表'
})
@Get('/getPage')
async getPage(@Query() dto: PageDto) {
const result = await this.service.getPage(dto.Page, dto.PageSize, dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS,
{
total: result[1],
page: 1,
pageSize: 10,
rows: result[0],
}
);
}
@ApiOperation({
summary: '查询省市区列表',
description: '查询省市区列表'
})
@Get('/getList')
async getList(@Query() dto: ListDto) {
const result = await this.service.getList(dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID查询省市区',
description: '根据ID查询省市区'
})
@Get('/getById')
async getById(@Query() dto: IdDto) {
const result = await this.service.getById(dto.Id);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID数组查询省市区',
description: '根据ID数组查询省市区'
})
@Get('/getByIds')
async getByIds(@Query() dto: IdsDto) {
const result = await this.service.getByIds(dto.Ids);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '新增省市区',
description: '新增省市区'
})
@Post('/add')
async add(@Body() model: SysCity) {
const isRepeat = await this.service.repository.createQueryBuilder()
.orWhere({ RegionName: Equal(model.RegionName.trim()), })
.orWhere({ RegionCode: Equal(model.RegionCode.trim()) }).getCount();
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "地区名称或者行政编号重复!"
);
} else {
const result = await this.service.save(new SysCity());
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
}
@ApiOperation({
summary: '修改省市区',
description: '修改省市区'
})
@Post('/edit')
async edit(@Body() model: SysCity) {
const isRepeat = await this.service.repository.createQueryBuilder()
.where({ Id: Not(model.Id) })
.andWhere([{ RegionName: Equal(model.RegionName.trim()) }, { RegionCode: Equal(model.RegionCode.trim()) }])
.getCount();
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "地区名称或者行政编号重复!"
);
} else {
const result = await this.service.save(new SysCity());
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
}
@ApiOperation({
summary: '假删除省市区',
description: '假删除省市区'
})
@Post('/del')
async del(@Body() dto: IdDto) {
const result = await this.service.destroy(dto.Id);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
//#endregion
}

View File

@ -1,57 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:05:41
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:11:01
* @FilePath: \tsplatform\backend\src\Controller\UserController.ts
* @Description: 系统用户控制器
*/
import { Controller, Get } from '@nestjs/common';
import { RestfulReturn } from 'src/Expand/RestfulReturn';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { RestfulHttpCodeEnum } from 'src/Enum/Global';
import { CityService } from 'src/Service/CityService';
import { RoleService } from 'src/Service/RoleService';
import { MenuService } from 'src/Service/MenuService';
import { UserService } from 'src/Service/UserService';
@ApiBearerAuth()
@ApiTags('系统管理')
@Controller('/')
export class IndexController {
constructor(
public readonly restful: RestfulReturn,
public readonly role_service: RoleService,
public readonly menu_service: MenuService,
public readonly user_service: UserService,
public readonly city_service: CityService,
) {
}
@ApiOperation({
summary: '数据初始化',
description: '数据初始化'
})
@Get('/seed')
async seedData() {
//初始化账号数据
const userCount = await this.user_service.seedData();
//初始化菜单数据
const menuCount = await this.menu_service.seedData();
//初始化角色数据
const roleCounts = await this.role_service.seedData();
//初始化省市区数据
const cityCount = await this.city_service.seedData();
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS,
`一共Seed${userCount}条记录!\r\n` +
`一共Seed${menuCount}条记录!\r\n` +
`一共Seed${roleCounts[0]}条记录!\r\n` +
`一共Seed${roleCounts[0]}条记录!\r\n` +
`一共Seed${cityCount}条记录!`
);
}
}

View File

@ -1,294 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 00:23:12
* @FilePath: \tsplatform\backend\src\Controller\MenuController.ts
* @Description: 系统菜单控制器
*/
import { Body, Controller, Get, HttpException, HttpStatus, Post, Query } from '@nestjs/common';
import { RestfulReturn } from 'src/Expand/RestfulReturn';
import {
ApiBearerAuth,
ApiOperation,
ApiProperty,
ApiTags,
} from '@nestjs/swagger';
import { SysMenu } from 'src/Entity/SysMenu';
import { plainToClass } from 'class-transformer';
import { IdDto, IdsDto, IdStatusDto, ListDto, PageDto, RoleIdDto } from 'src/EntiyDto/CommonDto';
import { MenuService } from 'src/Service/MenuService';
import { RestfulHttpCodeEnum } from 'src/Enum/Global';
import { MenuDto } from 'src/EntiyDto/MenuDto';
import { RouteMenu } from 'src/EntiyDto/RouteMenu';
import { Equal, In, Not } from 'typeorm';
import { RoleService } from 'src/Service/RoleService';
@Controller('/menu')
@ApiBearerAuth()
@ApiTags('系统菜单管理')
export class MenuController {
constructor(
public readonly restful: RestfulReturn,
public readonly service: MenuService,
public readonly _roleService: RoleService,
) {
}
@ApiOperation({
summary: '获取所有菜单Tree菜单列表使用',
description: '获取所有菜单Tree'
})
@Get('/getMenuTree')
async getMenuTree() {
const result = await this.MenuTree(0);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据角色ID获取用户授权的菜单Tree前端router使用',
description: '根据角色ID获取用户授权的菜单Tree'
})
@Get('/getAuthMenuTree')
async getAuthMenuTree(@Query() dto: RoleIdDto) {
if (dto.RoleId > 0) {
const result = await this.RouteTree(0, dto.RoleId);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
throw new HttpException("角色Id不能为空", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
//递归获取菜单Tree
private async MenuTree(parentId: number): Promise<SysMenu[]> {
const result = await this.service.repository.createQueryBuilder()
.where({ ParentId: parentId })
.orderBy("sort", "ASC")
.getMany();
const treeList: SysMenu[] = new Array<SysMenu>;
for await (let info of result) {
const children = await this.MenuTree(info.Id);
const node: any = {
Id: info.Id,
ParentId: info.ParentId,
Type: info.Type,
Title: info.Title,
Path: info.Path,
Permission: info.Permission,
Component: info.Component,
Icon: info.Icon,
Name: info.Name,
Redirect: info.Redirect,
IsLink: info.IsLink,
IsHide: info.IsHide,
IsKeepAlive: info.IsKeepAlive,
IsAffix: info.IsAffix,
IsIframe: info.IsIframe,
Children: [],
Sort: info.Sort,
};
if (children.length > 0) {
node.Children = children;
}
treeList.push(node);
}
return treeList;
}
//递归获取前端路由Tree
private async RouteTree(parentId: number, RoleId: number): Promise<RouteMenu[]> {
const menuRole = await this._roleService.getMenusByRoleId(RoleId);
const menuIds: number[] = [];
for await (const info of menuRole) {
menuIds.push(info.MenuId);
}
const result = await this.service.repository.createQueryBuilder()
.where({ ParentId: parentId, Id: In(menuIds) })
.orderBy("sort", "ASC")
.getMany();
const treeList: RouteMenu[] = new Array<RouteMenu>;
for await (let info of result) {
const children = await this.RouteTree(info.Id, RoleId);
const node: RouteMenu = {
name: info.Name,
path: info.Path,
component: info.Component,
redirect: info.Redirect,
meta: {
title: info.Title,
icon: info.Icon,
isLink: info.IsLink,
isHide: info.IsHide,
isKeepAlive: info.IsKeepAlive,
isAffix: info.IsAffix,
isIframe: info.IsIframe,
permission: info.Permission,
},
children: [],
};
if (children.length > 0) {
for await (let item of children) {
node.children.push({
name: item.name,
path: item.path,
component: item.component,
redirect: item.redirect,
meta: {
title: item.meta.title,
icon: item.meta.icon,
isLink: item.meta.isLink,
isHide: item.meta.isHide,
isKeepAlive: item.meta.isKeepAlive,
isAffix: item.meta.isAffix,
isIframe: item.meta.isIframe,
permission: item.meta.permission,
},
children: [],
});
}
}
treeList.push(node);
}
return treeList;
}
//#region 基础控制器
@ApiOperation({
summary: '分页查询菜单列表',
description: '分页查询菜单列表'
})
@ApiProperty()
@Get('/getPage')
async getPage(@Query() dto: PageDto) {
const result = await this.service.getPage(dto.Page, dto.PageSize, dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS,
{
total: result[1],
page: 1,
pageSize: 10,
rows: result[0],
}
);
}
@ApiOperation({
summary: '查询菜单列表',
description: '查询菜单列表'
})
@Get('/getList')
async getList(@Query() dto: ListDto) {
const result = await this.service.getList(dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID查询菜单',
description: '根据ID查询菜单'
})
@Get('/getById')
async getById(@Query() dto: IdDto) {
const result = await this.service.getById(dto.Id);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID数组查询菜单',
description: '根据ID数组查询菜单'
})
@Get('/getByIds')
async getByIds(@Query() dto: IdsDto) {
const result = await this.service.getByIds(dto.Ids);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '新增菜单',
description: '新增菜单'
})
@Post('/add')
async add(@Body() dto: MenuDto) {
const model = plainToClass(SysMenu, dto);
const isRepeat = await this.service.repository.createQueryBuilder()
.orWhere({ Name: Equal(model.Name.trim()), })
.orWhere({ Permission: Equal(model.Permission.trim()) }).getCount();
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "路由名称或者权限标识重复!"
);
} else {
const result = await this.service.save(model);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
}
@ApiOperation({
summary: '修改菜单',
description: '修改菜单'
})
@Post('/edit')
async edit(@Body() dto: MenuDto) {
const model = plainToClass(SysMenu, dto);
const isRepeat = await this.service.repository.createQueryBuilder()
.where({ Id: Not(model.Id) })
.andWhere([{ Name: Equal(model.Name.trim()) }, { Permission: Equal(model.Permission.trim()) }])
.getCount();
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "路由名称或者权限标识重复!"
);
} else {
const result = await this.service.save(model);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
}
@ApiOperation({
summary: '假删除菜单',
description: '假删除菜单'
})
@Post('/del')
async del(@Body() dto: IdDto) {
const result = await this.service.destroy(dto.Id);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '修改菜单状态',
description: '修改菜单状态'
})
@Post('/setStatus')
async setStatus(@Body() dto: IdStatusDto) {
const result = await this.service.repository.createQueryBuilder()
.update()
.set({
Status: dto.Status
})
.where("Id = :Id", { Id: dto.Id })
.execute();
if (result.affected > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
//#endregion
}

View File

@ -1,189 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:05:40
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 22:32:29
* @FilePath: \tsplatform\backend\src\Controller\RoleController.ts
* @Description: 系统角色控制器
*/
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { RestfulReturn } from 'src/Expand/RestfulReturn';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { RoleService } from 'src/Service/RoleService';
import { IdDto, IdsDto, IdStatusDto, ListDto, PageDto } from 'src/EntiyDto/CommonDto';
import { RestfulHttpCodeEnum } from 'src/Enum/Global';
import { SysRole } from 'src/Entity/SysRole';
import { RoleDto } from 'src/EntiyDto/RoleDto';
import { plainToClass } from 'class-transformer';
import { Not } from 'typeorm';
@ApiBearerAuth()
@ApiTags('系统角色管理')
@Controller('/role')
export class RoleController {
constructor(
public readonly restful: RestfulReturn,
public readonly service: RoleService
) {
}
//#region 基础控制器
@ApiOperation({
summary: '分页查询角色列表',
description: '分页查询角色列表'
})
@Get('/getPage')
async getPage(@Query() dto: PageDto) {
const result = await this.service.getPage(dto.Page, dto.PageSize, dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS,
{
total: result[1],
page: 1,
pageSize: 10,
rows: result[0],
}
);
}
@ApiOperation({
summary: '查询角色列表',
description: '查询角色列表'
})
@Get('/getList')
async getList(@Query() dto: ListDto) {
const result = await this.service.getList(dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID查询角色',
description: '根据ID查询角色'
})
@Get('/getById')
async getById(@Query() dto: IdDto) {
const result = await this.service.getById(dto.Id);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID数组查询角色',
description: '根据ID数组查询角色'
})
@Get('/getByIds')
async getByIds(@Query() dto: IdsDto) {
const result = await this.service.getByIds(dto.Ids);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '新增角色',
description: '新增角色'
})
@Post('/add')
async add(@Body() dto: RoleDto) {
const model = plainToClass(SysRole, dto);
const isRepeat = await this.service.repository.count({
where: { Name: model.Name.trim() }
});
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "角色名称重复!"
);
} else {
const result = await this.service.save(model);
if (result) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
}
@ApiOperation({
summary: '修改角色',
description: '修改角色'
})
@Post('/edit')
async edit(@Body() dto: RoleDto) {
const model = plainToClass(SysRole, dto);
const isRepeat = await this.service.repository.count({
where: { Id: Not(model.Id), Name: model.Name.trim() }
});
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "角色名称重复!"
);
} else {
const result = await this.service.save(model);
if (result) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
}
@ApiOperation({
summary: '删除角色',
description: '删除角色'
})
@Post('/del')
async del(@Body() dto: IdDto) {
const result = await this.service.destroy(dto.Id);
if (result) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
@ApiOperation({
summary: '修改角色状态',
description: '修改角色状态'
})
@Post('/setStatus')
async setStatus(@Body() dto: IdStatusDto) {
const result = await this.service.repository.createQueryBuilder()
.update()
.set({
Status: dto.Status
})
.where("Id = :Id", { Id: dto.Id })
.execute();
if (result.affected > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
//#endregion
}

View File

@ -1,188 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:05:41
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 22:49:42
* @FilePath: \tsplatform\backend\src\Controller\UserController.ts
* @Description: 系统用户控制器
*/
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { RestfulReturn } from 'src/Expand/RestfulReturn';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { UserService } from 'src/Service/UserService';
import { SysUser } from 'src/Entity/SysUser';
import { IdDto, IdsDto, IdStatusDto, ListDto, PageDto } from 'src/EntiyDto/CommonDto';
import { RestfulHttpCodeEnum } from 'src/Enum/Global';
import { plainToClass } from 'class-transformer';
import { AppConfig } from 'src/Config/Index';
import { Md5 } from 'ts-md5';
import { UserDto } from 'src/EntiyDto/UserDto';
import { Equal, Not } from 'typeorm';
@ApiBearerAuth()
@ApiTags('系统用户管理')
@Controller('/user')
export class UserController {
constructor(
public readonly restful: RestfulReturn,
public readonly service: UserService
) {
}
//#region 基础控制器
@ApiOperation({
summary: '分页查询用户列表',
description: '分页查询用户列表'
})
@Get('/getPage')
async getPage(@Query() dto: PageDto) {
const result = await this.service.getPage(dto.Page, dto.PageSize, dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS,
{
total: result[1],
page: 1,
pageSize: 10,
rows: result[0],
}
);
}
@ApiOperation({
summary: '查询用户列表',
description: '查询用户列表'
})
@Get('/getList')
async getList(@Query() dto: ListDto) {
const result = await this.service.getList(dto.Keywords, dto.OrderBy);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID查询用户',
description: '根据ID查询用户'
})
@Get('/getById')
async getById(@Query() dto: IdDto) {
const result = await this.service.getById(dto.Id);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '根据ID数组查询用户',
description: '根据ID数组查询用户'
})
@Get('/getByIds')
async getByIds(@Query() dto: IdsDto) {
const result = await this.service.getByIds(dto.Ids);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '新增用户',
description: '新增用户'
})
@Post('/add')
async add(@Body() dto: UserDto) {
const model = plainToClass(SysUser, dto);
const isRepeat = await this.service.repository.createQueryBuilder()
.orWhere({ UserName: Equal(model.UserName.trim()), })
.orWhere({ Phone: Equal(model.Phone.trim()) })
.orWhere({ Email: Equal(model.Email.trim()) }).getCount();
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "账号/手机号/邮箱重复!"
);
} else {
//给新用户设置个默认密码888888用户登录后可以自行修改密码
model.Password = Md5.hashStr(Md5.hashStr("888888") + AppConfig.md5Key);
const result = await this.service.save(model);
if (result) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
}
@ApiOperation({
summary: '修改用户',
description: '修改用户'
})
@Post('/edit')
async edit(@Body() dto: UserDto) {
const model = plainToClass(SysUser, dto);
const isRepeat = await this.service.repository.createQueryBuilder()
.where({ Id: Not(model.Id) })
.andWhere([{ UserName: Equal(model.UserName.trim()) }, { Phone: Equal(model.Phone.trim()) }, { Email: Equal(model.Email.trim()) }])
.getCount();
if (isRepeat > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, "账号/手机号/邮箱重复!"
);
} else {
//给新用户设置个默认密码888888用户登录后可以自行修改密码
model.Password = Md5.hashStr(Md5.hashStr("888888") + AppConfig.md5Key);
const result = await this.service.save(model);
if (result) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
}
@ApiOperation({
summary: '假删除用户',
description: '假删除用户'
})
@Post('/del')
async del(@Body() dto: IdDto) {
const result = await this.service.destroy(dto.Id);
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
}
@ApiOperation({
summary: '修改用户状态',
description: '修改用户状态'
})
@Post('/setStatus')
async setStatus(@Body() dto: IdStatusDto) {
const result = await this.service.repository.createQueryBuilder()
.update()
.set({
Status: dto.Status
})
.where("Id = :Id", { Id: dto.Id })
.execute();
if (result.affected > 0) {
return await this.restful.toJson(
RestfulHttpCodeEnum.SUCCESS, result
);
} else {
return await this.restful.toJson(
RestfulHttpCodeEnum.FAIL, result
);
}
}
//#endregion
}

View File

@ -1,43 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 11:26:13
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-22 21:57:27
* @FilePath: \tsplatform\backend\src\Entity\SysBase.ts
* @Description: 系统基类实体模型
*/
import { Entity, Column, PrimaryColumn, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, VersionColumn } from 'typeorm';
@Entity()
export class SysBase {
@PrimaryGeneratedColumn()
@PrimaryColumn({ comment: "ID" })
Id: number;
@Column({ comment: "状态", default: 0, nullable: true })
Status: number;
@Column({ comment: "商户/站点ID", default: 0, nullable: true })
MerchantId: number;
//自动为实体插入日期
@CreateDateColumn()
CreateTime: Date;
//每次调用实体管理器或存储库的save时自动更新实体日期
@UpdateDateColumn()
UpdateTime: Date;
@Column({ comment: "操作人ID", default: 0, nullable: true })
OperatorId: number;
@Column({ comment: "修改人ID", default: 0, nullable: true })
UpdateUserId: number;
@Column({ comment: "是否删除", default: false, nullable: true })
IsDelete: boolean;
//每次调用实体管理器或存储库的save时自动增长实体版本
@VersionColumn()
Version: number;
}

View File

@ -1,35 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 13:12:19
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-18 14:18:50
* @FilePath: \tsplatform\backend\src\Entity\SysCity.ts
* @Description: 系统城市实体模型
*/
import { Entity, Column, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class SysCity {
@PrimaryGeneratedColumn()
@PrimaryColumn({ comment: "ID" })
Id: number;
@Column({ comment: "地区编号" })
RegionId: string;
@Column({ comment: "地区名称" })
RegionName: string;
@Column({ comment: "地区简称", nullable: true })
RegionShortName: string;
@Column({ comment: "地区行政编号", nullable: true })
RegionCode: string;
@Column({ comment: "地区父级ID" })
RegionParentId: string;
@Column({ comment: "地区级别" })
RegionLevel: number;
}

View File

@ -1,19 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 13:10:02
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-22 15:08:00
* @FilePath: \tsplatform\backend\src\Entity\SysDictionaries.ts
* @Description: 系统字典实体模型
*/
import { Entity, Column } from 'typeorm';
import { SysBase } from 'src/Entity/SysBase';
@Entity()
export class SysDictionaries extends SysBase {
@Column({ comment: "字典名称" })
Name: string;
@Column({ comment: "备注", nullable: true })
Remark: string;
}

View File

@ -1,61 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 11:21:40
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 21:52:49
* @FilePath: \tsplatform\backend\src\Entity\SysMenu.ts
* @Description: 系统菜单实体模型
*/
import { Entity, Column } from 'typeorm';
import { SysBase } from 'src/Entity/SysBase';
import { MenuEnum } from 'src/Enum/Global';
@Entity()
export class SysMenu extends SysBase {
@Column({ comment: "父级ID", default: 0 })
ParentId: number;
@Column({ comment: "菜单类型", default: 0 })
Type: MenuEnum;
@Column({ comment: "路由名称", unique: true })
Name: string;
@Column({ comment: "路由地址", nullable: true })
Path: string;
@Column({ comment: "组件名称", nullable: true })
Component: string;
@Column({ comment: "重定向路径", nullable: true })
Redirect: string;
@Column({ comment: "菜单名称" })
Title: string;
@Column({ comment: "是否外链,开启外链条件,`1、isLink: 链接地址不为空 2、isIframe:false`", nullable: true })
IsLink: string;
@Column({ comment: "是否隐藏此路由" })
IsHide: boolean;
@Column({ comment: "是否缓存组件状态" })
IsKeepAlive: boolean;
@Column({ comment: "是否固定在 tagsView 栏上" })
IsAffix: boolean;
@Column({ comment: "是否内嵌窗口,开启条件,`1、isIframe:true 2、isLink链接地址不为空`" })
IsIframe: boolean;
@Column({ comment: "路由权限标识", unique: true })
Permission: string;
@Column({ comment: "菜单图标", nullable: true })
Icon: string;
@Column({ comment: "排序", nullable: true, default: 0 })
Sort: number;
Children: SysMenu[];
}

View File

@ -1,22 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 13:08:01
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 16:46:20
* @FilePath: \tsplatform\backend\src\Entity\SysRole.ts
* @Description: 系统角色实体模型
*/
import { Entity, Column } from 'typeorm';
import { SysBase } from 'src/Entity/SysBase';
import { SysRoleMenu } from 'src/Entity/SysRoleMenu';
@Entity()
export class SysRole extends SysBase {
@Column({ comment: "角色名称", unique: true })
Name: string;
@Column({ comment: "备注", nullable: true })
Remark: string;
SysRoleMenu: SysRoleMenu[];
}

View File

@ -1,18 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 13:54:50
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 17:32:38
* @FilePath: \tsplatform\backend\src\Entity\SysRoleMenu.ts
* @Description: 系统角色菜单映射实体模型
*/
import { Entity, Column, ManyToOne } from 'typeorm';
import { SysBase } from 'src/Entity/SysBase';
@Entity()
export class SysRoleMenu extends SysBase {
@Column({ comment: "菜单Id", default: 0 })
MenuId: number;
@Column({ comment: "角色Id", default: 0 })
RoleId: number;
}

View File

@ -1,57 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 13:55:35
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 23:13:45
* @FilePath: \tsplatform\backend\src\Entity\SysUser.ts
* @Description: 系统用户映射实体模型
*/
import { Entity, Column, Index } from 'typeorm';
import { SysBase } from 'src/Entity/SysBase';
import { SexEnum, UserTypeEnum } from 'src/Enum/Global';
@Entity()
export class SysUser extends SysBase {
@Column({ comment: "账号类型", default: 1 })
UserType: UserTypeEnum;
@Column({ comment: "账号", unique: true })
UserName: string;
@Column({ comment: "密码" })
Password: string;
@Column({ comment: "昵称", nullable: true })
NickName: string;
@Column({ comment: "头像", nullable: true })
Avatar: string;
@Column({ comment: "出生日期", nullable: true })
Birthday: string;
@Column({ comment: "性别", nullable: true, default: 0 })
Sex: SexEnum;
@Column({ comment: "邮箱", unique: true })
Email: string;
@Column({ comment: "手机号码", unique: true })
Phone: string;
@Column({ comment: "真实姓名", nullable: true })
RealName: string;
@Column({ comment: "身份证号", nullable: true })
IdCard: string;
@Column({ comment: "个性签名", nullable: true })
Signature: string;
@Column({ comment: "个人简介", nullable: true })
Introduction: string;
@Column({ comment: "备注", nullable: true })
Remark: string;
@Column({ comment: "角色Id", default: 0 })
RoleId: number;
}

View File

@ -1,78 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-18 09:02:28
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 00:20:22
* @FilePath: \tsplatform\backend\src\Expand\CommonDto.ts
* @Description: 公共DTO服务
*/
import { Injectable } from "@nestjs/common";
import { ApiProperty } from "@nestjs/swagger";
import { IsArray, IsNotEmpty, IsNumber, IsNumberString } from "class-validator";
//分页查询DTO
@Injectable()
export class PageDto {
@IsNotEmpty()
@IsNumberString()
@ApiProperty({ name: "Page", required: true })
Page: number
@IsNotEmpty()
@IsNumberString()
@ApiProperty({ name: "PageSize", required: true })
PageSize: number
@ApiProperty({ name: "Keywords", required: false })
Keywords: string
@ApiProperty({ name: "OrderBy", required: false })
OrderBy: string
}
//列表查询DTO
@Injectable()
export class ListDto {
@ApiProperty({ name: "Keywords", required: false })
Keywords: string
@ApiProperty({ name: "OrderBy", required: false })
OrderBy: string
}
//ids DTO
@Injectable()
export class IdsDto {
@IsNotEmpty()
@IsArray()
@ApiProperty({ name: "Ids", required: true })
Ids: number[]
}
//id DTO
@Injectable()
export class IdDto {
@IsNotEmpty()
@IsNumber()
@ApiProperty({ name: "Id", required: true })
Id: number
}
@Injectable()
export class RoleIdDto {
@ApiProperty({ name: "RoleId" })
RoleId: number;
}
//Id Status DTO
@Injectable()
export class IdStatusDto {
@IsNotEmpty()
@IsNumber()
@ApiProperty({ name: "Id", required: true })
Id: number
@IsNotEmpty()
@IsNumber()
@ApiProperty({ name: "Status", required: true })
Status: number
}

View File

@ -1,21 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-18 17:35:08
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 12:12:42
* @FilePath: \tsplatform\backend\src\EntiyDto\Login.ts
* @Description: DTO类
*/
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
export class LoginDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ name: "UserName", required: true })
UserName: string;
@IsNotEmpty()
@IsString()
@ApiProperty({ name: "Password", required: true })
Password: string;
}

View File

@ -1,70 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-18 17:35:08
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 00:14:13
* @FilePath: \tsplatform\backend\src\EntiyDto\MenuDto.ts
* @Description: SysMenu DTO类
*/
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsNotEmpty, IsNumber, IsString } from "class-validator";
import { MenuEnum } from "src/Enum/Global";
export class MenuDto {
@ApiProperty({ name: "ParentId" })
ParentId: number;
@IsNotEmpty()
@IsNumber()
@ApiProperty({ name: "Type", required: true })
Type: MenuEnum;
@ApiProperty({ name: "Name", required: false })
Name: string;
@ApiProperty({ name: "Path", required: false })
Path: string;
@ApiProperty({ name: "Component", required: false })
Component: string;
@ApiProperty({ name: "Redirect", required: false })
Redirect: string;
@IsNotEmpty()
@IsString()
@ApiProperty({ name: "Title", required: true })
Title: string;
@ApiProperty({ name: "IsLink", required: false })
IsLink: string;
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ name: "IsHide", required: true })
IsHide: boolean;
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ name: "IsKeepAlive", required: true })
IsKeepAlive: boolean;
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ name: "IsAffix", required: true })
IsAffix: boolean;
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ name: "IsIframe", required: true })
IsIframe: boolean;
@IsNotEmpty()
@IsString()
@ApiProperty({ name: "Permission", required: true })
Permission: string;
@ApiProperty({ name: "Icon", required: false })
Icon: string;
}

View File

@ -1,25 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-22 16:16:08
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 12:12:56
* @FilePath: \backend\src\EntiyDto\RoleDto.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { ApiProperty } from "@nestjs/swagger";
import { IsArray, IsNotEmpty } from "class-validator";
export class RoleDto {
@IsNotEmpty()
@ApiProperty({ name: "Name", required: true })
Name: string;
@ApiProperty({ name: "Remark" })
Remark: string;
@IsNotEmpty()
@IsArray()
@ApiProperty({ name: "SysRoleMenu", required: true })
SysRoleMenu: number[];
}

View File

@ -1,26 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-18 17:35:08
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 12:13:13
* @FilePath: \tsplatform\backend\src\EntiyDto\RouteMenu.ts
* @Description: RouteMenu类 菜单树查询不需要dto映射
*/
export class RouteMenu {
name: string;
path: string;
component: string;
redirect: string;
meta: Meta;
children: RouteMenu[];
}
export class Meta {
title: string;
isLink: string;
isHide: boolean;
isKeepAlive: boolean;
isAffix: boolean;
isIframe: boolean;
permission: string;
icon: string;
}

View File

@ -1,65 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-22 16:16:19
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 21:32:25
* @FilePath: \backend\src\EntiyDto\UserDto.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsNumber, IsNumberString } from "class-validator";
export class UserDto {
@IsNotEmpty()
@IsNumber()
@ApiProperty({ name: "UserType", required: true })
UserType: number;
@IsNotEmpty()
@ApiProperty({ name: "Name", required: true })
UserName: string;
@ApiProperty({ name: "Password" })
Password: string;
@ApiProperty({ name: "NickName" })
NickName: string;
@ApiProperty({ name: "Avatar" })
Avatar: string;
@ApiProperty({ name: "Birthday" })
Birthday: string;
@IsNotEmpty()
@IsNumber()
@ApiProperty({ name: "Sex" })
Sex: number;
@ApiProperty({ name: "Email", required: true })
Email: string;
@IsNotEmpty()
@IsNumberString()
@ApiProperty({ name: "Phone", required: true })
Phone: string;
@ApiProperty({ name: "RealName" })
RealName: string;
@ApiProperty({ name: "IdCard" })
IdCard: string;
@ApiProperty({ name: "Signature" })
Signature: string;
@ApiProperty({ name: "Introduction" })
Introduction: string;
@ApiProperty({ name: "Remark" })
Remark: string;
@ApiProperty({ name: "RoleId", required: true })
RoleId: number;
}

View File

@ -1,28 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 13:25:43
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-18 09:15:04
* @FilePath: \tsplatform\backend\src\Enum\Global.ts
* @Description: 全局枚举类
*/
export enum RestfulHttpCodeEnum {
SUCCESS = 200,
FAIL = 300,
NODATA = 201,
}
export enum UserTypeEnum {
Admin = 0,
None = 1,
SuperAdmin = 999,
}
export enum SexEnum {
Man = 0,
WoMan = 1
}
export enum MenuEnum {
Directory = 0,
Menu = 1,
Btn = 2
}

View File

@ -1,24 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-18 09:10:15
* @FilePath: \tsplatform\backend\src\Expand\RestfulReturn.ts
* @Description: RestfulAPI全局响应结构
*/
import { Injectable } from "@nestjs/common";
import { RestfulHttpCodeEnum } from "src/Enum/Global";
@Injectable()
export class RestfulReturn {
//HttpStatus=200时的状态码规则
toJson(code: RestfulHttpCodeEnum, data: any) {
return {
code: code,
msg: code === RestfulHttpCodeEnum.SUCCESS ? "操作成功!" :
code === RestfulHttpCodeEnum.FAIL ? "操作失败!" :
code === RestfulHttpCodeEnum.NODATA ? "数据为空!" : "未知错误!",
data: data,
timespan: new Date().getTime()
}
}
}

View File

@ -1,15 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-18 09:02:28
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:13:53
* @FilePath: \tsplatform\backend\src\Logging\LoggerPrint.ts
* @Description: 全局日志打印服务
*/
import { Injectable, Logger } from "@nestjs/common";
@Injectable()
export class LoggerPrint {
constructor(private readonly logger: Logger) {
}
}

View File

@ -1,33 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-21 11:57:30
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-22 10:41:09
* @FilePath: \tsplatform\backend\src\Middleware\JwtHandle.ts
* @Description: 授权中间件
*/
import { HttpException, HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { NextFunction } from 'express';
import { AppConfig } from 'src/Config/Index';
@Injectable()
export class JwtHandle implements NestMiddleware {
constructor(
public readonly _jwt: JwtService
) { }
async use(req: Request, res: Response, next: NextFunction) {
try {
const token = req.headers["authorization"];
if (!token) {
throw new HttpException("Token指令牌不存在", HttpStatus.UNAUTHORIZED);
} else {
await this._jwt.verifyAsync(token, { secret: AppConfig.jwt.secret, ignoreExpiration: false })
next();
}
}
catch (ex) {
throw new HttpException(ex.message, HttpStatus.UNAUTHORIZED);
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,216 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-24 09:22:50
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 09:58:45
* @FilePath: \backend\src\SeedData\Menu.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
export const menus = [{
"Id": 8,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-23T07:53:21.453Z",
"UpdateTime": "2022-11-24T01:29:41.000Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 5,
"ParentId": 2,
"Type": 1,
"Name": "system.city",
"Path": "/system/city",
"Component": "/system/city/index",
"Redirect": "",
"Title": "message.router.system.city",
"IsLink": "",
"IsHide": false,
"IsKeepAlive": true,
"IsAffix": false,
"IsIframe": false,
"Permission": "system.city",
"Icon": "ele-OfficeBuilding",
"Sort": 101
},
{
"Id": 7,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-23T07:50:37.192Z",
"UpdateTime": "2022-11-24T01:21:19.762Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 4,
"ParentId": 2,
"Type": 1,
"Name": "system.dic",
"Path": "/system/dic",
"Component": "/system/dic/index",
"Redirect": "",
"Title": "message.router.system.dic",
"IsLink": "",
"IsHide": false,
"IsKeepAlive": true,
"IsAffix": false,
"IsIframe": false,
"Permission": "system.dic",
"Icon": "iconfont icon-zhongyingwen1",
"Sort": 100
},
{
"Id": 6,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.445Z",
"UpdateTime": "2022-11-23T05:56:53.726Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 0,
"ParentId": 0,
"Type": 0,
"Name": "personal",
"Path": "/personal",
"Component": "personal/index",
"Redirect": null,
"Title": "message.router.personal",
"IsLink": "",
"IsHide": true,
"IsKeepAlive": false,
"IsAffix": false,
"IsIframe": false,
"Permission": "personal",
"Icon": "iconfont icon-gerenzhongxin",
"Sort": 6
},
{
"Id": 5,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.445Z",
"UpdateTime": "2022-11-23T08:05:06.779Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 0,
"ParentId": 2,
"Type": 1,
"Name": "system.role",
"Path": "/system/role",
"Component": "system/role/index",
"Redirect": null,
"Title": "message.router.system.role",
"IsLink": "",
"IsHide": false,
"IsKeepAlive": true,
"IsAffix": false,
"IsIframe": false,
"Permission": "system.role",
"Icon": "fa fa-street-view",
"Sort": 5
},
{
"Id": 4,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.445Z",
"UpdateTime": "2022-11-23T08:05:03.689Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 0,
"ParentId": 2,
"Type": 1,
"Name": "system.user",
"Path": "/system/user",
"Component": "system/user/index",
"Redirect": null,
"Title": "message.router.system.user",
"IsLink": null,
"IsHide": false,
"IsKeepAlive": true,
"IsAffix": false,
"IsIframe": false,
"Permission": "system.user",
"Icon": "iconfont icon-icon-",
"Sort": 4
},
{
"Id": 3,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.445Z",
"UpdateTime": "2022-11-23T08:05:00.650Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 0,
"ParentId": 2,
"Type": 1,
"Name": "system.menu",
"Path": "/system/menu",
"Component": "system/menu/index",
"Redirect": null,
"Title": "message.router.system.menu",
"IsLink": "",
"IsHide": false,
"IsKeepAlive": true,
"IsAffix": false,
"IsIframe": false,
"Permission": "system.menu",
"Icon": "iconfont icon-caidan",
"Sort": 3
},
{
"Id": 2,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.445Z",
"UpdateTime": "2022-11-23T08:05:16.188Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 0,
"ParentId": 0,
"Type": 0,
"Name": "system",
"Path": "/system",
"Component": "layout/routerView/parent",
"Redirect": "/system/menu",
"Title": "message.router.system.system",
"IsLink": "",
"IsHide": false,
"IsKeepAlive": true,
"IsAffix": false,
"IsIframe": false,
"Permission": "system",
"Icon": "iconfont icon-xitongshezhi",
"Sort": 2
},
{
"Id": 1,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.445Z",
"UpdateTime": "2022-11-22T13:59:37.471Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 0,
"ParentId": 0,
"Type": 0,
"Name": "home",
"Path": "/home",
"Component": "home/index",
"Redirect": "",
"Title": "message.router.home",
"IsLink": "",
"IsHide": false,
"IsKeepAlive": true,
"IsAffix": true,
"IsIframe": false,
"Permission": "home",
"Icon": "iconfont icon-shouye",
"Sort": 1
}];

View File

@ -1,179 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-24 09:23:07
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:04:17
* @FilePath: \backend\src\SeedData\Role.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
export const roles = [
{
"Id": 1,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.567Z",
"UpdateTime": "2022-11-24T01:35:03.000Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 4,
"Name": "超级管理员",
"Remark": null
},
{
"Id": 2,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.567Z",
"UpdateTime": "2022-11-24T01:36:45.000Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 3,
"Name": "普通用户",
"Remark": null
}
];
export const roleMenus = [{
"Id": 1,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 1,
"RoleId": 1
},
{
"Id": 2,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 2,
"RoleId": 1
},
{
"Id": 3,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 3,
"RoleId": 1
},
{
"Id": 4,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 4,
"RoleId": 1
},
{
"Id": 5,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 5,
"RoleId": 1
},
{
"Id": 6,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 6,
"RoleId": 1
},
{
"Id": 7,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 7,
"RoleId": 1
},
{
"Id": 8,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:35:03.437Z",
"UpdateTime": "2022-11-24T01:35:03.437Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 8,
"RoleId": 1
},
{
"Id": 9,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:36:45.486Z",
"UpdateTime": "2022-11-24T01:36:45.486Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 1,
"RoleId": 2
},
{
"Id": 10,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:36:45.486Z",
"UpdateTime": "2022-11-24T01:36:45.486Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 2,
"RoleId": 2
},
{
"Id": 11,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-24T01:36:45.486Z",
"UpdateTime": "2022-11-24T01:36:45.486Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 1,
"MenuId": 4,
"RoleId": 2
}];

View File

@ -1,60 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-24 09:22:58
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:04:55
* @FilePath: \backend\src\SeedData\User.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
export const users = [{
"Id": 1,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-22T13:59:37.496Z",
"UpdateTime": "2022-11-24T01:21:53.423Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 0,
"UserType": 999,
"UserName": "admin",
"Password": "1a23c5066d05474304c03342e57850d9",
"NickName": "甜蜜蜜",
"Avatar": "https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500",
"Birthday": null,
"Sex": 0,
"Email": "490912587@qq.com",
"Phone": "13000000000",
"RealName": null,
"IdCard": null,
"Signature": null,
"Introduction": null,
"Remark": null,
"RoleId": 1
},
{
"Id": 2,
"Status": 0,
"MerchantId": 0,
"CreateTime": "2022-11-23T08:45:37.321Z",
"UpdateTime": "2022-11-24T01:39:33.000Z",
"OperatorId": 0,
"UpdateUserId": 0,
"IsDelete": false,
"Version": 4,
"UserType": 0,
"UserName": "test",
"Password": "7b0ea1aa91470cd870111415a94b475b",
"NickName": "甜蜜蜜的小号",
"Avatar": "https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500",
"Birthday": null,
"Sex": 1,
"Email": "1844045442@qq.com",
"Phone": "15000000000",
"RealName": null,
"IdCard": null,
"Signature": null,
"Introduction": null,
"Remark": null,
"RoleId": 2
}];

View File

@ -1,94 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:08:52
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 11:12:25
* @FilePath: \tsplatform\backend\src\Service\UserService.ts
* @Description: 省市区服务
*/
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, In, InsertResult, Like, Repository } from 'typeorm';
import { LoggerPrint } from 'src/Logging/LoggerPrint';
import { SysCity } from 'src/Entity/SysCity';
import { citys } from 'src/SeedData/City';
@Injectable()
export class CityService {
private readonly tableName: string = "省市区";
constructor(
public readonly loggerPrint: LoggerPrint,
public readonly logger: Logger,
@InjectRepository(SysCity)
public readonly repository: Repository<SysCity>
) {
}
//#region 基础控制器
// 初始化数据
async seedData(): Promise<number> {
let result: InsertResult;
await this.repository.queryRunner?.startTransaction();
try {
await this.repository.clear();
result = await this.repository.createQueryBuilder().insert().values(citys).execute();
await this.repository.queryRunner?.commitTransaction();
await this.repository.createQueryBuilder().useTransaction(true);
return result.identifiers.length;
} catch (err) {
//如果遇到错误,可以回滚事务
await this.repository.queryRunner?.rollbackTransaction();
this.logger.warn(err);
return 0;
}
}
async getPage(
page: number = 1,
pageSize: number = 10,
keywords: string = "",
orderBy: string = ""
): Promise<[SysCity[], number]> {
const result = await this.repository.findAndCount({
where: [{ RegionName: Like("%" + keywords + "%") }, { RegionShortName: Like("%" + keywords + "%") }],
order: {
Id: "ASC"
},
skip: page - 1,
take: pageSize
})
return result;
}
async getList(
keywords: string = "",
orderBy: string = "",
): Promise<SysCity[]> {
const result = await this.repository.find({
where: [{ RegionName: Like("%" + keywords + "%") }, { RegionShortName: Like("%" + keywords + "%") }],
order: {
Id: "ASC"
},
})
return result;
}
async getById(id: number): Promise<SysCity> {
const result = await this.repository.findOne({ where: { Id: id } });
return result;
}
async getByIds(ids: number[]): Promise<SysCity[]> {
const result = await this.repository.find({
where: { Id: In(ids) },
})
return result;
}
async save(model: SysCity): Promise<SysCity> {
const result = await this.repository.save(model);
return result;
}
async destroy(id: number): Promise<DeleteResult> {
const result = await this.repository.delete(id);
return result;
}
//#endregion
}

View File

@ -1,94 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:08:52
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditSysMenuime: 2022-11-18 11:31:43
* @FilePath: \tsplatform\backend\src\Service\MenuService.ts
* @Description: 系统菜单服务
*/
import { Injectable, Logger } from '@nestjs/common';
import { SysMenu } from 'src/Entity/SysMenu';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, In, InsertResult, Like, Repository } from 'typeorm';
import { LoggerPrint } from 'src/Logging/LoggerPrint';
import { menus } from 'src/SeedData/Menu';
@Injectable()
export class MenuService {
private readonly tableName: string = "系统菜单";
constructor(
public readonly loggerPrint: LoggerPrint,
public readonly logger: Logger,
@InjectRepository(SysMenu)
public readonly repository: Repository<SysMenu>
) {
}
//#region 基础控制器
// 初始化数据
async seedData(): Promise<number> {
let result: InsertResult;
await this.repository.queryRunner?.startTransaction();
try {
await this.repository.clear();
result = await this.repository.createQueryBuilder().insert().values(menus).execute();
await this.repository.queryRunner?.commitTransaction();
await this.repository.createQueryBuilder().useTransaction(true);
return result.identifiers.length;
} catch (err) {
//如果遇到错误,可以回滚事务
await this.repository.queryRunner?.rollbackTransaction();
this.logger.warn(err);
return 0;
}
}
async getPage(
page: number = 1,
pageSize: number = 10,
keywords: string = "",
orderBy: string = ""
): Promise<[SysMenu[], number]> {
const result = await this.repository.findAndCount({
where: [{ Title: Like("%" + keywords + "%") }, { Name: Like("%" + keywords + "%") }],
order: {
Id: "DESC"
},
skip: page - 1,
take: pageSize
})
return result;
}
async getList(
keywords: string = "",
orderBy: string = ""
): Promise<SysMenu[]> {
const result = await this.repository.find({
where: [{ Name: Like("%" + keywords + "%") }],
order: {
Id: "DESC"
},
})
return result;
}
async getById(id: number): Promise<SysMenu> {
const result = await this.repository.findOne({ where: { Id: id } });
return result;
}
async getByIds(ids: number[]): Promise<SysMenu[]> {
const result = await this.repository.find({
where: { Id: In(ids) },
})
return result;
}
async save(model: SysMenu): Promise<SysMenu> {
const result = await this.repository.save(model);
return result;
}
async destroy(id: number): Promise<DeleteResult> {
const result = await this.repository.delete(id);
return result;
}
//#endregion
}

View File

@ -1,156 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:08:52
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:15:40
* @FilePath: \tsplatform\backend\src\Service\RoleService.ts
* @Description: 系统角色服务
*/
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, In, InsertResult, Like, Repository } from 'typeorm';
import { SysRole } from 'src/Entity/SysRole';
import { LoggerPrint } from 'src/Logging/LoggerPrint';
import { SysRoleMenu } from 'src/Entity/SysRoleMenu';
import { roleMenus, roles } from 'src/SeedData/Role';
// where: { Name: keywords },//and查询
// where: [{ Name: keywords }],//or查询
// where: { Name: Like("%out #%") },//like查询
// where: { Id: In([0,1]) },//in查询
// where: { Name: Like("%out #%") },//like查询
@Injectable()
export class RoleService {
private readonly tableName: string = "系统角色";
constructor(
public readonly loggerPrint: LoggerPrint,
public readonly logger: Logger,
@InjectRepository(SysRole)
public readonly repository: Repository<SysRole>,
@InjectRepository(SysRoleMenu)
public readonly roleMenuRepository: Repository<SysRoleMenu>,
) {
}
//根据角色Id获得路由菜单
async getMenusByRoleId(id: number): Promise<SysRoleMenu[]> {
//根据角色Id获得角色菜单映射数据
const result = await this.roleMenuRepository.find({
where: { RoleId: id }
});
return result;
}
//#region 基础控制器
// 初始化数据
async seedData(): Promise<number[]> {
let result: InsertResult;
let result1: InsertResult;
await this.repository.queryRunner?.startTransaction();
try {
await this.repository.clear();
result = await this.repository.createQueryBuilder().insert().values(roles).execute();
result1 = await this.roleMenuRepository.createQueryBuilder().insert().values(roleMenus).execute();
await this.repository.queryRunner?.commitTransaction();
await this.repository.createQueryBuilder().useTransaction(true);
return [result.identifiers.length, result1.identifiers.length]
} catch (err) {
//如果遇到错误,可以回滚事务
await this.repository.queryRunner?.rollbackTransaction();
this.logger.warn(err);
return [0, 0];
}
}
async getPage(
page: number = 1,
pageSize: number = 10,
keywords: string = "",
orderBy: string = ""
): Promise<[SysRole[], number]> {
const result = await this.repository.findAndCount({
where: [{ Name: Like("%" + keywords + "%") }],
order: {
Id: "ASC"
},
skip: page - 1,
take: pageSize
})
for await (let item of result[0]) {
const roleMenuResult = await this.roleMenuRepository.find({
where: { RoleId: item.Id },
})
item.SysRoleMenu = roleMenuResult;
}
return result;
}
async getList(
keywords: string = "",
orderBy: string = ""
): Promise<SysRole[]> {
const result = await this.repository.find({
where: [{ Name: Like("%" + keywords + "%") }],
order: {
Id: "ASC"
},
})
for await (let item of result) {
const roleMenuResult = await this.roleMenuRepository.find({
where: { RoleId: item.Id },
})
item.SysRoleMenu = roleMenuResult;
}
return result;
}
async getById(id: number): Promise<SysRole> {
const result = await this.repository.findOne({ where: { Id: id } });
return result;
}
async getByIds(ids: number[]): Promise<SysRole[]> {
const result = await this.repository.find({
where: { Id: In(ids) }
})
return result;
}
async save(model: SysRole): Promise<boolean> {
await this.repository.queryRunner?.startTransaction();
try {
//如果是编辑先删除主子表后,修改主表,新增子表
if (model.Id > 0) {
await this.repository.createQueryBuilder().update().set({ Name: model.Name, Remark: model.Remark, Status: model.Status }).where("Id = :Id", { Id: model.Id }).execute();
await this.roleMenuRepository.delete({ RoleId: model.Id });
for await (let item of model.SysRoleMenu) {
item.RoleId = model.Id
}
} else {
const result = await this.repository.createQueryBuilder().insert().values(model).execute();
for await (let item of model.SysRoleMenu) {
item.RoleId = result.identifiers[0].Id
}
}
await this.roleMenuRepository.createQueryBuilder().insert().values(model.SysRoleMenu).execute();
await this.repository.queryRunner?.commitTransaction();
await this.repository.createQueryBuilder().useTransaction(true);
return true;
} catch (err) {
await this.repository.queryRunner?.rollbackTransaction();
this.logger.warn(err);
return false;
}
}
async destroy(id: number): Promise<boolean> {
await this.repository.queryRunner?.startTransaction();
try {
//删除主子表
await this.repository.delete({ Id: id });
await this.roleMenuRepository.delete({ RoleId: id });
await this.repository.queryRunner?.commitTransaction();
await this.repository.createQueryBuilder().useTransaction(true);
return true;
} catch (err) {
await this.repository.queryRunner?.rollbackTransaction();
this.logger.warn(err);
return false;
}
}
//#endregion
}

View File

@ -1,93 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 17:08:52
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:15:57
* @FilePath: \tsplatform\backend\src\Service\UserService.ts
* @Description: 系统用户服务
*/
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, In, InsertResult, Like, Repository } from 'typeorm';
import { LoggerPrint } from 'src/Logging/LoggerPrint';
import { SysUser } from 'src/Entity/SysUser';
import { users } from 'src/SeedData/User';
@Injectable()
export class UserService {
private readonly tableName: string = "系统用户";
constructor(
public readonly loggerPrint: LoggerPrint,
public readonly logger: Logger,
@InjectRepository(SysUser)
public readonly repository: Repository<SysUser>
) {
}
//#region 基础控制器
// 初始化数据
async seedData(): Promise<number> {
let result: InsertResult;
await this.repository.queryRunner?.startTransaction();
try {
await this.repository.clear();
result = await this.repository.createQueryBuilder().insert().values(users).execute();
await this.repository.queryRunner?.commitTransaction();
await this.repository.createQueryBuilder().useTransaction(true);
return result.identifiers.length;
} catch (err) {
//如果遇到错误,可以回滚事务
await this.repository.queryRunner?.rollbackTransaction();
this.logger.warn(err);
return 0;
}
}
async getPage(
page: number = 1,
pageSize: number = 10,
keywords: string = "",
orderBy: string = ""
): Promise<[SysUser[], number]> {
const result = await this.repository.findAndCount({
where: [{ Phone: Like("%" + keywords + "%") }, { UserName: Like("%" + keywords + "%") }, { NickName: Like("%" + keywords + "%") }],
order: {
Id: "ASC"
},
skip: page - 1,
take: pageSize
})
return result;
}
async getList(
keywords: string = "",
orderBy: string = ""
): Promise<SysUser[]> {
const result = await this.repository.find({
where: [{ Phone: Like("%" + keywords + "%") }, { UserName: Like("%" + keywords + "%") }, { NickName: Like("%" + keywords + "%") }],
order: {
Id: "ASC"
},
})
return result;
}
async getById(id: number): Promise<SysUser> {
const result = await this.repository.findOne({ where: { Id: id } });
return result;
}
async getByIds(ids: number[]): Promise<SysUser[]> {
const result = await this.repository.find({
where: { Id: In(ids) },
})
return result;
}
async save(model: SysUser): Promise<SysUser> {
const result = await this.repository.save(model);
return result;
}
async destroy(id: number): Promise<DeleteResult> {
const result = await this.repository.delete(id);
return result;
}
//#endregion
}

View File

@ -1,50 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 11:44:48
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 17:51:54
* @FilePath: \tsplatform\backend\src\Utils\LoadModules.ts
* @Description: 全局加载模块
*/
import { Logger } from '@nestjs/common';
import { RestfulReturn } from 'src/Expand/RestfulReturn';
import { LoggerPrint } from 'src/Logging/LoggerPrint';
import { JwtService } from '@nestjs/jwt';
import { SysMenu } from 'src/Entity/SysMenu';
import { SysCity } from 'src/Entity/SysCity';
import { SysUser } from 'src/Entity/SysUser';
import { SysRole } from 'src/Entity/SysRole';
import { SysRoleMenu } from 'src/Entity/SysRoleMenu';
import { IndexController } from 'src/Controller/IndexController';
import { MenuController } from 'src/Controller/MenuController';
import { AuthController } from 'src/Controller/AuthController';
import { UserController } from 'src/Controller/UserController';
import { RoleController } from 'src/Controller/RoleController';
import { CityController } from 'src/Controller/CityController';
import { MenuService } from 'src/Service/MenuService';
import { UserService } from 'src/Service/UserService';
import { RoleService } from 'src/Service/RoleService';
import { Repository } from 'typeorm';
import { CityService } from 'src/Service/CityService';
//自动导入Entity
export function LoadPlatFormEntity() {
return [SysCity, SysMenu, SysUser, SysRole, SysRoleMenu];
}
//自动导入Controller
export function LoadController() {
return [IndexController, CityController, MenuController, UserController, AuthController, RoleController,];
}
//自动导入Service服务
export function LoadService() {
return [
//特殊服务提供者
RestfulReturn, Logger, LoggerPrint, Repository,JwtService,
//Service服务提供者
MenuService, UserService, RoleService, CityService
];
}

View File

@ -1,61 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 10:16:52
* @FilePath: \tsplatform\backend\src\app.module.ts
* @Description: 全局模块入口
*/
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoadController, LoadPlatFormEntity, LoadService } from 'src/Utils/LoadModules';
import { AppConfig, DataBaseConfig } from 'src/Config/Index';
import { JwtHandle } from 'src/Middleware/JwtHandle';
@Module({
imports: [
TypeOrmModule.forFeature(LoadPlatFormEntity()),
TypeOrmModule.forRootAsync({
useFactory: () => ({
debug: false,
//重试连接数据库的次数
retryAttempts: 10,
//两次重试连接的间隔(ms)
retryDelay: 3000,
//自动加载实体
autoLoadEntities: true,
type: 'mysql',
host: DataBaseConfig.DataBase.host,
port: DataBaseConfig.DataBase.port,
username: DataBaseConfig.DataBase.username,
password: DataBaseConfig.DataBase.password,
database: DataBaseConfig.DataBase.database,
entities: LoadPlatFormEntity(),
logging: AppConfig.debug,
synchronize: AppConfig.debug,
})
}),
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env', '.env.development', '.env.production'],
}),
],
controllers: LoadController(),
providers: LoadService()
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(JwtHandle)
.exclude(
{ path: 'auth/login', method: RequestMethod.POST },
{ path: 'auth/getUserInfo', method: RequestMethod.GET },
{ path: '/seed', method: RequestMethod.GET },
)
.forRoutes("*");
}
}

View File

@ -1,33 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 11:17:23
* @FilePath: \tsplatform\backend\src\main.ts
* @Description: 主程序入口
*/
import { NestFactory } from '@nestjs/core';
import { AppModule } from 'src/app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppConfig } from 'src/Config/Index';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { logger: ['error', 'warn', "log", "debug", "verbose"], cors: true });
//全局Dto参数校验管道
app.useGlobalPipes(new ValidationPipe());
const config = new DocumentBuilder()
//基础文档里添加安全定义
.addBearerAuth()
.setTitle('甜蜜蜜TS开发总平台')
.setDescription('甜蜜蜜TS开发总平台')
.setContact("甜蜜蜜", "https://gitee.com/tmm-top/vue-next-admin-ts", "490912587@qq.com")
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('/api', app, document);
await app.listen(AppConfig.port, '0.0.0.0');
}
bootstrap();

View File

@ -1,24 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
}

View File

@ -1,21 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2022",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

9
doc/.gitignore vendored
View File

@ -1,9 +0,0 @@
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

25
frontend/.gitignore vendored
View File

@ -1,25 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env
.env.development
.env.production
*.lock
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
package-lock.json
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,4 +0,0 @@
declare module 'vue-grid-layout';
declare module 'qrcodejs2-fixes';
declare module 'splitpanes';
declare module 'js-cookie';

13
frontend/shim.d.ts vendored
View File

@ -1,13 +0,0 @@
/* eslint-disable */
// 声明文件,*.vue 后缀的文件交给 vue 模块来处理
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
// 声明文件,定义全局变量。其它 app.config.globalProperties.xxx使用 getCurrentInstance() 来获取
interface Window {
nextLoading: boolean;
}

View File

@ -1,6 +0,0 @@
declare module '*.json';
declare module '*.png';
declare module '*.jpg';
declare module '*.scss';
declare module '*.ts';
declare module '*.js';

View File

@ -1,107 +0,0 @@
<!--
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 17:25:54
* @FilePath: \frontend\src\App.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
<router-view v-show="themeConfig.lockScreenTime > 1" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
<CloseFull v-if="!themeConfig.isLockScreen" />
</el-config-provider>
</template>
<script lang="ts">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, defineComponent, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
import setIntroduction from '/@/utils/setIconfont';
export default defineComponent({
name: 'app',
components: {
LockScreen: defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue')),
Setings: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue')),
CloseFull: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue')),
},
setup() {
const { messages, locale } = useI18n();
const setingsRef = ref();
const route = useRoute();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 获取全局组件大小
const getGlobalComponentSize = computed(() => {
return other.globalComponentSize();
});
// 获取全局 i18n
const getGlobalI18n = computed(() => {
return messages.value[locale.value];
});
// 设置初始化,防止刷新时恢复默认
onBeforeMount(() => {
// 设置批量第三方 icon 图标
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
});
// 页面加载时
onMounted(() => {
nextTick(() => {
// 监听布局配置弹窗点击打开
mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer();
});
// 获取缓存中的布局配置
if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig(Local.get('themeConfig'));
document.documentElement.style.cssText = Local.get('themeConfigStyle');
}
// 获取缓存中的全屏配置
if (Session.get('isTagsViewCurrenFull')) {
stores.setCurrenFullscreen(Session.get('isTagsViewCurrenFull'));
}
});
});
// 页面销毁时,关闭监听布局配置/i18n监听
onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {});
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,
() => {
other.useTitle();
},
{
deep: true,
}
);
return {
themeConfig,
setingsRef,
getGlobalComponentSize,
getGlobalI18n,
};
},
});
</script>
<style>
.searchBox {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
</style>

View File

@ -1,23 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 09:43:23
* @FilePath: \tsplatform\frontend\src\api\menu\index.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '/@/utils/request';
export function getPage(params: object) {
return request({
url: '/city/getPage',
method: 'get',
params
});
}
export function getList(params: object) {
return request({
url: '/city/getList',
method: 'get',
params
});
}

View File

@ -1,34 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 17:01:13
* @FilePath: \tsplatform\frontend\src\api\login\index.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '/@/utils/request';
/**
* 登录api接口集合
* @method signIn 用户登录
* @method signOut 用户退出登录
*/
export function signIn(params: object) {
return request({
url: '/auth/login',
method: 'post',
data: params,
});
}
export function getUserInfo() {
return request({
url: '/auth/getUserInfo',
method: 'get'
});
}
export function signOut() {
return request({
url: '/auth/loginOut',
method: 'get'
});
}

View File

@ -1,51 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-24 00:24:11
* @FilePath: \tsplatform\frontend\src\api\menu\index.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '/@/utils/request';
export function getAuthMenu(params: object) {
return request({
url: '/menu/getAuthMenuTree',
method: 'get',
params,
});
}
export function getMenuTree(params: object) {
return request({
url: '/menu/getMenuTree',
method: 'get',
params,
});
}
export function add(params: object) {
return request({
url: '/menu/add',
method: 'post',
data: params
});
}
export function edit(params: object) {
return request({
url: '/menu/edit',
method: 'post',
data: params
});
}
export function del(params: object) {
return request({
url: '/menu/del',
method: 'post',
data: params
});
}
export function setStatus(params: object) {
return request({
url: '/menu/setStatus',
method: 'post',
data: params
});
}

View File

@ -1,52 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 21:35:46
* @FilePath: \tsplatform\frontend\src\api\menu\index.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '/@/utils/request';
export function getPage(params: object) {
return request({
url: '/role/getPage',
method: 'get',
params
});
}
export function getList(params: object) {
return request({
url: '/role/getList',
method: 'get',
params
});
}
export function add(params: object) {
return request({
url: '/role/add',
method: 'post',
data: params
});
}
export function edit(params: object) {
return request({
url: '/role/edit',
method: 'post',
data: params
});
}
export function del(params: object) {
return request({
url: '/role/del',
method: 'post',
data: params
});
}
export function setStatus(params: object) {
return request({
url: '/role/setStatus',
method: 'post',
data: params
});
}

View File

@ -1,51 +0,0 @@
/*
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-23 21:35:53
* @FilePath: \tsplatform\frontend\src\api\menu\index.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '/@/utils/request';
export function getPage(params: object) {
return request({
url: '/user/getPage',
method: 'get',
params
});
}
export function getList(params: object) {
return request({
url: '/role/getList',
method: 'get',
params
});
}
export function add(params: object) {
return request({
url: '/user/add',
method: 'post',
data: params
});
}
export function edit(params: object) {
return request({
url: '/user/edit',
method: 'post',
data: params
});
}
export function del(params: object) {
return request({
url: '/user/del',
method: 'post',
data: params
});
}
export function setStatus(params: object) {
return request({
url: '/user/setStatus',
method: 'post',
data: params
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,30 +0,0 @@
<template>
<slot v-if="getUserAuthBtnList" />
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo';
export default defineComponent({
name: 'auth',
props: {
value: {
type: String,
default: () => '',
},
},
setup(props) {
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 vuex 中的用户权限
const getUserAuthBtnList = computed(() => {
return userInfos.value.authBtnList.some((v: string) => v === props.value);
});
return {
getUserAuthBtnList,
};
},
});
</script>

View File

@ -1,31 +0,0 @@
<template>
<slot v-if="getUserAuthBtnList" />
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo';
import { judementSameArr } from '/@/utils/arrayOperation';
export default defineComponent({
name: 'authAll',
props: {
value: {
type: Array,
default: () => [],
},
},
setup(props) {
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 pinia 中的用户权限
const getUserAuthBtnList = computed(() => {
return judementSameArr(props.value, userInfos.value.authBtnList);
});
return {
getUserAuthBtnList,
};
},
});
</script>

View File

@ -1,36 +0,0 @@
<template>
<slot v-if="getUserAuthBtnList" />
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo';
export default defineComponent({
name: 'auths',
props: {
value: {
type: Array,
default: () => [],
},
},
setup(props) {
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 vuex 中的用户权限
const getUserAuthBtnList = computed(() => {
let flag = false;
userInfos.value.authBtnList.map((val: string) => {
props.value.map((v) => {
if (val === v) flag = true;
});
});
return flag;
});
return {
getUserAuthBtnList,
};
},
});
</script>

View File

@ -1,88 +0,0 @@
<template>
<div class="editor-container">
<Toolbar :editor="editorRef" :mode="mode" />
<Editor :mode="mode" :defaultConfig="editorConfig" :style="{ height }" v-model="editorVal" @onCreated="handleCreated" @onChange="handleChange" />
</div>
</template>
<script lang="ts">
// https://www.wangeditor.com/v5/for-frame.html#vue3
import '@wangeditor/editor/dist/css/style.css';
import { defineComponent, reactive, toRefs, shallowRef, watch, onBeforeUnmount } from 'vue';
import { Toolbar, Editor } from '@wangeditor/editor-for-vue';
export default defineComponent({
name: 'wngEditor',
components: { Toolbar, Editor },
props: {
// 是否禁用
disable: {
type: Boolean,
default: () => true,
},
// 内容框默认 placeholder
placeholder: {
type: String,
default: () => '请输入内容...',
},
// 双向绑定:双向绑定值,字段名为固定,改了之后将不生效
// 参考https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
modelValue: String,
// https://www.wangeditor.com/v5/getting-started.html#mode-%E6%A8%A1%E5%BC%8F
// 模式,可选 <default|simple>,默认 default
mode: {
type: String,
default: () => 'default',
},
// 高度
height: {
type: String,
default: () => '310px',
},
},
setup(props, { emit }) {
const editorRef = shallowRef();
const state = reactive({
editorConfig: {
placeholder: props.placeholder,
},
editorVal: props.modelValue,
});
// 编辑器回调函数
const handleCreated = (editor: any) => {
editorRef.value = editor;
};
// 编辑器内容改变时
const handleChange = (editor: any) => {
// console.log(editor.getText());
// console.log(editor.getHtml());
emit('update:modelValue', editor.getHtml());
};
// 监听是否禁用改变
// https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I
watch(
() => props.disable,
(bool) => {
const editor = editorRef.value;
if (editor == null) return;
bool ? editor.disable() : editor.enable();
},
{
deep: true,
}
);
// 页面销毁时
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
return {
editorRef,
handleCreated,
handleChange,
...toRefs(state),
};
},
});
</script>

View File

@ -1,252 +0,0 @@
<template>
<div class="icon-selector w100 h100">
<el-popover
placement="bottom"
:width="fontIconWidth"
trigger="click"
transition="el-zoom-in-top"
popper-class="icon-selector-popper"
@show="onPopoverShow"
>
<template #reference>
<el-input
v-model="fontIconSearch"
:placeholder="fontIconPlaceholder"
:clearable="clearable"
:disabled="disabled"
:size="size"
ref="inputWidthRef"
@clear="onClearFontIcon"
@focus="onIconFocus"
@blur="onIconBlur"
>
<template #prepend>
<SvgIcon
:name="fontIconPrefix === '' ? prepend : fontIconPrefix"
class="font14"
v-if="fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : fontIconPrefix?.indexOf('ele-') > -1"
/>
<i v-else :class="fontIconPrefix === '' ? prepend : fontIconPrefix" class="font14"></i>
</template>
</el-input>
</template>
<template #default>
<div class="icon-selector-warp">
<div class="icon-selector-warp-title flex">
<div class="flex-auto">{{ title }}</div>
<div class="icon-selector-warp-title-tab" v-if="type === 'all'">
<span :class="{ 'span-active': fontIconType === 'ali' }" @click="onIconChange('ali')" class="ml10" title="iconfont 图标">ali</span>
<span :class="{ 'span-active': fontIconType === 'ele' }" @click="onIconChange('ele')" class="ml10" title="elementPlus 图标">ele</span>
<span :class="{ 'span-active': fontIconType === 'awe' }" @click="onIconChange('awe')" class="ml10" title="fontawesome 图标">awe</span>
</div>
</div>
<div class="icon-selector-warp-row">
<el-scrollbar ref="selectorScrollbarRef">
<el-row :gutter="10" v-if="fontIconSheetsFilterList.length > 0">
<el-col :xs="6" :sm="4" :md="4" :lg="4" :xl="4" @click="onColClick(v)" v-for="(v, k) in fontIconSheetsFilterList" :key="k">
<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': fontIconPrefix === v }">
<div class="flex-margin">
<div class="icon-selector-warp-item-value">
<SvgIcon :name="v" />
</div>
</div>
</div>
</el-col>
</el-row>
<el-empty :image-size="100" v-if="fontIconSheetsFilterList.length <= 0" :description="emptyDescription"></el-empty>
</el-scrollbar>
</div>
</div>
</template>
</el-popover>
</div>
</template>
<script lang="ts">
import { ref, toRefs, reactive, onMounted, nextTick, computed, watch, defineComponent } from 'vue';
import initIconfont from '/@/utils/getStyleSheets';
export default defineComponent({
name: 'iconSelector',
emits: ['update:modelValue', 'get', 'clear'],
props: {
// 输入框前置内容
prepend: {
type: String,
default: () => 'ele-Pointer',
},
// 输入框占位文本
placeholder: {
type: String,
default: () => '请输入内容搜索图标或者选择图标',
},
// 输入框占位文本
size: {
type: String,
default: () => 'default',
},
// 弹窗标题
title: {
type: String,
default: () => '请选择图标',
},
// icon 图标类型
type: {
type: String,
default: () => 'ele',
},
// 禁用
disabled: {
type: Boolean,
default: () => false,
},
// 是否可清空
clearable: {
type: Boolean,
default: () => true,
},
// 自定义空状态描述文字
emptyDescription: {
type: String,
default: () => '无相关图标',
},
// 双向绑定值,默认为 modelValue
// 参考https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
// 参考https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
modelValue: String,
},
setup(props, { emit }) {
const inputWidthRef = ref();
const selectorScrollbarRef = ref();
const state = reactive({
fontIconPrefix: '',
fontIconWidth: 0,
fontIconSearch: '',
fontIconTabsIndex: 0,
fontIconSheetsList: [],
fontIconPlaceholder: '',
fontIconType: 'ali',
fontIconShow: true,
});
// 处理 input 获取焦点时modelValue 有值时,改变 input 的 placeholder 值
const onIconFocus = () => {
if (!props.modelValue) return false;
state.fontIconSearch = '';
state.fontIconPlaceholder = props.modelValue;
};
// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
const onIconBlur = () => {
setTimeout(() => {
const icon = state.fontIconSheetsList.filter((icon: string) => icon === state.fontIconSearch);
if (icon.length <= 0) state.fontIconSearch = '';
}, 300);
};
// 处理 icon 双向绑定数值回显
const initModeValueEcho = () => {
if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder);
(<string | undefined>state.fontIconPlaceholder) = props.modelValue;
(<string | undefined>state.fontIconPrefix) = props.modelValue;
};
// 处理 icon type 类型为 all 时,类型 ali、ele、awe 回显问题
const initFontIconTypeEcho = () => {
if ((<any>props.modelValue)?.indexOf('iconfont') > -1) onIconChange('ali');
else if ((<any>props.modelValue)?.indexOf('ele-') > -1) onIconChange('ele');
else if ((<any>props.modelValue)?.indexOf('fa') > -1) onIconChange('awe');
else onIconChange('ali');
};
// 图标搜索及图标数据显示
const fontIconSheetsFilterList = computed(() => {
if (!state.fontIconSearch) return state.fontIconSheetsList;
let search = state.fontIconSearch.trim().toLowerCase();
return state.fontIconSheetsList.filter((item: any) => {
if (item.toLowerCase().indexOf(search) !== -1) return item;
});
});
// 获取 input 的宽度
const getInputWidth = () => {
nextTick(() => {
state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
});
};
// 监听页面宽度改变
const initResize = () => {
window.addEventListener('resize', () => {
getInputWidth();
});
};
// 初始化数据
const initFontIconData = async (type: string) => {
state.fontIconSheetsList = [];
if (type === 'ali') {
await initIconfont.ali().then((res: any) => {
// 阿里字体图标使用 `iconfont xxx`
state.fontIconSheetsList = res.map((i: string) => `iconfont ${i}`);
});
} else if (type === 'ele') {
await initIconfont.ele().then((res: any) => {
state.fontIconSheetsList = res;
});
} else if (type === 'awe') {
await initIconfont.awe().then((res: any) => {
// fontawesome字体图标使用 `fa xxx`
state.fontIconSheetsList = res.map((i: string) => `fa ${i}`);
});
}
// 初始化 input 的 placeholder
// 参考单项数据流https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
state.fontIconPlaceholder = props.placeholder;
// 初始化双向绑定回显
initModeValueEcho();
};
// 图标点击切换
const onIconChange = (type: string) => {
state.fontIconType = type;
initFontIconData(type);
};
// 获取当前点击的 icon 图标
const onColClick = (v: any) => {
state.fontIconPlaceholder = v;
state.fontIconPrefix = v;
emit('get', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
};
// 清空当前点击的 icon 图标
const onClearFontIcon = () => {
state.fontIconPrefix = '';
emit('clear', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
};
// 监听 Popover 打开,用于双向绑定值回显
const onPopoverShow = () => {
initModeValueEcho();
initFontIconTypeEcho();
};
// 页面加载时
onMounted(() => {
initModeValueEcho();
initResize();
getInputWidth();
});
// 监听双向绑定 modelValue 的变化
watch(
() => props.modelValue,
() => {
initModeValueEcho();
}
);
return {
inputWidthRef,
selectorScrollbarRef,
fontIconSheetsFilterList,
onColClick,
onIconChange,
onClearFontIcon,
onIconFocus,
onIconBlur,
onPopoverShow,
...toRefs(state),
};
},
});
</script>

View File

@ -1,195 +0,0 @@
<template>
<div class="notice-bar" :style="{ background, height: `${height}px` }" v-show="!isMode">
<div class="notice-bar-warp" :style="{ color, fontSize: `${size}px` }">
<i v-if="leftIcon" class="notice-bar-warp-left-icon" :class="leftIcon"></i>
<div class="notice-bar-warp-text-box" ref="noticeBarWarpRef">
<div class="notice-bar-warp-text" ref="noticeBarTextRef" v-if="!scrollable">{{ text }}</div>
<div class="notice-bar-warp-slot" v-else><slot /></div>
</div>
<SvgIcon :name="rightIcon" v-if="rightIcon" class="notice-bar-warp-right-icon" @click="onRightIconClick" />
</div>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, ref, onMounted, nextTick } from 'vue';
export default defineComponent({
name: 'noticeBar',
props: {
// 通知栏模式,可选值为 closeable link
mode: {
type: String,
default: () => '',
},
// 通知文本内容
text: {
type: String,
default: () => '',
},
// 通知文本颜色
color: {
type: String,
default: () => 'var(--el-color-warning)',
},
// 通知背景色
background: {
type: String,
default: () => 'var(--el-color-warning-light-9)',
},
// 字体大小单位px
size: {
type: [Number, String],
default: () => 14,
},
// 通知栏高度单位px
height: {
type: Number,
default: () => 40,
},
// 动画延迟时间 (s)
delay: {
type: Number,
default: () => 1,
},
// 滚动速率 (px/s)
speed: {
type: Number,
default: () => 100,
},
// 是否开启垂直滚动
scrollable: {
type: Boolean,
default: () => false,
},
// 自定义左侧图标
leftIcon: {
type: String,
default: () => '',
},
// 自定义右侧图标
rightIcon: {
type: String,
default: () => '',
},
},
setup(props, { emit }) {
const noticeBarWarpRef = ref();
const noticeBarTextRef = ref();
const state = reactive({
order: 1,
oneTime: 0,
twoTime: 0,
warpOWidth: 0,
textOWidth: 0,
isMode: false,
});
// 初始化 animation 各项参数
const initAnimation = () => {
nextTick(() => {
state.warpOWidth = noticeBarWarpRef.value.offsetWidth;
state.textOWidth = noticeBarTextRef.value.offsetWidth;
document.styleSheets[0].insertRule(`@keyframes oneAnimation {0% {left: 0px;} 100% {left: -${state.textOWidth}px;}}`);
document.styleSheets[0].insertRule(`@keyframes twoAnimation {0% {left: ${state.warpOWidth}px;} 100% {left: -${state.textOWidth}px;}}`);
computeAnimationTime();
setTimeout(() => {
changeAnimation();
}, props.delay * 1000);
});
};
// 计算 animation 滚动时长
const computeAnimationTime = () => {
state.oneTime = state.textOWidth / props.speed;
state.twoTime = (state.textOWidth + state.warpOWidth) / props.speed;
};
// 改变 animation 动画调用
const changeAnimation = () => {
if (state.order === 1) {
noticeBarTextRef.value.style.cssText = `animation: oneAnimation ${state.oneTime}s linear; opactity: 1;}`;
state.order = 2;
} else {
noticeBarTextRef.value.style.cssText = `animation: twoAnimation ${state.twoTime}s linear infinite; opacity: 1;`;
}
};
// 监听 animation 动画的结束
const listenerAnimationend = () => {
noticeBarTextRef.value.addEventListener(
'animationend',
() => {
changeAnimation();
},
false
);
};
// 右侧 icon 图标点击
const onRightIconClick = () => {
if (!props.mode) return false;
if (props.mode === 'closeable') {
state.isMode = true;
emit('close');
} else if (props.mode === 'link') {
emit('link');
}
};
// 页面加载时
onMounted(() => {
if (props.scrollable) return false;
initAnimation();
listenerAnimationend();
});
return {
noticeBarWarpRef,
noticeBarTextRef,
onRightIconClick,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.notice-bar {
padding: 0 15px;
width: 100%;
border-radius: 4px;
.notice-bar-warp {
display: flex;
align-items: center;
width: 100%;
height: inherit;
.notice-bar-warp-text-box {
flex: 1;
height: inherit;
display: flex;
align-items: center;
overflow: hidden;
position: relative;
.notice-bar-warp-text {
white-space: nowrap;
position: absolute;
left: 0;
}
.notice-bar-warp-slot {
width: 100%;
white-space: nowrap;
:deep(.el-carousel__item) {
display: flex;
align-items: center;
}
}
}
.notice-bar-warp-left-icon {
width: 24px;
font-size: inherit !important;
}
.notice-bar-warp-right-icon {
width: 24px;
text-align: right;
font-size: inherit !important;
&:hover {
cursor: pointer;
}
}
}
}
</style>

View File

@ -1,73 +0,0 @@
<template>
<i v-if="isShowIconSvg" class="el-icon" :style="setIconSvgStyle">
<component :is="getIconName" />
</i>
<div v-else-if="isShowIconImg" :style="setIconImgOutStyle">
<img :src="getIconName" :style="setIconSvgInsStyle" />
</div>
<i v-else :class="getIconName" :style="setIconSvgStyle" />
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
export default defineComponent({
name: 'svgIcon',
props: {
// svg 图标组件名字
name: {
type: String,
},
// svg 大小
size: {
type: Number,
default: () => 14,
},
// svg 颜色
color: {
type: String,
},
},
setup(props) {
// 在线链接、本地引入地址前缀
const linesString = ['https', 'http', '/src', '/assets', import.meta.env.VITE_PUBLIC_PATH];
// 获取 icon 图标名称
const getIconName = computed(() => {
return props?.name;
});
// 用于判断 element plus 自带 svg 图标的显示、隐藏
const isShowIconSvg = computed(() => {
return props?.name?.startsWith('ele-');
});
// 用于判断在线链接、本地引入等图标显示、隐藏
const isShowIconImg = computed(() => {
return linesString.find((str) => props.name?.startsWith(str));
});
// 设置图标样式
const setIconSvgStyle = computed(() => {
return `font-size: ${props.size}px;color: ${props.color};`;
});
// 设置图片样式
const setIconImgOutStyle = computed(() => {
return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
});
// 设置图片样式
// https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0
const setIconSvgInsStyle = computed(() => {
const filterStyle: string[] = [];
const compatibles: string[] = ['-webkit', '-ms', '-o', '-moz'];
compatibles.forEach((j) => filterStyle.push(`${j}-filter: drop-shadow(${props.color} 30px 0);`));
return `width: ${props.size}px;height: ${props.size}px;position: relative;left: -${props.size}px;${filterStyle.join('')}`;
});
return {
getIconName,
isShowIconSvg,
isShowIconImg,
setIconSvgStyle,
setIconImgOutStyle,
setIconSvgInsStyle,
};
},
});
</script>

View File

@ -1,68 +0,0 @@
import { createI18n } from 'vue-i18n';
import pinia from '/@/stores/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn';
import enLocale from 'element-plus/lib/locale/lang/en';
import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw';
import nextZhcn from '/@/i18n/lang/zh-cn';
import nextEn from '/@/i18n/lang/en';
import nextZhtw from '/@/i18n/lang/zh-tw';
import pagesLoginZhcn from '/@/i18n/pages/login/zh-cn';
import pagesLoginEn from '/@/i18n/pages/login/en';
import pagesLoginZhtw from '/@/i18n/pages/login/zh-tw';
import pagesFormI18nZhcn from '/@/i18n/pages/formI18n/zh-cn';
import pagesFormI18nEn from '/@/i18n/pages/formI18n/en';
import pagesFormI18nZhtw from '/@/i18n/pages/formI18n/zh-tw';
// 定义语言国际化内容
/**
* 说明:
* /src/i18n/lang 下的 ts 为框架的国际化内容
* /src/i18n/pages 下的 ts 为各界面的国际化内容
*/
const messages = {
[zhcnLocale.name]: {
...zhcnLocale,
message: {
...nextZhcn,
...pagesLoginZhcn,
...pagesFormI18nZhcn,
},
},
[enLocale.name]: {
...enLocale,
message: {
...nextEn,
...pagesLoginEn,
...pagesFormI18nEn,
},
},
[zhtwLocale.name]: {
...zhtwLocale,
message: {
...nextZhtw,
...pagesLoginZhtw,
...pagesFormI18nZhtw,
},
},
};
// 读取 pinia 默认语言
const stores = useThemeConfig(pinia);
const { themeConfig } = storeToRefs(stores);
// 导出语言国际化
// https://vue-i18n.intlify.dev/guide/essentials/fallback.html#explicit-fallback-with-one-locale
export const i18n = createI18n({
legacy: false,
silentTranslationWarn: true,
missingWarn: false,
silentFallbackWarn: true,
fallbackWarn: false,
locale: themeConfig.value.globalI18n,
fallbackLocale: zhcnLocale.name,
messages,
});

View File

@ -1,165 +0,0 @@
<template>
<div class="h100" v-show="!isTagsViewCurrenFull">
<el-aside class="layout-aside" :class="setCollapseStyle">
<Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef" @mouseenter="onAsideEnterLeave(true)" @mouseleave="onAsideEnterLeave(false)">
<Vertical :menuList="menuList" />
</el-scrollbar>
</el-aside>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, toRefs, reactive, computed, watch, onBeforeMount, defineComponent, ref } from 'vue';
import { storeToRefs } from 'pinia';
import pinia from '/@/stores/index';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import mittBus from '/@/utils/mitt';
export default defineComponent({
name: 'layoutAside',
components: {
Logo: defineAsyncComponent(() => import('/@/layout/logo/index.vue')),
Vertical: defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue')),
},
setup() {
const layoutAsideScrollbarRef = ref();
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes();
const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
const state = reactive({
menuList: [],
clientWidth: 0,
});
// 设置菜单展开/收起时的宽度
const setCollapseStyle = computed(() => {
const { layout, isCollapse, menuBar } = themeConfig.value;
const asideBrTheme = ['#FFFFFF', '#FFF', '#fff', '#ffffff'];
const asideBrColor = asideBrTheme.includes(menuBar) ? 'layout-el-aside-br-color' : '';
// 判断是否是手机端
if (state.clientWidth <= 1000) {
if (isCollapse) {
document.body.setAttribute('class', 'el-popup-parent--hidden');
const asideEle = document.querySelector('.layout-container') as HTMLElement;
const modeDivs = document.createElement('div');
modeDivs.setAttribute('class', 'layout-aside-mobile-mode');
asideEle.appendChild(modeDivs);
modeDivs.addEventListener('click', closeLayoutAsideMobileMode);
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-open'];
} else {
// 关闭弹窗
closeLayoutAsideMobileMode();
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-close'];
}
} else {
if (layout === 'columns') {
// 分栏布局,菜单收起时宽度给 1px
if (isCollapse) return [asideBrColor, 'layout-aside-pc-1'];
else return [asideBrColor, 'layout-aside-pc-220'];
} else {
// 其它布局给 64px
if (isCollapse) return [asideBrColor, 'layout-aside-pc-64'];
else return [asideBrColor, 'layout-aside-pc-220'];
}
}
});
// 关闭移动端蒙版
const closeLayoutAsideMobileMode = () => {
const el = document.querySelector('.layout-aside-mobile-mode');
el?.setAttribute('style', 'animation: error-img-two 0.3s');
setTimeout(() => {
el?.parentNode?.removeChild(el);
}, 300);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', '');
};
// 设置显示/隐藏 logo
const setShowLogo = computed(() => {
let { layout, isShowLogo } = themeConfig.value;
return (isShowLogo && layout === 'defaults') || (isShowLogo && layout === 'columns');
});
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
if (themeConfig.value.layout === 'columns') return false;
(state.menuList as any) = filterRoutesFun(routesList.value);
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<string>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 设置菜单导航是否固定(移动端)
const initMenuFixed = (clientWidth: number) => {
state.clientWidth = clientWidth;
};
// 鼠标移入、移出
const onAsideEnterLeave = (bool: Boolean) => {
let { layout } = themeConfig.value;
if (layout !== 'columns') return false;
if (!bool) mittBus.emit('restoreDefault');
stores.setColumnsMenuHover(bool);
};
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(themeConfig.value, (val) => {
if (val.isShowLogoChange !== val.isShowLogo) {
if (layoutAsideScrollbarRef.value) layoutAsideScrollbarRef.value.update();
}
});
// 监听vuex值的变化动态赋值给菜单中
watch(
pinia.state,
(val) => {
let { layout, isClassicSplitMenu } = val.themeConfig.themeConfig;
if (layout === 'classic' && isClassicSplitMenu) return false;
setFilterRoutes();
},
{
deep: true,
}
);
// 页面加载前
onBeforeMount(() => {
initMenuFixed(document.body.clientWidth);
setFilterRoutes();
// 此界面不需要取消监听(mittBus.off('setSendColumnsChildren))
// 因为切换布局时有的监听需要使用,取消了监听,某些操作将不生效
mittBus.on('setSendColumnsChildren', (res: any) => {
state.menuList = res.children;
});
mittBus.on('setSendClassicChildren', (res: any) => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = [];
state.menuList = res.children;
}
});
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
mittBus.on('layoutMobileResize', (res: any) => {
initMenuFixed(res.clientWidth);
closeLayoutAsideMobileMode();
});
});
return {
layoutAsideScrollbarRef,
setCollapseStyle,
setShowLogo,
isTagsViewCurrenFull,
onAsideEnterLeave,
...toRefs(state),
};
},
});
</script>

View File

@ -1,296 +0,0 @@
<template>
<div class="layout-columns-aside">
<el-scrollbar>
<ul @mouseleave="onColumnsAsideMenuMouseleave()">
<li
v-for="(v, k) in columnsAsideList"
:key="k"
@click="onColumnsAsideMenuClick(v, k)"
@mouseenter="onColumnsAsideMenuMouseenter(v, k)"
:ref="
(el) => {
if (el) columnsAsideOffsetTopRefs[k] = el;
}
"
:class="{ 'layout-columns-active': liIndex === k, 'layout-columns-hover': liHoverIndex === k }"
:title="$t(v.meta.title)"
>
<div :class="themeConfig.columnsAsideLayout" v-if="!v.meta.isLink || (v.meta.isLink && v.meta.isIframe)">
<SvgIcon :name="v.meta.icon" />
<div class="columns-vertical-title font12">
{{
$t(v.meta.title) && $t(v.meta.title).length >= 4
? $t(v.meta.title).substr(0, themeConfig.columnsAsideLayout === 'columns-vertical' ? 4 : 3)
: $t(v.meta.title)
}}
</div>
</div>
<div :class="themeConfig.columnsAsideLayout" v-else>
<a :href="v.meta.isLink" target="_blank">
<SvgIcon :name="v.meta.icon" />
<div class="columns-vertical-title font12">
{{
$t(v.meta.title) && $t(v.meta.title).length >= 4
? $t(v.meta.title).substr(0, themeConfig.columnsAsideLayout === 'columns-vertical' ? 4 : 3)
: $t(v.meta.title)
}}
</div>
</a>
</div>
</li>
<div ref="columnsAsideActiveRef" :class="themeConfig.columnsAsideStyle"></div>
</ul>
</el-scrollbar>
</div>
</template>
<script lang="ts">
import { reactive, toRefs, ref, onMounted, nextTick, watch, onUnmounted, defineComponent } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
import { storeToRefs } from 'pinia';
import pinia from '/@/stores/index';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import mittBus from '/@/utils/mitt';
// 定义接口来定义对象的类型
interface ColumnsAsideState {
columnsAsideList: any[];
liIndex: number;
liOldIndex: null | number;
liHoverIndex: null | number;
liOldPath: null | string;
difference: number;
routeSplit: string[];
}
export default defineComponent({
name: 'layoutColumnsAside',
setup() {
const columnsAsideOffsetTopRefs: any = ref([]);
const columnsAsideActiveRef = ref();
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { routesList, isColumnsMenuHover, isColumnsNavHover } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const router = useRouter();
const state = reactive<ColumnsAsideState>({
columnsAsideList: [],
liIndex: 0,
liOldIndex: null,
liHoverIndex: null,
liOldPath: null,
difference: 0,
routeSplit: [],
});
// 设置菜单高亮位置移动
const setColumnsAsideMove = (k: number) => {
state.liIndex = k;
columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
};
// 菜单高亮点击事件
const onColumnsAsideMenuClick = (v: Object, k: number) => {
setColumnsAsideMove(k);
let { path, redirect } = v as any;
if (redirect) router.push(redirect);
else router.push(path);
};
// 鼠标移入时,显示当前的子级菜单
const onColumnsAsideMenuMouseenter = (v: RouteRecordRaw, k: number) => {
if (!themeConfig.value.isColumnsMenuHoverPreload) return false;
let { path } = v;
state.liOldPath = path;
state.liOldIndex = k;
state.liHoverIndex = k;
mittBus.emit('setSendColumnsChildren', setSendChildren(path));
stores.setColumnsMenuHover(false);
stores.setColumnsNavHover(true);
};
// 鼠标移走时,显示原来的子级菜单
const onColumnsAsideMenuMouseleave = async () => {
await stores.setColumnsNavHover(false);
// 添加延时器,防止拿到的 store.state.routesList 值不是最新的
setTimeout(() => {
if (!isColumnsMenuHover && !isColumnsNavHover) mittBus.emit('restoreDefault');
}, 100);
};
// 设置高亮动态位置
const onColumnsAsideDown = (k: number) => {
nextTick(() => {
setColumnsAsideMove(k);
});
};
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
state.columnsAsideList = filterRoutesFun(routesList.value);
const resData: any = setSendChildren(route.path);
if (Object.keys(resData).length <= 0) return false;
onColumnsAsideDown(resData.item[0].k);
mittBus.emit('setSendColumnsChildren', resData);
};
// 传送当前子级数据到菜单中
const setSendChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: any = {};
state.columnsAsideList.map((v: any, k: number) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = [{ ...v }];
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<string>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// tagsView 点击时,根据路由查找下标 columnsAsideList实现左侧菜单高亮
const setColumnsMenuHighlight = (path: string) => {
state.routeSplit = path.split('/');
state.routeSplit.shift();
const routeFirst = `/${state.routeSplit[0]}`;
const currentSplitRoute = state.columnsAsideList.find((v: any) => v.path === routeFirst);
if (!currentSplitRoute) return false;
// 延迟拿值,防止取不到
setTimeout(() => {
onColumnsAsideDown((<any>currentSplitRoute).k);
}, 0);
};
// 监听布局配置信息的变化,动态增加菜单高亮位置移动像素
watch(
pinia.state,
(val) => {
val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
if (!val.routesList.isColumnsMenuHover && !val.routesList.isColumnsNavHover) {
state.liHoverIndex = null;
mittBus.emit('setSendColumnsChildren', setSendChildren(route.path));
} else {
state.liHoverIndex = state.liOldIndex;
if (!state.liOldPath) return false;
mittBus.emit('setSendColumnsChildren', setSendChildren(state.liOldPath));
}
},
{
deep: true,
}
);
// 页面加载时
onMounted(() => {
setFilterRoutes();
// 销毁变量,防止鼠标再次移入时,保留了上次的记录
mittBus.on('restoreDefault', () => {
state.liOldIndex = null;
state.liOldPath = null;
});
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('restoreDefault', () => {});
});
// 路由更新时
onBeforeRouteUpdate((to) => {
setColumnsMenuHighlight(to.path);
mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
});
return {
themeConfig,
columnsAsideOffsetTopRefs,
columnsAsideActiveRef,
onColumnsAsideDown,
onColumnsAsideMenuClick,
onColumnsAsideMenuMouseenter,
onColumnsAsideMenuMouseleave,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.layout-columns-aside {
width: 70px;
height: 100%;
background: var(--next-bg-columnsMenuBar);
ul {
position: relative;
.layout-columns-active {
color: var(--next-bg-columnsMenuBarColor) !important;
transition: 0.3s ease-in-out;
}
.layout-columns-hover {
color: var(--el-color-primary);
a {
color: var(--el-color-primary);
}
}
li {
color: var(--next-bg-columnsMenuBarColor);
width: 100%;
height: 50px;
text-align: center;
display: flex;
cursor: pointer;
position: relative;
z-index: 1;
&:hover {
@extend .layout-columns-hover;
}
.columns-vertical {
margin: auto;
.columns-vertical-title {
padding-top: 1px;
}
}
.columns-horizontal {
display: flex;
height: 50px;
width: 100%;
align-items: center;
padding: 0 5px;
i {
margin-right: 3px;
}
a {
display: flex;
.columns-horizontal-title {
padding-top: 1px;
}
}
}
a {
text-decoration: none;
color: var(--next-bg-columnsMenuBarColor);
}
}
.columns-round {
background: var(--el-color-primary);
color: var(--el-color-white);
position: absolute;
left: 50%;
top: 2px;
height: 44px;
width: 65px;
transform: translateX(-50%);
z-index: 0;
transition: 0.3s ease-in-out;
border-radius: 5px;
}
.columns-card {
@extend .columns-round;
top: 0;
height: 50px;
width: 100%;
border-radius: 0;
}
}
}
</style>

View File

@ -1,25 +0,0 @@
<template>
<el-header class="layout-header" v-show="!isTagsViewCurrenFull">
<NavBarsIndex />
</el-header>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
export default defineComponent({
name: 'layoutHeader',
components: {
NavBarsIndex: defineAsyncComponent(() => import('/@/layout/navBars/index.vue')),
},
setup() {
const storesTagsViewRoutes = useTagsViewRoutes();
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
return {
isTagsViewCurrenFull,
};
},
});
</script>

View File

@ -1,59 +0,0 @@
<template>
<el-main class="layout-main" :style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
<el-scrollbar ref="layoutMainScrollbarRef" class="layout-main-scroll" wrap-class="layout-main-scroll" view-class="layout-main-scroll">
<LayoutParentView />
<LayoutFooter v-if="isFooter" />
</el-scrollbar>
<el-backtop target=".layout-backtop .el-scrollbar__wrap" />
</el-main>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent, onMounted, computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading';
export default defineComponent({
name: 'layoutMain',
components: {
LayoutParentView: defineAsyncComponent(() => import('/@/layout/routerView/parent.vue')),
LayoutFooter: defineAsyncComponent(() => import('/@/layout/footer/index.vue')),
},
setup() {
const layoutMainScrollbarRef = ref('');
const route = useRoute();
const storesTagsViewRoutes = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
// 设置 footer 显示/隐藏
const isFooter = computed(() => {
return themeConfig.value.isFooter && !route.meta.isIframe;
});
// 设置 header 固定
const isFixedHeader = computed(() => {
return themeConfig.value.isFixedHeader;
});
// 设置主内容区的高度
const setMainHeight = computed(() => {
if (isTagsViewCurrenFull.value) return '0px';
const { isTagsview, layout } = themeConfig.value;
if (isTagsview && layout !== 'classic') return '85px';
else return '51px';
});
// 页面加载前
onMounted(() => {
NextLoading.done(600);
});
return {
layoutMainScrollbarRef,
isFooter,
isFixedHeader,
setMainHeight,
};
},
});
</script>

View File

@ -1,38 +0,0 @@
<!--
* @Author: '490912587@qq.com' '490912587@qq.com'
* @Date: 2022-11-17 09:19:20
* @LastEditors: '490912587@qq.com' '490912587@qq.com'
* @LastEditTime: 2022-11-22 13:43:03
* @FilePath: \frontend\src\layout\footer\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="layout-footer pb15">
<div class="layout-footer-warp">
<div>TsPlatformMade by 490912587@qq.com with </div>
<div class="mt5">甜蜜蜜版权所有</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'layoutFooter',
});
</script>
<style scoped lang="scss">
.layout-footer {
width: 100%;
display: flex;
&-warp {
margin: auto;
color: var(--el-text-color-secondary);
text-align: center;
animation: error-num 0.3s ease;
}
}
</style>

View File

@ -1,54 +0,0 @@
<template>
<component :is="themeConfig.layout" />
</template>
<script lang="ts">
import { onBeforeMount, onUnmounted, defineComponent, defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { Local } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
export default defineComponent({
name: 'layout',
components: {
defaults: defineAsyncComponent(() => import('/@/layout/main/defaults.vue')),
classic: defineAsyncComponent(() => import('/@/layout/main/classic.vue')),
transverse: defineAsyncComponent(() => import('/@/layout/main/transverse.vue')),
columns: defineAsyncComponent(() => import('/@/layout/main/columns.vue')),
},
setup() {
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 窗口大小改变时(适配移动端)
const onLayoutResize = () => {
if (!Local.get('oldLayout')) Local.set('oldLayout', themeConfig.value.layout);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) {
themeConfig.value.isCollapse = false;
mittBus.emit('layoutMobileResize', {
layout: 'defaults',
clientWidth,
});
} else {
mittBus.emit('layoutMobileResize', {
layout: Local.get('oldLayout') ? Local.get('oldLayout') : themeConfig.value.layout,
clientWidth,
});
}
};
// 页面加载前
onBeforeMount(() => {
onLayoutResize();
window.addEventListener('resize', onLayoutResize);
});
// 页面卸载时
onUnmounted(() => {
window.removeEventListener('resize', onLayoutResize);
});
return {
themeConfig,
};
},
});
</script>

View File

@ -1,77 +0,0 @@
<template>
<el-container class="layout-container flex-center">
<LayoutHeader />
<el-container class="layout-mian-height-50">
<LayoutAside />
<div class="flex-center layout-backtop">
<LayoutTagsView v-if="isTagsview" />
<LayoutMain ref="layoutMainRef" />
</div>
</el-container>
</el-container>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent, computed, ref, watch, nextTick, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
export default defineComponent({
name: 'layoutClassic',
components: {
LayoutAside: defineAsyncComponent(() => import('/@/layout/component/aside.vue')),
LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
LayoutTagsView: defineAsyncComponent(() => import('/@/layout/navBars/tagsView/tagsView.vue')),
},
setup() {
const layoutMainRef = ref<any>('');
const route = useRoute();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 判断是否显示 tasgview
const isTagsview = computed(() => {
return themeConfig.value.isTagsview;
});
// 重置滚动条高度,更新子级 scrollbar
const updateScrollbar = () => {
layoutMainRef.value.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
}, 500);
});
};
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
// 页面加载时
onMounted(() => {
initScrollBarHeight();
});
return {
layoutMainRef,
isTagsview,
};
},
});
</script>

View File

@ -1,78 +0,0 @@
<template>
<el-container class="layout-container">
<ColumnsAside />
<el-container class="layout-columns-warp layout-container-view h100">
<LayoutAside />
<el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
<LayoutHeader />
<LayoutMain ref="layoutMainRef" />
</el-scrollbar>
</el-container>
</el-container>
</template>
<script lang="ts">
import { defineAsyncComponent, watch, defineComponent, onMounted, nextTick, ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
export default defineComponent({
name: 'layoutColumns',
components: {
LayoutAside: defineAsyncComponent(() => import('/@/layout/component/aside.vue')),
LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
ColumnsAside: defineAsyncComponent(() => import('/@/layout/component/columnsAside.vue')),
},
setup() {
const layoutScrollbarRef = ref<any>('');
const layoutMainRef = ref<any>('');
const route = useRoute();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 重置滚动条高度
const updateScrollbar = () => {
// 更新父级 scrollbar
layoutScrollbarRef.value.update();
// 更新子级 scrollbar
layoutMainRef.value.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutScrollbarRef.value.wrap$.scrollTop = 0;
layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
}, 500);
});
};
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
// 页面加载时
onMounted(() => {
initScrollBarHeight();
});
return {
layoutScrollbarRef,
layoutMainRef,
};
},
});
</script>

View File

@ -1,78 +0,0 @@
<template>
<el-container class="layout-container">
<LayoutAside />
<el-container class="layout-container-view h100">
<el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
<LayoutHeader />
<LayoutMain ref="layoutMainRef" />
</el-scrollbar>
</el-container>
</el-container>
</template>
<script lang="ts">
import { defineAsyncComponent, watch, defineComponent, onMounted, nextTick, ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading';
export default defineComponent({
name: 'layoutDefaults',
components: {
LayoutAside: defineAsyncComponent(() => import('/@/layout/component/aside.vue')),
LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
},
setup() {
const layoutScrollbarRef = ref<any>('');
const layoutMainRef = ref<any>('');
const route = useRoute();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 重置滚动条高度
const updateScrollbar = () => {
// 更新父级 scrollbar
layoutScrollbarRef.value.update();
// 更新子级 scrollbar
layoutMainRef.value.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutScrollbarRef.value.wrap$.scrollTop = 0;
layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
}, 500);
});
};
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
// 页面加载时
onMounted(() => {
initScrollBarHeight();
NextLoading.done(600);
});
return {
layoutScrollbarRef,
layoutMainRef,
};
},
});
</script>

View File

@ -1,64 +0,0 @@
<template>
<el-container class="layout-container flex-center layout-backtop">
<LayoutHeader />
<LayoutMain ref="layoutMainRef" />
</el-container>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent, ref, watch, nextTick, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
export default defineComponent({
name: 'layoutTransverse',
components: {
LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
},
setup() {
const layoutMainRef = ref<any>('');
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
// 重置滚动条高度,更新子级 scrollbar
const updateScrollbar = () => {
layoutMainRef.value.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
}, 500);
});
};
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
// 页面加载时
onMounted(() => {
initScrollBarHeight();
});
return {
layoutMainRef,
};
},
});
</script>

View File

@ -1,163 +0,0 @@
<template>
<div v-if="isShowBreadcrumb" class="layout-navbars-breadcrumb">
<SvgIcon
class="layout-navbars-breadcrumb-icon"
:name="themeConfig.isCollapse ? 'ele-Expand' : 'ele-Fold'"
:size="16"
@click="onThemeConfigChange"
/>
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(v, k) in breadcrumbList" :key="!v.meta.tagsViewName ? v.meta.title : v.meta.tagsViewName">
<span v-if="k === breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
<div v-if="!v.meta.tagsViewName">{{ $t(v.meta.title) }}</div>
<div v-else>{{ v.meta.tagsViewName }}</div>
</span>
<a v-else @click.prevent="onBreadcrumbClick(v)">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />{{ $t(v.meta.title) }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, computed, onMounted, defineComponent } from 'vue';
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import { Local } from '/@/utils/storage';
import other from '/@/utils/other';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useRoutesList } from '/@/stores/routesList';
// 定义接口来定义对象的类型
interface BreadcrumbState {
breadcrumbList: Array<any>;
routeSplit: Array<string>;
routeSplitFirst: string;
routeSplitIndex: number;
}
export default defineComponent({
name: 'layoutBreadcrumb',
setup() {
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { routesList } = storeToRefs(stores);
const route = useRoute();
const router = useRouter();
const state = reactive<BreadcrumbState>({
breadcrumbList: [],
routeSplit: [],
routeSplitFirst: '',
routeSplitIndex: 1,
});
// 动态设置经典、横向布局不显示
const isShowBreadcrumb = computed(() => {
initRouteSplit(route.path);
const { layout, isBreadcrumb } = themeConfig.value;
if (layout === 'classic' || layout === 'transverse') return false;
else return isBreadcrumb ? true : false;
});
// 面包屑点击时
const onBreadcrumbClick = (v: any) => {
const { redirect, path } = v;
if (redirect) router.push(redirect);
else router.push(path);
};
// 展开/收起左侧菜单点击
const onThemeConfigChange = () => {
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
setLocalThemeConfig();
};
// 存储布局配置
const setLocalThemeConfig = () => {
Local.remove('themeConfig');
Local.set('themeConfig', themeConfig.value);
};
// 处理面包屑数据
const getBreadcrumbList = (arr: Array<string>) => {
arr.forEach((item: any) => {
state.routeSplit.forEach((v: any, k: number, arrs: any) => {
if (state.routeSplitFirst === item.path) {
state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
state.breadcrumbList.push(item);
state.routeSplitIndex++;
if (item.children) getBreadcrumbList(item.children);
}
});
});
};
// 当前路由字符串切割成数组,并删除第一项空内容
const initRouteSplit = (path: string) => {
if (!themeConfig.value.isBreadcrumb) return false;
state.breadcrumbList = [routesList.value[0]];
state.routeSplit = path.split('/');
state.routeSplit.shift();
state.routeSplitFirst = `/${state.routeSplit[0]}`;
state.routeSplitIndex = 1;
getBreadcrumbList(routesList.value);
if (route.name === 'home' || (route.name === 'notFound' && state.breadcrumbList.length > 1)) state.breadcrumbList.shift();
if (state.breadcrumbList.length > 0) state.breadcrumbList[state.breadcrumbList.length - 1].meta.tagsViewName = other.setTagsViewNameI18n(route);
};
// 页面加载时
onMounted(() => {
initRouteSplit(route.path);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
initRouteSplit(to.path);
});
return {
onThemeConfigChange,
isShowBreadcrumb,
themeConfig,
onBreadcrumbClick,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb {
flex: 1;
height: inherit;
display: flex;
align-items: center;
.layout-navbars-breadcrumb-icon {
cursor: pointer;
font-size: 18px;
color: var(--next-bg-topBarColor);
height: 100%;
width: 40px;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
.layout-navbars-breadcrumb-span {
display: flex;
opacity: 0.7;
color: var(--next-bg-topBarColor);
}
.layout-navbars-breadcrumb-iconfont {
font-size: 14px;
margin-right: 5px;
}
:deep(.el-breadcrumb__separator) {
opacity: 0.7;
color: var(--next-bg-topBarColor);
}
:deep(.el-breadcrumb__inner a, .el-breadcrumb__inner.is-link) {
font-weight: unset !important;
color: var(--next-bg-topBarColor);
&:hover {
color: var(--el-color-primary) !important;
}
}
}
</style>

View File

@ -1,120 +0,0 @@
<template>
<div class="layout-navbars-breadcrumb-index">
<Logo v-if="setIsShowLogo" />
<Breadcrumb />
<Horizontal :menuList="menuList" v-if="isLayoutTransverse" />
<User />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, computed, reactive, toRefs, onMounted, onUnmounted, defineComponent } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import mittBus from '/@/utils/mitt';
// 定义接口来定义对象的类型
interface IndexState {
menuList: object[];
}
export default defineComponent({
name: 'layoutBreadcrumbIndex',
components: {
Breadcrumb: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/breadcrumb.vue')),
User: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/user.vue')),
Logo: defineAsyncComponent(() => import('/@/layout/logo/index.vue')),
Horizontal: defineAsyncComponent(() => import('/@/layout/navMenu/horizontal.vue')),
},
setup() {
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { routesList } = storeToRefs(stores);
const route = useRoute();
const state = reactive<IndexState>({
menuList: [],
});
// 设置 logo 显示/隐藏
const setIsShowLogo = computed(() => {
let { isShowLogo, layout } = themeConfig.value;
return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse');
});
// 设置是否显示横向导航菜单
const isLayoutTransverse = computed(() => {
let { layout, isClassicSplitMenu } = themeConfig.value;
return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic');
});
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = delClassicChildren(filterRoutesFun(routesList.value));
const resData = setSendClassicChildren(route.path);
mittBus.emit('setSendClassicChildren', resData);
} else {
state.menuList = filterRoutesFun(routesList.value);
}
};
// 设置了分割菜单时,删除底下 children
const delClassicChildren = (arr: Array<object>) => {
arr.map((v: any) => {
if (v.children) delete v.children;
});
return arr;
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<string>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 传送当前子级数据到菜单中
const setSendClassicChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: any = {};
filterRoutesFun(routesList.value).map((v, k) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = [{ ...v }];
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 页面加载时
onMounted(() => {
setFilterRoutes();
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('getBreadcrumbIndexSetFilterRoutes', () => {});
});
return {
setIsShowLogo,
isLayoutTransverse,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb-index {
height: 50px;
display: flex;
align-items: center;
background: var(--next-bg-topBar);
border-bottom: 1px solid var(--next-border-color-light);
}
</style>

View File

@ -1,143 +0,0 @@
<template>
<div class="layout-search-dialog">
<el-dialog v-model="isShowSearch" destroy-on-close :show-close="false">
<template #footer>
<el-autocomplete
v-model="menuQuery"
:fetch-suggestions="menuSearch"
:placeholder="$t('message.user.searchPlaceholder')"
ref="layoutMenuAutocompleteRef"
@select="onHandleSelect"
:fit-input-width="true"
>
<template #prefix>
<el-icon class="el-input__icon">
<ele-Search />
</el-icon>
</template>
<template #default="{ item }">
<div>
<SvgIcon :name="item.meta.icon" class="mr5" />
{{ $t(item.meta.title) }}
</div>
</template>
</el-autocomplete>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { reactive, toRefs, defineComponent, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
// 定义接口来定义对象的类型
interface SearchState {
isShowSearch: boolean;
menuQuery: string;
tagsViewList: object[];
}
interface Restaurant {
path: string;
meta: {
title: string;
};
}
export default defineComponent({
name: 'layoutBreadcrumbSearch',
setup() {
const storesTagsViewRoutes = useTagsViewRoutes();
const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
const layoutMenuAutocompleteRef = ref();
const { t } = useI18n();
const router = useRouter();
const state = reactive<SearchState>({
isShowSearch: false,
menuQuery: '',
tagsViewList: [],
});
// 搜索弹窗打开
const openSearch = () => {
state.menuQuery = '';
state.isShowSearch = true;
initTageView();
nextTick(() => {
setTimeout(() => {
layoutMenuAutocompleteRef.value.focus();
});
});
};
// 搜索弹窗关闭
const closeSearch = () => {
state.isShowSearch = false;
};
// 菜单搜索数据过滤
const menuSearch = (queryString: string, cb: Function) => {
let results = queryString ? state.tagsViewList.filter(createFilter(queryString)) : state.tagsViewList;
cb(results);
};
// 菜单搜索过滤
const createFilter: any = (queryString: string) => {
return (restaurant: Restaurant) => {
return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
t(restaurant.meta.title).indexOf(queryString.toLowerCase()) > -1
);
};
};
// 初始化菜单数据
const initTageView = () => {
if (state.tagsViewList.length > 0) return false;
tagsViewRoutes.value.map((v: any) => {
if (!v.meta.isHide) state.tagsViewList.push({ ...v });
});
};
// 当前菜单选中时
const onHandleSelect = (item: any) => {
let { path, redirect } = item;
if (item.meta.isLink && !item.meta.isIframe) window.open(item.meta.isLink);
else if (redirect) router.push(redirect);
else router.push(path);
closeSearch();
};
return {
layoutMenuAutocompleteRef,
openSearch,
closeSearch,
menuSearch,
onHandleSelect,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.layout-search-dialog {
position: relative;
:deep(.el-dialog) {
.el-dialog__header,
.el-dialog__body {
display: none;
}
.el-dialog__footer {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -53vh;
}
}
:deep(.el-autocomplete) {
width: 560px;
position: absolute;
top: 150px;
left: 50%;
transform: translateX(-50%);
}
}
</style>

View File

@ -1,289 +0,0 @@
<template>
<div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
<div class="layout-navbars-breadcrumb-user-icon">
<i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="large" :disabled="disabledSize === 'large'">{{
$t('message.user.dropdownLarge')
}}</el-dropdown-item>
<el-dropdown-item command="default" :disabled="disabledSize === 'default'">{{
$t('message.user.dropdownDefault')
}}</el-dropdown-item>
<el-dropdown-item command="small" :disabled="disabledSize === 'small'">{{
$t('message.user.dropdownSmall')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange">
<div class="layout-navbars-breadcrumb-user-icon">
<i class="iconfont" :class="disabledI18n === 'en' ? 'icon-fuhao-yingwen' : 'icon-fuhao-zhongwen'"
:title="$t('message.user.title1')"></i>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh-cn" :disabled="disabledI18n === 'zh-cn'">简体中文</el-dropdown-item>
<el-dropdown-item command="en" :disabled="disabledI18n === 'en'">English</el-dropdown-item>
<el-dropdown-item command="zh-tw" :disabled="disabledI18n === 'zh-tw'">繁體中文</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<el-icon :title="$t('message.user.title2')">
<ele-Search />
</el-icon>
</div>
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div>
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :is-dot="true">
<el-icon :title="$t('message.user.title4')">
<ele-Bell />
</el-icon>
</el-badge>
</template>
<template #default>
<UserNews />
</template>
</el-popover>
</div>
<div class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick">
<i class="iconfont" :title="isScreenfull ? $t('message.user.title6') : $t('message.user.title5')"
:class="!isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"></i>
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link">
<img :src="userInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfos.userName === '' ? 'common' : userInfos.userName }}
<el-icon class="el-icon--right">
<ele-ArrowDown />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<Search ref="searchRef" />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, ref, computed, reactive, toRefs, onMounted, defineComponent } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage, ElNotification } from 'element-plus';
import screenfull from 'screenfull';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo';
import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage';
import { signOut } from '/@/api/login';
export default defineComponent({
name: 'layoutBreadcrumbUser',
components: {
UserNews: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue')),
Search: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue')),
},
setup() {
const { locale, t } = useI18n();
const router = useRouter();
const stores = useUserInfo();
const storesThemeConfig = useThemeConfig();
const { userInfos } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const searchRef = ref();
const state = reactive({
isScreenfull: false,
disabledI18n: 'zh-cn',
disabledSize: 'large',
});
// 设置分割样式
const layoutUserFlexNum = computed(() => {
let num: string | number = '';
const { layout, isClassicSplitMenu } = themeConfig.value;
const layoutArr: string[] = ['defaults', 'columns'];
if (layoutArr.includes(layout) || (layout === 'classic' && !isClassicSplitMenu)) num = '1';
else num = '';
return num;
});
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('暂不不支持全屏');
return false;
}
screenfull.toggle();
screenfull.on('change', () => {
if (screenfull.isFullscreen) state.isScreenfull = true;
else state.isScreenfull = false;
});
};
// 布局配置 icon 点击时
const onLayoutSetingClick = () => {
mittBus.emit('openSetingsDrawer');
};
// 下拉菜单点击时
const onHandleCommandClick = (path: string) => {
if (path === 'logOut') {
ElMessageBox({
closeOnClickModal: false,
closeOnPressEscape: false,
title: t('message.user.logOutTitle'),
message: t('message.user.logOutMessage'),
showCancelButton: true,
confirmButtonText: t('message.user.logOutConfirm'),
cancelButtonText: t('message.user.logOutCancel'),
buttonSize: 'default',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
instance.confirmButtonText = t('message.user.logOutExit');
setTimeout(() => {
done();
setTimeout(() => {
instance.confirmButtonLoading = false;
}, 300);
}, 700);
} else {
done();
}
},
})
.then(async () => {
const result = await signOut();
if (result.data.code === 200) {
ElNotification({
message: result.data.msg,
type: 'success',
duration: 500,
onClose() {
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
},
})
}
})
.catch(() => { });
} else if (path === 'wareHouse') {
window.open('https://gitee.com/tmm-top/vue-next-admin-ts');
} else {
router.push(path);
}
};
// 菜单搜索点击
const onSearchClick = () => {
searchRef.value.openSearch();
};
// 组件大小改变
const onComponentSizeChange = (size: string) => {
Local.remove('themeConfig');
themeConfig.value.globalComponentSize = size;
Local.set('themeConfig', themeConfig.value);
initI18nOrSize('globalComponentSize', 'disabledSize');
window.location.reload();
};
// 语言切换
const onLanguageChange = (lang: string) => {
Local.remove('themeConfig');
themeConfig.value.globalI18n = lang;
Local.set('themeConfig', themeConfig.value);
locale.value = lang;
other.useTitle();
initI18nOrSize('globalI18n', 'disabledI18n');
};
// 初始化组件大小/i18n
const initI18nOrSize = (value: string, attr: string) => {
(<any>state)[attr] = Local.get('themeConfig')[value];
};
// 页面加载时
onMounted(() => {
if (Local.get('themeConfig')) {
initI18nOrSize('globalComponentSize', 'disabledSize');
initI18nOrSize('globalI18n', 'disabledI18n');
}
});
return {
userInfos,
onLayoutSetingClick,
onHandleCommandClick,
onScreenfullClick,
onSearchClick,
onComponentSizeChange,
onLanguageChange,
searchRef,
layoutUserFlexNum,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb-user {
display: flex;
align-items: center;
justify-content: flex-end;
&-link {
height: 100%;
display: flex;
align-items: center;
white-space: nowrap;
&-photo {
width: 25px;
height: 25px;
border-radius: 100%;
}
}
&-icon {
padding: 0 10px;
cursor: pointer;
color: var(--next-bg-topBarColor);
height: 50px;
line-height: 50px;
display: flex;
align-items: center;
&:hover {
background: var(--next-color-user-hover);
i {
display: inline-block;
animation: logoAnimation 0.3s ease-in-out;
}
}
}
:deep(.el-dropdown) {
color: var(--next-bg-topBarColor);
}
:deep(.el-badge) {
height: 40px;
line-height: 40px;
display: flex;
align-items: center;
}
:deep(.el-badge__content.is-fixed) {
top: 12px;
}
}
</style>

View File

@ -1,41 +0,0 @@
<template>
<div class="layout-navbars-container">
<BreadcrumbIndex />
<TagsView v-if="setShowTagsView" />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, computed, defineComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
export default defineComponent({
name: 'layoutNavBars',
components: {
BreadcrumbIndex: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/index.vue')),
TagsView: defineAsyncComponent(() => import('/@/layout/navBars/tagsView/tagsView.vue')),
},
setup() {
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 是否显示 tagsView
const setShowTagsView = computed(() => {
let { layout, isTagsview } = themeConfig.value;
return layout !== 'classic' && isTagsview;
});
return {
setShowTagsView,
};
},
});
</script>
<style scoped lang="scss">
.layout-navbars-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
</style>

View File

@ -1,138 +0,0 @@
<template>
<transition name="el-zoom-in-center">
<div
aria-hidden="true"
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:key="Math.random()"
v-show="isShow"
>
<ul class="el-dropdown-menu">
<template v-for="(v, k) in dropdownList">
<li
class="el-dropdown-menu__item"
aria-disabled="false"
tabindex="-1"
:key="k"
v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
>
<SvgIcon :name="v.icon" />
<span>{{ $t(v.txt) }}</span>
</li>
</template>
</ul>
<div class="el-popper__arrow" :style="{ left: `${arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, onMounted, onUnmounted, watch } from 'vue';
export default defineComponent({
name: 'layoutTagsViewContextmenu',
props: {
dropdown: {
type: Object,
default: () => {
return {
x: 0,
y: 0,
};
},
},
},
setup(props, { emit }) {
const state = reactive({
isShow: false,
dropdownList: [
{ contextMenuClickId: 0, txt: 'message.tagsView.refresh', affix: false, icon: 'ele-RefreshRight' },
{ contextMenuClickId: 1, txt: 'message.tagsView.close', affix: false, icon: 'ele-Close' },
{ contextMenuClickId: 2, txt: 'message.tagsView.closeOther', affix: false, icon: 'ele-CircleClose' },
{ contextMenuClickId: 3, txt: 'message.tagsView.closeAll', affix: false, icon: 'ele-FolderDelete' },
{
contextMenuClickId: 4,
txt: 'message.tagsView.fullscreen',
affix: false,
icon: 'iconfont icon-fullscreen',
},
],
item: {},
arrowLeft: 10,
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', Object.assign({}, { contextMenuClickId }, state.item));
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
item.meta.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
closeContextmenu();
setTimeout(() => {
state.isShow = true;
}, 10);
};
// 关闭右键菜单
const closeContextmenu = () => {
state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
},
{
deep: true,
}
);
return {
dropdowns,
openContextmenu,
closeContextmenu,
onCurrentContextmenuClick,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.custom-contextmenu {
transform-origin: center top;
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;
i {
font-size: 12px !important;
}
}
}
</style>

View File

@ -1,769 +0,0 @@
<template>
<div class="layout-navbars-tagsview" :class="{ 'layout-navbars-tagsview-shadow': getThemeConfig.layout === 'classic' }">
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
<li
v-for="(v, k) in tagsViewList"
:key="k"
class="layout-navbars-tagsview-ul-li"
:data-url="v.url"
:class="{ 'is-active': isActive(v) }"
@contextmenu.prevent="onContextmenu(v, $event)"
@mousedown="onMousedownMenu(v, $event)"
@click="onTagsClick(v, k)"
:ref="
(el) => {
if (el) tagsRefs[k] = el;
}
"
>
<i class="iconfont icon-webicon318 layout-navbars-tagsview-ul-li-iconfont" v-if="isActive(v)"></i>
<SvgIcon :name="v.meta.icon" v-if="!isActive(v) && getThemeConfig.isTagsviewIcon" class="pr5" />
<span>{{ setTagsViewNameI18n(v) }}</span>
<template v-if="isActive(v)">
<SvgIcon
name="ele-RefreshRight"
class="ml5 layout-navbars-tagsview-ul-li-refresh"
@click.stop="refreshCurrentTagsView($route.fullPath)"
/>
<SvgIcon
name="ele-Close"
class="layout-navbars-tagsview-ul-li-icon layout-icon-active"
v-if="!v.meta.isAffix"
@click.stop="closeCurrentTagsView(getThemeConfig.isShareTagsView ? v.path : v.url)"
/>
</template>
<SvgIcon
name="ele-Close"
class="layout-navbars-tagsview-ul-li-icon layout-icon-three"
v-if="!v.meta.isAffix"
@click.stop="closeCurrentTagsView(getThemeConfig.isShareTagsView ? v.path : v.url)"
/>
</li>
</ul>
</el-scrollbar>
<Contextmenu :dropdown="dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
</div>
</template>
<script lang="ts">
import {
defineAsyncComponent,
toRefs,
reactive,
onMounted,
computed,
ref,
nextTick,
onBeforeUpdate,
onBeforeMount,
onUnmounted,
watch,
defineComponent,
} from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import Sortable from 'sortablejs';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import pinia from '/@/stores/index';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useKeepALiveNames } from '/@/stores/keepAliveNames';
import { Session } from '/@/utils/storage';
import { isObjectValueEqual } from '/@/utils/arrayOperation';
import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
// 定义接口来定义对象的类型
interface TagsViewState {
routeActive: string;
routePath: string | unknown;
dropdown: {
x: string | number;
y: string | number;
};
sortable: any;
tagsRefsIndex: number;
tagsViewList: any[];
tagsViewRoutesList: any[];
}
interface RouteParams {
path: string;
url: string;
query: object;
params: object;
}
interface CurrentContextmenu {
meta: {
isDynamic: boolean;
};
params: any;
query: any;
path: string;
contextMenuClickId: string | number;
}
export default defineComponent({
name: 'layoutTagsView',
components: {
Contextmenu: defineAsyncComponent(() => import('/@/layout/navBars/tagsView/contextmenu.vue')),
},
setup() {
const tagsRefs = ref<any[]>([]);
const scrollbarRef = ref();
const contextmenuRef = ref();
const tagsUlRef = ref();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
const storesKeepALiveNames = useKeepALiveNames();
const route = useRoute();
const router = useRouter();
const state = reactive<TagsViewState>({
routeActive: '',
routePath: route.path,
dropdown: { x: '', y: '' },
sortable: '',
tagsRefsIndex: 0,
tagsViewList: [],
tagsViewRoutesList: [],
});
// 动态设置 tagsView 风格样式
const setTagsStyle = computed(() => {
return themeConfig.value.tagsStyle;
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
});
// 设置 自定义 tagsView 名称、 自定义 tagsView 名称国际化
const setTagsViewNameI18n = computed(() => {
return (v: any) => {
return other.setTagsViewNameI18n(v);
};
});
// 设置 tagsView 高亮
const isActive = (v: RouteParams) => {
if (getThemeConfig.value.isShareTagsView) {
return v.path === state.routePath;
} else {
if ((v.query && Object.keys(v.query).length) || (v.params && Object.keys(v.params).length)) {
// 普通传参
return v.url ? v.url === state.routeActive : v.path === state.routeActive;
} else {
// 通过 name 传参params 取值,刷新页面参数消失
// https://gitee.com/lyt-top/vue-next-admin/issues/I51RS9
return v.path === state.routePath;
}
}
};
// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
const addBrowserSetSession = (tagsViewList: Array<object>) => {
Session.set('tagsViewList', tagsViewList);
};
// 获取 vuex 中的 tagsViewRoutes 列表
const getTagsViewRoutes = async () => {
state.routeActive = await setTagsViewHighlight(route);
state.routePath = (await route.meta.isDynamic) ? route.meta.isDynamicPath : route.path;
state.tagsViewList = [];
state.tagsViewRoutesList = tagsViewRoutes.value;
initTagsView();
};
// vuex 中获取路由信息如果是设置了固定的isAffix进行初始化显示
const initTagsView = async () => {
if (Session.get('tagsViewList') && getThemeConfig.value.isCacheTagsView) {
state.tagsViewList = await Session.get('tagsViewList');
} else {
await state.tagsViewRoutesList.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) {
v.url = setTagsViewHighlight(v);
state.tagsViewList.push({ ...v });
storesKeepALiveNames.addCachedView(v);
}
});
await addTagsView(route.path, route);
}
// 初始化当前元素(li)的下标
getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
};
// 处理可开启多标签详情单标签详情动态路由xxx/:id/:name"),普通路由处理)
const solveAddTagsView = async (path: string, to?: any) => {
let isDynamicPath = to.meta.isDynamic ? to.meta.isDynamicPath : path;
let current = state.tagsViewList.filter(
(v: any) =>
v.path === isDynamicPath &&
isObjectValueEqual(
to.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
to.meta.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null
)
);
if (current.length <= 0) {
// 防止Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead.
let findItem = state.tagsViewRoutesList.find((v: any) => v.path === isDynamicPath);
if (!findItem) return false;
if (findItem.meta.isAffix) return false;
if (findItem.meta.isLink && !findItem.meta.isIframe) return false;
to.meta.isDynamic ? (findItem.params = to.params) : (findItem.query = to.query);
findItem.url = setTagsViewHighlight(findItem);
state.tagsViewList.push({ ...findItem });
await storesKeepALiveNames.addCachedView(findItem);
addBrowserSetSession(state.tagsViewList);
}
};
// 处理单标签时,第二次的值未覆盖第一次的 tagsViewList 值Session Storage
const singleAddTagsView = (path: string, to?: any) => {
let isDynamicPath = to.meta.isDynamic ? to.meta.isDynamicPath : path;
state.tagsViewList.forEach((v) => {
if (
v.path === isDynamicPath &&
!isObjectValueEqual(
to.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
to.meta.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null
)
) {
to.meta.isDynamic ? (v.params = to.params) : (v.query = to.query);
v.url = setTagsViewHighlight(v);
addBrowserSetSession(state.tagsViewList);
}
});
};
// 1、添加 tagsView未设置隐藏isHide也添加到在 tagsView 中(可开启多标签详情,单标签详情)
const addTagsView = (path: string, to?: any) => {
// 防止拿取不到路由信息
nextTick(async () => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
let item: any = '';
if (to && to.meta.isDynamic) {
// 动态路由xxx/:id/:name"):参数不同,开启多个 tagsview
if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
else await singleAddTagsView(path, to);
if (state.tagsViewList.some((v: any) => v.path === to.meta.isDynamicPath)) {
// 防止首次进入界面时(登录进入) tagsViewList 不存浏览器中
addBrowserSetSession(state.tagsViewList);
return false;
}
item = state.tagsViewRoutesList.find((v: any) => v.path === to.meta.isDynamicPath);
} else {
// 普通路由:参数不同,开启多个 tagsview
if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
else await singleAddTagsView(path, to);
if (state.tagsViewList.some((v: any) => v.path === path)) {
// 防止首次进入界面时(登录进入) tagsViewList 不存浏览器中
addBrowserSetSession(state.tagsViewList);
return false;
}
item = state.tagsViewRoutesList.find((v: any) => v.path === path);
}
if (!item) return false;
if (item.meta.isLink && !item.meta.isIframe) return false;
if (to && to.meta.isDynamic) item.params = to?.params ? to?.params : route.params;
else item.query = to?.query ? to?.query : route.query;
item.url = setTagsViewHighlight(item);
await storesKeepALiveNames.addCachedView(item);
await state.tagsViewList.push({ ...item });
await addBrowserSetSession(state.tagsViewList);
});
};
// 2、刷新当前 tagsView
const refreshCurrentTagsView = async (fullPath: string) => {
const decodeURIPath = decodeURI(fullPath);
let item: any = {};
state.tagsViewList.forEach((v: any) => {
v.transUrl = transUrlParams(v);
if (v.transUrl) {
if (v.transUrl === transUrlParams(v)) item = v;
} else {
if (v.path === decodeURIPath) item = v;
}
});
if (!item) return false;
await storesKeepALiveNames.delCachedView(item);
mittBus.emit('onTagsViewRefreshRouterView', fullPath);
if (item.meta.isKeepAlive) storesKeepALiveNames.addCachedView(item);
};
// 3、关闭当前 tagsView如果是设置了固定的isAffix不可以关闭
const closeCurrentTagsView = (path: string) => {
state.tagsViewList.map((v: any, k: number, arr: any) => {
if (!v.meta.isAffix) {
if (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path) {
storesKeepALiveNames.delCachedView(v);
state.tagsViewList.splice(k, 1);
setTimeout(() => {
if (state.tagsViewList.length === k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
// 最后一个且高亮时
if (arr[arr.length - 1].meta.isDynamic) {
// 动态路由xxx/:id/:name"
if (k !== arr.length) router.push({ name: arr[k].name, params: arr[k].params });
else router.push({ name: arr[arr.length - 1].name, params: arr[arr.length - 1].params });
} else {
// 普通路由
if (k !== arr.length) router.push({ path: arr[k].path, query: arr[k].query });
else router.push({ path: arr[arr.length - 1].path, query: arr[arr.length - 1].query });
}
} else {
// 非最后一个且高亮时,跳转到下一个
if (state.tagsViewList.length !== k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
if (arr[k].meta.isDynamic) {
// 动态路由xxx/:id/:name"
router.push({ name: arr[k].name, params: arr[k].params });
} else {
// 普通路由
router.push({ path: arr[k].path, query: arr[k].query });
}
}
}
}, 0);
}
}
});
addBrowserSetSession(state.tagsViewList);
};
// 4、关闭其它 tagsView如果是设置了固定的isAffix不进行关闭
const closeOtherTagsView = (path: string) => {
if (Session.get('tagsViewList')) {
state.tagsViewList = [];
Session.get('tagsViewList').map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) {
v.url = setTagsViewHighlight(v);
storesKeepALiveNames.delOthersCachedViews(v);
state.tagsViewList.push({ ...v });
}
});
addTagsView(path, route);
addBrowserSetSession(state.tagsViewList);
}
};
// 5、关闭全部 tagsView如果是设置了固定的isAffix不进行关闭
const closeAllTagsView = () => {
if (Session.get('tagsViewList')) {
storesKeepALiveNames.delAllCachedViews();
state.tagsViewList = [];
Session.get('tagsViewList').map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) {
v.url = setTagsViewHighlight(v);
state.tagsViewList.push({ ...v });
router.push({ path: state.tagsViewList[state.tagsViewList.length - 1].path });
}
});
addBrowserSetSession(state.tagsViewList);
}
};
// 6、开启当前页面全屏
const openCurrenFullscreen = async (path: string) => {
const item = state.tagsViewList.find((v: any) => (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path));
if (item.meta.isDynamic) await router.push({ name: item.name, params: item.params });
else await router.push({ name: item.name, query: item.query });
stores.setCurrenFullscreen(true);
};
// 当前项右键菜单点击,拿当前点击的路由路径对比 浏览器缓存中的 tagsView 路由数组,取当前点击项的详细路由信息
// 防止 tagsView 非当前页演示时,操作异常
const getCurrentRouteItem = (path: string, cParams: any) => {
const itemRoute = Session.get('tagsViewList') ? Session.get('tagsViewList') : state.tagsViewList;
return itemRoute.find((v: any) => {
if (
v.path === path &&
isObjectValueEqual(
v.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
cParams && Object.keys(cParams ? cParams : {}).length > 0 ? cParams : null
)
) {
return v;
} else if (v.path === path && Object.keys(cParams ? cParams : {}).length <= 0) {
return v;
}
});
};
// 当前项右键菜单点击
const onCurrentContextmenuClick = async (item: CurrentContextmenu) => {
const cParams = item.meta.isDynamic ? item.params : item.query;
if (!getCurrentRouteItem(item.path, cParams)) return ElMessage({ type: 'warning', message: '请正确输入路径及完整参数query、params' });
const { path, name, params, query, meta, url } = getCurrentRouteItem(item.path, cParams);
switch (item.contextMenuClickId) {
case 0:
// 刷新当前
if (meta.isDynamic) await router.push({ name, params });
else await router.push({ path, query });
refreshCurrentTagsView(route.fullPath);
break;
case 1:
// 关闭当前
closeCurrentTagsView(getThemeConfig.value.isShareTagsView ? path : url);
break;
case 2:
// 关闭其它
if (meta.isDynamic) await router.push({ name, params });
else await router.push({ path, query });
closeOtherTagsView(path);
break;
case 3:
// 关闭全部
closeAllTagsView();
break;
case 4:
// 开启当前页面全屏
openCurrenFullscreen(getThemeConfig.value.isShareTagsView ? path : url);
break;
}
};
// 右键点击时:传 x,y 坐标值到子组件中props
const onContextmenu = (v: any, e: any) => {
const { clientX, clientY } = e;
state.dropdown.x = clientX;
state.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(v);
};
// 鼠标按下时,判断是鼠标中键就关闭当前 tasgview
const onMousedownMenu = (v: any, e: any) => {
if (!v.meta.isAffix && e.which === 2) {
const item = Object.assign({}, { contextMenuClickId: 1, ...v });
onCurrentContextmenuClick(item);
}
};
// 当前的 tagsView 项点击时
const onTagsClick = (v: any, k: number) => {
state.tagsRefsIndex = k;
router.push(v);
};
// 处理 url地址栏链接有参数时tagsview 右键菜单刷新功能失效问题
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K3YO
const transUrlParams = (v: any) => {
let params = v.query && Object.keys(v.query).length > 0 ? v.query : v.params;
if (!params) return '';
let path = '';
for (let [key, value] of Object.entries(params)) {
if (v.meta.isDynamic) path += `/${value}`;
else path += `&${key}=${value}`;
}
// 判断是否是动态路由xxx/:id/:name"isDynamic
return v.meta.isDynamic ? `${v.path.split(':')[0]}${path.replace(/^\//, '')}` : `${v.path}${path.replace(/^&/, '?')}`;
};
// 处理 tagsView 高亮(多标签详情时使用,单标签详情未使用)
const setTagsViewHighlight = (v: any) => {
let params = v.query && Object.keys(v.query).length > 0 ? v.query : v.params;
if (!params || Object.keys(params).length <= 0) return v.path;
let path = '';
for (let i in params) {
path += params[i];
}
// 判断是否是动态路由xxx/:id/:name"
return `${v.meta.isDynamic ? v.meta.isDynamicPath : v.path}-${path}`;
};
// 鼠标滚轮滚动
const onHandleScroll = (e: any) => {
scrollbarRef.value.$refs.wrap$.scrollLeft += e.wheelDelta / 4;
};
// tagsView 横向滚动
const tagsViewmoveToCurrentTag = () => {
nextTick(() => {
if (tagsRefs.value.length <= 0) return false;
// 当前 li 元素
let liDom = tagsRefs.value[state.tagsRefsIndex];
// 当前 li 元素下标
let liIndex = state.tagsRefsIndex;
// 当前 ul 下 li 元素总长度
let liLength = tagsRefs.value.length;
// 最前 li
let liFirst: any = tagsRefs.value[0];
// 最后 li
let liLast: any = tagsRefs.value[tagsRefs.value.length - 1];
// 当前滚动条的值
let scrollRefs = scrollbarRef.value.$refs.wrap$;
// 当前滚动条滚动宽度
let scrollS = scrollRefs.scrollWidth;
// 当前滚动条偏移宽度
let offsetW = scrollRefs.offsetWidth;
// 当前滚动条偏移距离
let scrollL = scrollRefs.scrollLeft;
// 上一个 tags li dom
let liPrevTag: any = tagsRefs.value[state.tagsRefsIndex - 1];
// 下一个 tags li dom
let liNextTag: any = tagsRefs.value[state.tagsRefsIndex + 1];
// 上一个 tags li dom 的偏移距离
let beforePrevL: any = '';
// 下一个 tags li dom 的偏移距离
let afterNextL: any = '';
if (liDom === liFirst) {
// 头部
scrollRefs.scrollLeft = 0;
} else if (liDom === liLast) {
// 尾部
scrollRefs.scrollLeft = scrollS - offsetW;
} else {
// 非头/尾部
if (liIndex === 0) beforePrevL = liFirst.offsetLeft - 5;
else beforePrevL = liPrevTag?.offsetLeft - 5;
if (liIndex === liLength) afterNextL = liLast.offsetLeft + liLast.offsetWidth + 5;
else afterNextL = liNextTag.offsetLeft + liNextTag.offsetWidth + 5;
if (afterNextL > scrollL + offsetW) {
scrollRefs.scrollLeft = afterNextL - offsetW;
} else if (beforePrevL < scrollL) {
scrollRefs.scrollLeft = beforePrevL;
}
}
// 更新滚动条,防止不出现
scrollbarRef.value.update();
});
};
// 获取 tagsView 的下标:用于处理 tagsView 点击时的横向滚动
const getTagsRefsIndex = (path: string | unknown) => {
nextTick(async () => {
// await 使用该写法,防止拿取不到 tagsViewList 列表数据不完整
let tagsViewList = await state.tagsViewList;
state.tagsRefsIndex = tagsViewList.findIndex((v: any) => {
if (getThemeConfig.value.isShareTagsView) {
return v.path === path;
} else {
return v.url === path;
}
});
// 添加初始化横向滚动条移动到对应位置
tagsViewmoveToCurrentTag();
});
};
// 设置 tagsView 可以进行拖拽
const initSortable = async () => {
const el = <HTMLElement>document.querySelector('.layout-navbars-tagsview-ul');
if (!el) return false;
state.sortable.el && state.sortable.destroy();
state.sortable = Sortable.create(el, {
animation: 300,
dataIdAttr: 'data-url',
disabled: getThemeConfig.value.isSortableTagsView ? false : true,
onEnd: () => {
const sortEndList: any = [];
state.sortable.toArray().map((val: any) => {
state.tagsViewList.map((v: any) => {
if (v.url === val) sortEndList.push({ ...v });
});
});
addBrowserSetSession(sortEndList);
},
});
};
// 拖动问题https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
const onSortableResize = async () => {
await initSortable();
if (other.isMobile()) state.sortable.el && state.sortable.destroy();
};
// 页面加载前
onBeforeMount(() => {
// 初始化,防止手机端直接访问时还可以拖拽
onSortableResize();
// 拖动问题https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
window.addEventListener('resize', onSortableResize);
// 监听非本页面调用 0 刷新当前1 关闭当前2 关闭其它3 关闭全部 4 当前页全屏
mittBus.on('onCurrentContextmenuClick', (data: CurrentContextmenu) => {
onCurrentContextmenuClick(data);
});
// 监听布局配置界面开启/关闭拖拽
mittBus.on('openOrCloseSortable', () => {
initSortable();
});
// 监听布局配置开启 TagsView 共用,为了演示还原默认值
mittBus.on('openShareTagsView', () => {
if (getThemeConfig.value.isShareTagsView) {
router.push('/home');
state.tagsViewList = [];
state.tagsViewRoutesList.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) {
v.url = setTagsViewHighlight(v);
state.tagsViewList.push({ ...v });
}
});
}
});
});
// 页面卸载时
onUnmounted(() => {
// 取消非本页面调用监听
mittBus.off('onCurrentContextmenuClick', () => {});
// 取消监听布局配置界面开启/关闭拖拽
mittBus.off('openOrCloseSortable', () => {});
// 取消监听布局配置开启 TagsView 共用
mittBus.off('openShareTagsView', () => {});
// 取消窗口 resize 监听
window.removeEventListener('resize', onSortableResize);
});
// 页面更新时
onBeforeUpdate(() => {
tagsRefs.value = [];
});
// 页面加载时
onMounted(() => {
// 初始化 pinia 中的 tagsViewRoutes 列表
getTagsViewRoutes();
initSortable();
});
// 路由更新时(组件内生命钩子)
onBeforeRouteUpdate(async (to) => {
state.routeActive = setTagsViewHighlight(to);
state.routePath = to.meta.isDynamic ? to.meta.isDynamicPath : to.path;
await addTagsView(to.path, to);
getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
});
// 监听路由的变化,动态赋值给 tagsView
watch(
pinia.state,
(val) => {
if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
getTagsViewRoutes();
},
{
deep: true,
}
);
return {
isActive,
onContextmenu,
onMousedownMenu,
onTagsClick,
tagsRefs,
contextmenuRef,
scrollbarRef,
tagsUlRef,
onHandleScroll,
getThemeConfig,
setTagsStyle,
setTagsViewNameI18n,
refreshCurrentTagsView,
closeCurrentTagsView,
onCurrentContextmenuClick,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.layout-navbars-tagsview {
background-color: var(--el-color-white);
border-bottom: 1px solid var(--next-border-color-light);
position: relative;
z-index: 4;
:deep(.el-scrollbar__wrap) {
overflow-x: auto !important;
}
&-ul {
list-style: none;
margin: 0;
padding: 0;
height: 34px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
font-size: 12px;
white-space: nowrap;
padding: 0 15px;
&-li {
height: 26px;
line-height: 26px;
display: flex;
align-items: center;
border: 1px solid var(--el-border-color-lighter);
padding: 0 15px;
margin-right: 5px;
border-radius: 2px;
position: relative;
z-index: 0;
cursor: pointer;
justify-content: space-between;
&:hover {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-5);
}
&-iconfont {
position: relative;
left: -5px;
font-size: 12px;
}
&-icon {
border-radius: 100%;
position: relative;
height: 14px;
width: 14px;
text-align: center;
line-height: 14px;
right: -5px;
&:hover {
color: var(--el-color-white);
background-color: var(--el-color-primary-light-3);
}
}
.layout-icon-active {
display: block;
}
.layout-icon-three {
display: none;
}
}
.is-active {
color: var(--el-color-white);
background: var(--el-color-primary);
border-color: var(--el-color-primary);
transition: border-color 3s ease;
}
}
// 风格4
.tags-style-four {
.layout-navbars-tagsview-ul-li {
margin-right: 0 !important;
border: none !important;
position: relative;
border-radius: 3px !important;
.layout-icon-active {
display: none;
}
.layout-icon-three {
display: block;
}
&:hover {
background: none !important;
}
}
.is-active {
background: none !important;
color: var(--el-color-primary) !important;
}
}
// 风格5
.tags-style-five {
align-items: flex-end;
.tags-style-five-svg {
-webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E")
12 27 15;
}
.layout-navbars-tagsview-ul-li {
padding: 0 5px;
border-width: 15px 27px 15px;
border-style: solid;
border-color: transparent;
margin: 0 -15px;
.layout-icon-active,
.layout-navbars-tagsview-ul-li-iconfont,
.layout-navbars-tagsview-ul-li-refresh {
display: none;
}
.layout-icon-three {
display: block;
}
&:hover {
@extend .tags-style-five-svg;
background: var(--el-color-primary-light-9);
color: unset;
}
}
.is-active {
@extend .tags-style-five-svg;
background: var(--el-color-primary-light-9) !important;
color: var(--el-color-primary) !important;
z-index: 1;
}
}
}
.layout-navbars-tagsview-shadow {
box-shadow: rgb(0 21 41 / 4%) 0px 1px 4px;
}
</style>

View File

@ -1,170 +0,0 @@
<template>
<div class="el-menu-horizontal-warp">
<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
<el-menu router :default-active="defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal">
<template v-for="val in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
<template v-else>
<el-menu-item :index="val.path" :key="val.path">
<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</template>
<template #title v-else>
<a class="w100" @click.prevent="onALinkClick(val)">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</a>
</template>
</el-menu-item>
</template>
</template>
</el-menu>
</el-scrollbar>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, toRefs, reactive, computed, defineComponent, onMounted, nextTick, onBeforeMount, ref } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import { verifyUrl } from '/@/utils/toolsValidate';
import mittBus from '/@/utils/mitt';
export default defineComponent({
name: 'navMenuHorizontal',
components: {
SubItem: defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue')),
},
props: {
menuList: {
type: Array,
default: () => [],
},
},
setup(props) {
const elMenuHorizontalScrollRef = ref();
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const router = useRouter();
const state = reactive({
defaultActive: null,
});
// 获取父级菜单数据
const menuLists = computed(() => {
return <any>props.menuList;
});
// 设置横向滚动条可以鼠标滚轮滚动
const onElMenuHorizontalScroll = (e: any) => {
const eventDelta = e.wheelDelta || -e.deltaY * 40;
elMenuHorizontalScrollRef.value.$refs.wrap$.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrap$.scrollLeft + eventDelta / 4;
};
// 初始化数据,页面刷新时,滚动条滚动到对应位置
const initElMenuOffsetLeft = () => {
nextTick(() => {
let els: any = document.querySelector('.el-menu.el-menu--horizontal li.is-active');
if (!els) return false;
elMenuHorizontalScrollRef.value.$refs.wrap$.scrollLeft = els.offsetLeft;
});
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<string>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 传送当前子级数据到菜单中
const setSendClassicChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: any = {};
filterRoutesFun(routesList.value).map((v, k) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = [{ ...v }];
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 设置页面当前路由高亮
const setCurrentRouterHighlight = (currentRoute: any) => {
const { path, meta } = currentRoute;
if (themeConfig.value.layout === 'classic') {
(<any>state.defaultActive) = `/${path.split('/')[1]}`;
} else {
const pathSplit = meta.isDynamic ? meta.isDynamicPath.split('/') : path.split('/');
if (pathSplit.length >= 4 && meta.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/');
else state.defaultActive = path;
}
};
// 打开外部链接
const onALinkClick = (val: any) => {
const { origin, pathname } = window.location;
router.push(val.path);
if (verifyUrl(val.meta.isLink)) window.open(val.meta.isLink);
else window.open(`${origin}${pathname}#${val.meta.isLink}`);
};
// 页面加载前
onBeforeMount(() => {
setCurrentRouterHighlight(route);
});
// 页面加载时
onMounted(() => {
initElMenuOffsetLeft();
});
// 路由更新时
onBeforeRouteUpdate((to) => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
setCurrentRouterHighlight(to);
// 修复经典布局开启切割菜单时点击tagsView后左侧导航菜单数据不变的问题
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
}
});
return {
elMenuHorizontalScrollRef,
menuLists,
onElMenuHorizontalScroll,
onALinkClick,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.el-menu-horizontal-warp {
flex: 1;
overflow: hidden;
margin-right: 30px;
:deep(.el-scrollbar__bar.is-vertical) {
display: none;
}
:deep(a) {
width: 100%;
}
.el-menu.el-menu--horizontal {
display: flex;
height: 100%;
width: 100%;
box-sizing: border-box;
}
}
</style>

View File

@ -1,112 +0,0 @@
<template>
<el-menu
router
:default-active="defaultActive"
background-color="transparent"
:collapse="isCollapse"
:unique-opened="getThemeConfig.isUniqueOpened"
:collapse-transition="false"
>
<template v-for="val in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
<template v-else>
<el-menu-item :index="val.path" :key="val.path">
<SvgIcon :name="val.meta.icon" />
<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<span>{{ $t(val.meta.title) }}</span>
</template>
<template #title v-else>
<a class="w100" @click.prevent="onALinkClick(val)">{{ $t(val.meta.title) }}</a>
</template>
</el-menu-item>
</template>
</template>
</el-menu>
</template>
<script lang="ts">
import { defineAsyncComponent, toRefs, reactive, computed, defineComponent, onMounted, watch } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { verifyUrl } from '/@/utils/toolsValidate';
export default defineComponent({
name: 'navMenuVertical',
components: {
SubItem: defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue')),
},
props: {
menuList: {
type: Array,
default: () => [],
},
},
setup(props) {
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const router = useRouter();
const state = reactive({
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
defaultActive: route.meta.isDynamic ? route.meta.isDynamicPath : route.path,
isCollapse: false,
});
// 获取父级菜单数据
const menuLists = computed(() => {
return <any>props.menuList;
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
});
// 菜单高亮(详情时,父级高亮)
const setParentHighlight = (currentRoute: any) => {
const { path, meta } = currentRoute;
const pathSplit = meta.isDynamic ? meta.isDynamicPath.split('/') : path.split('/');
if (pathSplit.length >= 4 && meta.isHide) return pathSplit.splice(0, 3).join('/');
else return path;
};
// 打开外部链接
const onALinkClick = (val: any) => {
const { origin, pathname } = window.location;
router.push(val.path);
if (verifyUrl(val.meta.isLink)) window.open(val.meta.isLink);
else window.open(`${origin}${pathname}#${val.meta.isLink}`);
};
// 设置菜单的收起/展开
watch(
themeConfig.value,
() => {
document.body.clientWidth <= 1000 ? (state.isCollapse = false) : (state.isCollapse = themeConfig.value.isCollapse);
},
{
immediate: true,
}
);
// 页面加载时
onMounted(() => {
state.defaultActive = setParentHighlight(route);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
state.defaultActive = setParentHighlight(to);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
});
return {
menuLists,
getThemeConfig,
onALinkClick,
...toRefs(state),
};
},
});
</script>

View File

@ -1,90 +0,0 @@
<template>
<div class="layout-padding layout-padding-unset layout-iframe">
<div class="layout-padding-auto layout-padding-view">
<div class="w100" v-for="v in setIframeList" :key="v.path" v-loading="v.meta.loading" element-loading-background="white">
<transition-group :name="name" mode="out-in">
<iframe
:src="v.meta.isLink"
:key="v.path"
frameborder="0"
height="100%"
width="100%"
style="position: absolute"
:data-url="v.path"
v-show="getRoutePath === v.path"
ref="iframeRef"
/>
</transition-group>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, watch, ref, nextTick } from 'vue';
import { useRoute } from 'vue-router';
export default defineComponent({
name: 'layoutIframeView',
props: {
refreshKey: {
type: String,
default: () => '',
},
name: {
type: String,
default: () => 'slide-right',
},
list: {
type: Array,
default: () => [],
},
},
setup(props) {
const iframeRef = ref();
const route = useRoute();
// 处理 list 列表,当打开时,才进行加载
const setIframeList = computed(() => {
return (<any>props.list).filter((v: any) => v.meta.isIframeOpen);
});
// 获取 iframe 当前路由 path
const getRoutePath = computed(() => {
return route.path;
});
// 监听路由变化,初始化 iframe 数据,防止多个 iframe 时,切换不生效
watch(
() => route.fullPath,
(val) => {
const item: any = props.list.find((v: any) => v.path === val);
if (item && !item.meta.isIframeOpen) item.meta.isIframeOpen = true;
nextTick(() => {
if (!iframeRef.value) return false;
iframeRef.value.forEach((v: any) => {
if (v.dataset.url === val) {
v.onload = () => {
if (item && item.meta.isIframeOpen && item.meta.loading) item.meta.loading = false;
};
}
});
});
},
{
immediate: true,
}
);
// 监听 iframe refreshKey 变化,用于 tagsview 右键菜单刷新
watch(
() => props.refreshKey,
() => {},
{
deep: true,
}
);
return {
iframeRef,
setIframeList,
getRoutePath,
};
},
});
</script>

Some files were not shown because too many files have changed in this diff Show More