101 Commits

Author SHA1 Message Date
xiaoma
9e3ebd62b2 1.8.1 2022-05-11 11:43:01 +08:00
Ah jung
5b852506a6 Merge pull request #117 from xiangshu233/main
修复  clickMenuItem 事件在子组件 emits 中未定义而引起警告的问题
2022-04-08 13:37:09 +08:00
xiangshu233
f42857884f 修复 clickMenuItem 事件在子组件 emits 中未定义而引起警告的问题 2022-04-07 17:47:10 +08:00
xiaoma
9ad5ba18a9 1.8.0 2022-04-01 10:52:30 +08:00
Ah jung
536e16f166 Merge pull request #114 from liub1934/main
feat:表格列支持draggable配置是否能拖拽
2022-04-01 09:44:52 +08:00
寻梦
42f2256ea1 feat:表格列支持draggable配置是否能拖拽 2022-03-30 18:18:20 +08:00
Ah jung
bf0d294322 Update README.md 2022-03-28 13:57:43 +08:00
Ah jung
51f5c64755 Update README.md 2022-03-22 13:31:24 +08:00
Ah jung
b49d9e8bd2 Merge pull request #111 from wangdaoo/feat/wangdaoo
fix: 优化部分无用代码及升级ui版本 🚀
2022-03-12 18:29:21 +08:00
山人自有妙计
12e62d1179 fix: 优化部分无用代码及升级ui版本 🚀 2022-03-12 18:22:28 +08:00
Ah jung
6558e1597c Merge pull request #106 from MaybeQHL/main
fix: 修复在移动端模式下tags显示的bug  update: 移动端模式下侧边菜单item点击隐藏drawer.
2022-03-08 08:42:29 +08:00
Maybe_QHL
7bf1e1265a fix: 修复在移动端模式下tags显示的bug update: 移动端模式下侧边菜单item点击隐藏drawer. 2022-03-07 17:40:17 +08:00
Ah jung
1213de598e Merge pull request #105 from MaybeQHL/main
更新侧边导航栏移动端显示为modal显示

> ![微信截图_20220307145012](https://user-images.githubusercontent.com/34638673/156982207-e5185056-cc2a-4fd4-b7d6-53182166b1a4.png) tags 还有点bug需要你去修复一下

好,我抽空看看
2022-03-07 14:53:30 +08:00
Maybe_QHL
20c9dbbfe1 忽略代理 2022-03-07 14:31:00 +08:00
Maybe_QHL
6dab2ab35b 更新侧边导航栏移动端显示为modal显示。 2022-03-07 14:21:27 +08:00
xiaoma
a424788c45 CHANGELOG 修改 2022-02-14 15:46:13 +08:00
xiaoma
b31d5c2bd6 1.7.0 2022-02-14 15:42:17 +08:00
xiaoma
b979d9db32 1.7.0 2022-02-14 15:41:46 +08:00
xiaoma
a53c86e41b 1.7.0 2022-02-14 15:12:32 +08:00
Ah jung
b16b5c8992 Merge pull request #97 from liu1013269528/main
修复 类型“ComputedRef<{ bgColor: string; fixed: boolean; show: boolean; }…
2022-02-14 10:05:27 +08:00
liu1013269528
58dadbb95a 修复 类型“ComputedRef<{ bgColor: string; fixed: boolean; show: boolean; }>”上不存在属性“fixed”。
获取计算属性值,需要增加 value 否则返回 undefined
2022-02-14 00:19:05 +08:00
Ah jung
4ebdbc7203 Update README.md 2022-02-13 15:54:29 +08:00
Ah jung
0729e56ed4 Merge pull request #91 from devilmengcry/main
更新naive-ui到2.24.6 show-password-toggle替换成showPasswordOn="click"
2022-01-26 16:06:15 +08:00
李志萌
a50cbfa44d 更新naive-ui到2.24.6 show-password-toggle替换成showPasswordOn="click" 2022-01-26 15:58:19 +08:00
Ah jung
65d6d4d21e Update README.md 2022-01-26 10:29:45 +08:00
Ah jung
c5bb818f13 Update README.md 2022-01-19 15:32:12 +08:00
Ah jung
9e255da5d7 Update README.md 2022-01-12 11:55:15 +08:00
Ah jung
e2b5086be3 Update README.md 2022-01-12 11:53:33 +08:00
xiaoma
caaca83f78 fix #81 2022-01-07 09:12:41 +08:00
xiaoma
91de971636 降低eslint-define-config版本 2022-01-06 12:54:32 +08:00
xiaoma
5fb005d5ae 依赖还原 2022-01-06 11:26:59 +08:00
xiaoma
b42e0a2fef 还原依赖 2022-01-06 11:26:17 +08:00
xiaoma
097dda5aa1 依赖升级 2022-01-06 10:47:03 +08:00
xiaoma
16714d4bdb 降低eslint版本 2022-01-06 10:36:05 +08:00
xiaoma
a5438b4f50 降低eslint-define-config依赖 2022-01-06 10:29:43 +08:00
xiaoma
4f5bbb0673 降低eslint-define-config依赖 2022-01-06 10:25:46 +08:00
xiaoma
24cbde8b95 降低 eslint-define-config 依赖 2022-01-06 10:23:39 +08:00
xiaoma
7222398cf0 依赖升级 2022-01-06 10:14:24 +08:00
xiaoma
261e27c139 还原yarn.lock 2022-01-06 10:08:03 +08:00
xiaoma
8288f0a84b 修复:项目配置 空白,多标签背景色 bug 2022-01-06 09:50:28 +08:00
xiaoma
c0ff8985ea Merge branch 'main' of github.com:jekip/naive-ui-admin into main 2021-12-30 15:04:31 +08:00
xiaoma
a8400ac475 Fix #80 And Depend on the upgrade 2021-12-30 15:04:10 +08:00
jack
d4b173e3c8 table 支持 dataSource 2021-12-26 10:18:44 +08:00
xiaoma
1ab8b5b221 1.6.0 README.md update 2021-12-24 10:48:49 +08:00
Ah jung
fc5e138ed8 Merge pull request #74 from chenyuxi2002/docs/README
docs: README.md 技术站->技术栈
2021-12-06 16:12:09 +08:00
tzcat8
b9b2c6a07e docs: README.md 技术站->技术栈 2021-12-06 16:01:03 +08:00
Ah jung
3503569dfe Update README.md 2021-11-26 19:13:24 +08:00
Ah jung
125b7f44e3 Update README.md 2021-11-19 23:42:38 +08:00
Ah jung
3307b927a4 Merge pull request #70 from litao36253/dev_tobealone
获得最新菜单后更新TagsView组件的localStorage缓存
2021-11-18 17:52:29 +08:00
litao
97de86eacb 获得最新菜单后更新TagsView组件的localStorage缓存 2021-11-18 14:35:05 +08:00
Ah jung
cb92f6e87b Merge pull request #67 from litao36253/dev_tobealone
按需引入NaiveUi并自动创建.d.ts声明,在volar中使用组件可获得代码提示和类型检查
2021-11-14 10:35:16 +08:00
litao
4f81743f90 按需引入NaiveUi并自动创建.d.ts声明,在volar中使用组件可获得代码提示和类型检查 2021-11-14 10:28:54 +08:00
xiaoma
7837c87392 eslintrc 配置更新 2021-11-11 15:34:36 +08:00
xiaoma
17a5d08d94 fix bug
README.md update
2021-11-08 09:24:33 +08:00
xiaoma
19e5e5fc4a fix bug
README.md update
2021-11-08 09:21:29 +08:00
xiaoma
822e69deec README.md update 2021-10-11 13:32:45 +08:00
xiaoma
3bf1e941d4 fix bug 2021-09-29 10:20:48 +08:00
xiaoma
ef4912636e optimize ts type 2021-09-24 09:08:23 +08:00
xiaoma
7929a74d20 Merge remote-tracking branch 'origin/main' into main 2021-09-23 13:37:39 +08:00
xiaoma
45862d4f9d Fix bug and README.md update 2021-09-23 13:37:18 +08:00
Ah jung
8e6471060c Merge pull request #54 from 2462870727/main
fix: 增加useModal确认按钮文本
2021-09-22 08:47:45 +08:00
Ah jung
30c0cd5c95 Update basicModal.vue 2021-09-22 08:46:34 +08:00
2462870727@qq.com
259e73c056 fix: useModal 2021-09-19 12:22:49 +08:00
xiaoma
8eaa889399 Fix bug and README.md update 2021-09-18 17:50:21 +08:00
xiaoma
d1635add5f Fix bug and README.md update 2021-09-18 17:49:04 +08:00
Ah jung
531a31ee5d Merge pull request #50 from eGluZl/main
fix: 修正角色页面的弹窗中的树型组件点击节点不会展开孩子节点的问题。
2021-09-15 18:31:04 +08:00
Ah jung
3b64fc1563 Merge pull request #41 from heng1025/patch-1
Update ProjectSetting.vue
2021-09-15 17:11:39 +08:00
Ah jung
229fc72cda Merge pull request #34 from Chika99/fix/tagsview
fix(tagsview): 多页签适配主题
2021-09-15 17:11:23 +08:00
Ah jung
900954d757 Merge pull request #33 from devlemoe/main
feat: TagsView自动滚动功能
2021-09-15 17:11:01 +08:00
Ah jung
fca4dddaf6 Merge pull request #43 from FE-Roading/main
fix: constantRouter重复添加和addRoute优化
2021-09-15 17:03:46 +08:00
eGluZl
4add3c3eaa fix: 修正角色页面的弹窗中的树型组件点击节点不会展开孩子节点的问题。 2021-09-15 16:52:59 +08:00
Ah jung
21e80f8041 Merge pull request #47 from eGluZl/main
fix: 修正点击左侧树形组件的某节点时,对应的数据在赋值后页面不响应的问题。
2021-09-14 10:21:50 +08:00
eGluZl
a63c34dbb5 fix: 修正点击左侧树形组件的某节点时,对应的数据在赋值后页面不响应的问题。 2021-09-14 10:15:50 +08:00
xiaoma
e02e96d4e0 标签名修改 2021-09-13 18:46:02 +08:00
xiaoma
70c2b75f17 fix bug 2021-09-10 18:36:43 +08:00
qixm
b186e045bc fix: constantRouter在useAsyncRouteStore.routers重复添加;BACK权限模式下,router.addRoute添加两次的问题 2021-09-02 15:16:41 +08:00
iron
742749a838 Update ProjectSetting.vue 2021-09-01 16:59:30 +08:00
xiaoma
82dc7d2589 fix 分页字段配置问题 2021-08-28 11:54:42 +08:00
xiaoma
f11af4fc76 fix 分页字段配置问题 2021-08-28 11:52:18 +08:00
xiaoma
85f1dbd5e9 fix 分页字段配置问题 2021-08-28 11:41:36 +08:00
xiaoma
ae001fc7bd fix 分页字段配置问题 2021-08-28 11:18:33 +08:00
chika
be770016bf fix(tagsview): 多页签适配主题 2021-08-21 15:19:07 +08:00
devlemoe
1bf722bed0 fix: prev和next按钮不能准确隐藏 2021-08-19 14:19:21 +08:00
devlemoe
8c7dd14004 feat: TagsView自动滚动功能 2021-08-19 11:56:56 +08:00
xiaoma
a0490e3b97 支持 Vue 3.2.x 语法升级为,script setup 2021-08-14 14:53:32 +08:00
xiaoma
905984367c 支持 Vue 3.2.x 语法升级为,script setup 2021-08-14 14:35:42 +08:00
xiaoma
d3f7fa0f9e CHANGELOG更新 2021-08-10 17:31:50 +08:00
xiaoma
9c512002d2 Fixes bug 动态路由配置重构 2021-08-10 17:16:58 +08:00
Ah jung
737f967aab Merge pull request #24 from CasbaL/fix/table-setting-bug
fix(Table): 基本表格设置列固定时,重复添加action列导致的样式错乱问题
2021-08-10 17:11:51 +08:00
casbal
1cdb02c9d7 fix: 基本表格设置列固定时,重复添加action列导致的样式错乱问题 2021-08-10 17:05:52 +08:00
Ah jung
bc8dd21405 Merge pull request #23 from Dishone/main
暗色模式下多页签背景问题
2021-08-10 17:00:38 +08:00
Dishone
2dba60405e 暗色模式下多页签背景问题 2021-08-10 03:21:40 +08:00
xiaoma
eba3047be2 iframe 开启滚动条 2021-08-09 16:32:33 +08:00
xiaoma
0979b5af5d 新增:Form组件支持响应式配置,路由支持外部地址(内联) 2021-08-09 16:16:16 +08:00
xiaoma
ade138997d logo美化,顶部菜单新增logo展示 2021-08-09 10:50:16 +08:00
xiaoma
d388ae5656 fix bug 2021-08-09 10:17:37 +08:00
啊俊
8f05b20ffa fix bug #22 表格列默认开启 ellipsis 属性 2021-08-08 15:17:02 +08:00
啊俊
d973b2a543 fix bug #20 2021-08-07 16:38:54 +08:00
Ah jung
1d5113a663 Merge pull request #21 from zhouyuf/master
添加锁屏时enter键解除锁屏
2021-08-07 16:14:48 +08:00
zhouyuf
f331d9c4c7 添加锁屏时enter键解除锁屏 2021-08-07 15:20:41 +08:00
Ah jung
c647e19d06 文档和预览地址变更 2021-08-07 10:00:03 +08:00
132 changed files with 14308 additions and 5481 deletions

View File

@@ -15,7 +15,7 @@ VITE_DROP_CONSOLE = true
# 跨域代理,可以配置多个,请注意不要换行 # 跨域代理,可以配置多个,请注意不要换行
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]] #VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
# VITE_PROXY=[["/api","https://naive-ui-admin"]] #VITE_PROXY=[["/api","https://naive-ui-admin"]]
# API 接口地址 # API 接口地址
VITE_GLOB_API_URL = VITE_GLOB_API_URL =

View File

@@ -1,4 +1,3 @@
*.sh *.sh
node_modules node_modules
*.md *.md
@@ -13,3 +12,5 @@ dist
.local .local
/bin /bin
Dockerfile Dockerfile
components.d.ts
components.d.ts

View File

@@ -22,9 +22,9 @@ module.exports = defineConfig({
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'prettier', 'prettier',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
'plugin:jest/recommended',
], ],
rules: { rules: {
'vue/script-setup-uses-vars': 'error',
'@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
@@ -61,7 +61,6 @@ module.exports = defineConfig({
'vue/singleline-html-element-content-newline': 'off', 'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off', 'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off', 'vue/require-default-prop': 'off',
'vue/script-setup-uses-vars': 'off',
'vue/html-self-closing': [ 'vue/html-self-closing': [
'error', 'error',
{ {

2
.gitignore vendored
View File

@@ -23,3 +23,5 @@ pnpm-debug.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/components.d.ts
/components.d.ts

View File

@@ -1,3 +1,104 @@
# CHANGELOG
## 1.8.1
- ### ✨ Features
- 新增 `clean:cache` 删除缓存命令
- 新增 `clean:lib` 删除 `node_modules` 命令
- 依赖升级
### 🐛 Bug Fixes
- 修复 `开发环境` 运行控制台错误提示
## 1.8.0 (2022-04-01)
- ### ✨ Features
- 新增 `多页签` 支持配置 `affix` 固定属性
- 新增 `usePage` Hooks
- 表格列支持 `draggable` 配置拖拽 合并 [#114](https://github.com/jekip/naive-ui-admin/pull/114)
- 依赖升级
### 🐛 Bug Fixes
- 修复 `多页签` 关闭全部缺陷
- 修复 `多页签` 跳转缺陷(记得清空多页签缓存)
## 1.7.0 (2022-02-14)
### 🐛 Bug Fixes
- 移除 `登录页面` 滑动验证组件
- 修复 `BasicUpload` 组件,回显问题
- 修复 `ts类型` 配置缺陷
- 修复 `登录页面` message 交互缺陷
- 修复 `表格编辑` 时间格式化异常 [#92](https://github.com/jekip/naive-ui-admin/issues/92)
- ### ✨ Features
- 依赖升级
## 1.6.1 (2022-01-06)
### 🐛 Bug Fixes
- 修复 `项目配置` 打开空白
- 修复 `多标签` 背景和字体色变量丢失
- ### ✨ Features
- 依赖升级
## 1.6.0 (2021-12-24)
### 🐛 Bug Fixes
- 修复 `低版本浏览器` 报 globalThis 未定义
- 修复 `Axios` api地址拼接异常
- 修复 `createStorage存在prefixKey` 会出bug
- ### ✨ Features
- 破坏 `Axios` 取消默认导出 `http` 可支持多个请求导出
- 搜索 `import http from '@/utils/http/axios'` 替换为 `import { http } from '@/utils/http/axios`
- 新增 `Axios` 多项配置 `urlPrefix``joinTime``ignoreCancelToken``withToken``uploadFile方法`
- 依赖升级
## 1.5.5 (2021-08-14)
### 🐛 Bug Fixes
- 修复路由只存在一个子路由,图标不显示问题
- UI样式美化
- ### ✨ Features
- 支持 Vue 3.2.x
- 代码全部按 `script setup` 语法重写完成80%
- 新增 `回到顶部` 功能
- 新增 `拖拽` 示例页面
- 新增 `富文本` 组件
- 新增 `路由切换动画` 可在项目设置切换
- 依赖升级
# CHANGELOG
## 1.5.4 (2021-08-10)
### 🐛 Bug Fixes
- `暗色模式下多页签背景问题 ` 合并 [#23](https://github.com/jekip/naive-ui-admin/pull/23) 感谢 [@Dishone](https://github.com/Dishone)
- `表格设置列重复添加action列样式错乱问题` 合并 [#24](https://github.com/jekip/naive-ui-admin/pull/24) 感谢 [@CasbaL](https://github.com/CasbaL)
- ### ✨ Features
- 新增 `路由支持(内联外部地址)`配置
- 新增 `顶部菜单` logo展示
-(破坏性更新)
- 优化 `动态路由配置` 取消`constantRouterComponents.ts`,中组件映射配置,更名为 `router-icons.ts`
- 优化 `admin_info接口结构`roles 更名为permissionsroles.roleName更名为label
- 优化 多级路由,当没有配置`redirect`时,默认为第一个子路由,配置则优先按配置
- 依赖升级
# 1.5.3 (2021-08-09)
### 🐛 Bug Fixes
- 修复顶部菜单,选中联动
- 修复混合菜单模式,切换其他模式菜单未重置
- 实例基础列表,和表格组件实例,开启横向滚动特性
- `naiveui` 升级成最新版
- ### ✨ Features
- table组件默认开启 `ellipsis` 特性
# 1.5.2 (2021-08-06) # 1.5.2 (2021-08-06)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复已知bug - 修复已知bug

View File

@@ -1,24 +1,56 @@
## 简介 ## 简介
[Naive Ui Admin](https://github.com/jekip/naive-ui-admin) 是一个基于 [Vue3.0](https://github.com/vuejs/vue-next)、[Vite](https://github.com/vitejs/vite)、 [Naive UI](https://www.naiveui.com/)、[TypeScript](https://www.typescriptlang.org/) 的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目,相信不管是从新技术使用还是其他方面,都能帮助到你。 [Naive Ui Admin](https://github.com/jekip/naive-ui-admin) 完全免费,且可商用,基于 [Vue3.0](https://github.com/vuejs/vue-next)、[Vite](https://github.com/vitejs/vite)、 [Naive UI](https://www.naiveui.com/)、[TypeScript](https://www.typescriptlang.org/) 的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目, 相信不管是从新技术使用还是其他方面,都能帮助到你。
## 特性 ## 特性
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发 - 二次封装实用高扩展性组件
- **TypeScript**: 应用程序级 JavaScript 的语言 - 响应式、多主题,多配置,快速集成,开箱即用
- **主题**:可配置的主题 - 最新技术栈,使用 `Vue3``Typescript``Pinia``Vite` 等前端前沿技术
- **Mock 数据** 内置 Mock 数据方案 - 强大的鉴权系统,对路由、菜单、功能点等支持`三种鉴权模式`,满足不同的业务鉴权需求
- **权限** 内置完善的动态路由权限生成方案 - 持续更新,实用性页面模板功能和交互,随意搭配组合,让构建页面变得简单化
- **组件** 二次封装了多个常用的组件
## 在线预览 ## 预览
- [naive-ui-admin](https://jekip.github.io) - [naive-ui-admin](https://naive-ui-admin.vercel.app)
账号admin密码123456随意 账号admin密码123456随意
## 提示
如果这个版本的功能和组件,并不能满足您的需求,不妨看看,我们全新 `NaiveAdmin v2` 他或许能让您眼前一亮O(∩_∩)O哈哈~
[NaiveAdmin 官网](https://www.naiveadmin.com)
[NaiveAdmin v2 预览](https://pro.naiveadmin.com)
[NaiveAdmin v2 变更日志](https://www.naiveadmin.com/guide/changelog)
## 新品
### Antd vue
新产品,如果您选的技术栈是 `Antd` 的话,不妨看看
[NaiveAdmin Antd 预览](https://antd.naiveadmin.com)
### Arco vue
新产品,智能设计体系,连接轻盈体验
[NaiveAdmin Arco 预览](https://arco.naiveadmin.com)
### Element Plus
新产品,面向设计师和开发者的组件库
[Element Plus Admin 预览](https://element.naiveadmin.com)
以上版本同时具备 `NaiveAdmin v2` 功能/组件/页面,一如既往、开箱即用,欢迎前往查看。
## 文档 ## 文档
[文档地址](https://jekip.github.io/docs/) [v1文档地址](https://naive-ui-admin-docs.vercel.app)
## 准备 ## 准备
@@ -31,7 +63,7 @@
- [Naive-ui-admin](https://www.naiveui.com/) - ui 基本使用 - [Naive-ui-admin](https://www.naiveui.com/) - ui 基本使用
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法 - [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
## 安装使用 ## 使用
- 获取项目代码 - 获取项目代码
@@ -64,9 +96,6 @@ yarn build
[CHANGELOG](./CHANGELOG.md) [CHANGELOG](./CHANGELOG.md)
## 感谢
[@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,再次感谢作者。
## 如何贡献 ## 如何贡献
@@ -113,7 +142,13 @@ yarn build
## 交流 ## 交流
`Naive Ui Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供 QQ 交流群使用问题欢迎在群内提问。 `Naive Ui Admin` 使用或者其他问题,都可以在群内讨论或提问。
- QQ 群 `328347666` ![abelianGroup](https://user-images.githubusercontent.com/19426584/160335146-c28dd205-4600-4d62-b2c6-6456034ab7b1.jpg)
## 赞助
#### 如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 🍹。
![donate](https://jekip.github.io/docs/images/sponsor.png)
[Paypal Me](https://www.paypal.com/paypalme/majunping)

View File

@@ -1,4 +1,6 @@
import type { Plugin } from 'vite'; import type { Plugin } from 'vite';
import Components from 'unplugin-vue-components/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx'; import vueJsx from '@vitejs/plugin-vue-jsx';
@@ -15,6 +17,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock)
vue(), vue(),
// have to // have to
vueJsx(), vueJsx(),
// 按需引入NaiveUi且自动创建组件声明
Components({
dts: true,
resolvers: [NaiveUiResolver()],
}),
]; ];
// vite-plugin-html // vite-plugin-html

View File

@@ -14,7 +14,7 @@ module.exports = {
'fixed', 'fixed',
'resolve', 'resolve',
'resolves', 'resolves',
'resolved' 'resolved',
], ],
issuePrefixes: ['#'], issuePrefixes: ['#'],
noteKeywords: ['BREAKING CHANGE'], noteKeywords: ['BREAKING CHANGE'],
@@ -23,8 +23,8 @@ module.exports = {
revertCorrespondence: ['header', 'hash'], revertCorrespondence: ['header', 'hash'],
warn() {}, warn() {},
mergePattern: null, mergePattern: null,
mergeCorrespondence: null mergeCorrespondence: null,
} },
}, },
rules: { rules: {
'body-leading-blank': [2, 'always'], 'body-leading-blank': [2, 'always'],
@@ -50,8 +50,8 @@ module.exports = {
'wip', 'wip',
'workflow', 'workflow',
'types', 'types',
'release' 'release',
] ],
] ],
} },
} };

View File

@@ -1,26 +1,122 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-cmn-Hans"> <html lang="zh-cmn-Hans" id="htmlRoot" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"/>
<meta name="renderer" content="webkit"/> <meta content="webkit" name="renderer"/>
<meta <meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
name="viewport"
/> />
<link rel="icon" href="/favicon.ico"/> <link href="/favicon.ico" rel="icon"/>
<title><%= title %></title> <title><%= title %></title>
<style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
</head> </head>
<body> <body>
<div id="appProvider" style="display: none"></div> <div id="appProvider" style="display: none"></div>
<div id="app"> <div id="app">
<div class="first-loading-wrp"> <style>
<div class="loading-wrp"> .first-loading-wrap {
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span> display: flex;
width: 100%;
height: 100vh;
justify-content: center;
align-items: center;
flex-direction: column;
}
.first-loading-wrap > h1 {
font-size: 128px
}
.first-loading-wrap .loading-wrap {
padding: 98px;
display: flex;
justify-content: center;
align-items: center
}
.dot {
animation: antRotate 1.2s infinite linear;
transform: rotate(45deg);
position: relative;
display: inline-block;
font-size: 32px;
width: 32px;
height: 32px;
box-sizing: border-box
}
.dot i {
width: 14px;
height: 14px;
position: absolute;
display: block;
background-color: #1890ff;
border-radius: 100%;
transform: scale(.75);
transform-origin: 50% 50%;
opacity: .3;
animation: antSpinMove 1s infinite linear alternate
}
.dot i:nth-child(1) {
top: 0;
left: 0
}
.dot i:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: .4s;
animation-delay: .4s
}
.dot i:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: .8s;
animation-delay: .8s
}
.dot i:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg)
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg)
}
}
@keyframes antSpinMove {
to {
opacity: 1
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1
}
}</style>
<div class="first-loading-wrap">
<div class="loading-wrap">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="/src/main.ts"></script> <script>var globalThis = window;</script>
<script src="/src/main.ts" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -4,7 +4,7 @@ const menusList = [
{ {
path: '/dashboard', path: '/dashboard',
name: 'Dashboard', name: 'Dashboard',
component: 'Layout', component: 'LAYOUT',
redirect: '/dashboard/console', redirect: '/dashboard/console',
meta: { meta: {
icon: 'DashboardOutlined', icon: 'DashboardOutlined',
@@ -14,7 +14,7 @@ const menusList = [
{ {
path: 'console', path: 'console',
name: 'dashboard_console', name: 'dashboard_console',
component: 'DashboardConsole', component: '/dashboard/console/console',
meta: { meta: {
title: '主控台', title: '主控台',
}, },
@@ -22,7 +22,7 @@ const menusList = [
{ {
path: 'monitor', path: 'monitor',
name: 'dashboard_monitor', name: 'dashboard_monitor',
component: 'DashboardMonitor', component: '/dashboard/monitor/monitor',
meta: { meta: {
title: '监控页', title: '监控页',
}, },
@@ -30,7 +30,7 @@ const menusList = [
{ {
path: 'workplace', path: 'workplace',
name: 'dashboard_workplace', name: 'dashboard_workplace',
component: 'DashboardWorkplace', component: '/dashboard/workplace/workplace',
meta: { meta: {
hidden: true, hidden: true,
title: '工作台', title: '工作台',

View File

@@ -13,25 +13,25 @@ const adminInfo = {
desc: 'manager', desc: 'manager',
password: Random.string('upper', 4, 16), password: Random.string('upper', 4, 16),
token, token,
roles: [ permissions: [
{ {
roleName: '主控台', label: '主控台',
value: 'dashboard_console', value: 'dashboard_console',
}, },
{ {
roleName: '监控页', label: '监控页',
value: 'dashboard_monitor', value: 'dashboard_monitor',
}, },
{ {
roleName: '工作台', label: '工作台',
value: 'dashboard_workplace', value: 'dashboard_workplace',
}, },
{ {
roleName: '基础列表', label: '基础列表',
value: 'basic_list', value: 'basic_list',
}, },
{ {
roleName: '基础列表删除', label: '基础列表删除',
value: 'basic_list_delete', value: 'basic_list_delete',
}, },
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "naive-ui-admin", "name": "naive-ui-admin",
"version": "1.5.2", "version": "1.8.0",
"author": { "author": {
"name": "Ahjung", "name": "Ahjung",
"email": "735878602@qq.com", "email": "735878602@qq.com",
@@ -14,7 +14,10 @@
"build": "vite build && esno ./build/script/postBuild.ts", "build": "vite build && esno ./build/script/postBuild.ts",
"build:no-cache": "yarn clean:cache && npm run build", "build:no-cache": "yarn clean:cache && npm run build",
"report": "cross-env REPORT=true npm run build", "report": "cross-env REPORT=true npm run build",
"preview": "vite preview", "preview": "npm run build && vite preview",
"preview:dist": "vite preview",
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
"clean:lib": "rimraf node_modules",
"build typecheck": "vuedx-typecheck . && vite build", "build typecheck": "vuedx-typecheck . && vite build",
"deploy": "gh-pages -d dist", "deploy": "gh-pages -d dist",
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix", "lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
@@ -27,72 +30,72 @@
"dependencies": { "dependencies": {
"@vicons/antd": "^0.10.0", "@vicons/antd": "^0.10.0",
"@vicons/ionicons5": "^0.10.0", "@vicons/ionicons5": "^0.10.0",
"@vueuse/core": "^5.0.3", "@vueup/vue-quill": "^1.0.0-beta.8",
"axios": "^0.21.1", "@vueuse/core": "^5.3.0",
"blueimp-md5": "^2.18.0", "axios": "^0.21.4",
"date-fns": "^2.23.0", "blueimp-md5": "^2.19.0",
"echarts": "^5.1.2", "date-fns": "^2.28.0",
"element-resize-detector": "^1.2.3", "echarts": "^5.3.2",
"element-resize-detector": "^1.2.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"makeit-captcha": "^1.2.5",
"mitt": "^2.1.0", "mitt": "^2.1.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"naive-ui": "^2.16.0", "naive-ui": "^2.28.4",
"pinia": "^2.0.0-beta.3", "pinia": "^2.0.14",
"qs": "^6.10.1", "qs": "^6.10.3",
"vfonts": "^0.1.0", "vfonts": "^0.1.0",
"vue": "^3.1.2", "vue": "^3.2.33",
"vue-router": "^4.0.10", "vue-router": "^4.0.15",
"vue-types": "^4.0.0", "vue-types": "^4.1.1",
"vuedraggable": "^4.0.3", "vuedraggable": "^4.1.0"
"vuex": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^12.1.4", "@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4", "@commitlint/config-conventional": "^12.1.4",
"@types/lodash": "^4.14.170", "@types/lodash": "^4.14.182",
"@types/node": "^15.12.2", "@types/node": "^15.14.9",
"@typescript-eslint/eslint-plugin": "^4.26.1", "@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.26.1", "@typescript-eslint/parser": "^4.33.0",
"@vitejs/plugin-vue": "^1.2.3", "@vitejs/plugin-vue": "^1.10.2",
"@vitejs/plugin-vue-jsx": "^1.1.5", "@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/compiler-sfc": "3.1.1", "@vue/compiler-sfc": "^3.2.33",
"@vue/eslint-config-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^7.0.0",
"autoprefixer": "^10.3.1", "autoprefixer": "^10.4.7",
"commitizen": "^4.2.4", "commitizen": "^4.2.4",
"core-js": "^3.14.0", "core-js": "^3.22.5",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^7.28.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.5.0",
"eslint-define-config": "^1.0.9", "eslint-define-config": "1.0.9",
"eslint-plugin-jest": "^24.4.0", "eslint-plugin-jest": "^24.7.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.11.1", "eslint-plugin-vue": "^7.20.0",
"esno": "^0.7.3", "esno": "^0.7.3",
"gh-pages": "^3.2.0", "gh-pages": "^3.2.3",
"husky": "^6.0.0", "husky": "^6.0.0",
"jest": "^27.0.6", "jest": "^27.5.1",
"less": "^4.1.1", "less": "^4.1.2",
"less-loader": "^9.0.0", "less-loader": "^9.1.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.2.6",
"postcss": "^8.3.5", "postcss": "^8.4.13",
"prettier": "^2.3.1", "prettier": "^2.6.2",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.3",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2", "stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0", "stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0", "stylelint-order": "^4.1.0",
"stylelint-scss": "^3.19.0", "stylelint-scss": "^3.21.0",
"tailwindcss": "^2.2.7", "tailwindcss": "^2.2.19",
"typescript": "^4.3.5", "typescript": "^4.6.4",
"vite": "2.3.6", "unplugin-vue-components": "^0.17.21",
"vite-plugin-compression": "^0.3.1", "vite": "^2.9.8",
"vite-plugin-html": "^2.0.7", "vite-plugin-compression": "^0.3.6",
"vite-plugin-mock": "^2.9.3", "vite-plugin-html": "^2.1.2",
"vite-plugin-style-import": "^1.0.1", "vite-plugin-mock": "^2.9.6",
"vue-eslint-parser": "^7.8.0" "vite-plugin-style-import": "^1.4.1",
"vue-eslint-parser": "^7.11.0"
}, },
"lint-staged": { "lint-staged": {
"*.{vue,js,ts,tsx}": "eslint --fix" "*.{vue,js,ts,tsx}": "eslint --fix"

7612
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="renderer" content="webkit"/>
<meta name="force-rendering" content="webkit"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="/favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<script src="<%= BASE_URL %>iconfont.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<!-- IE 浏览器跳转提示升级页面 start -->
<script>
if (/*@cc_on!@*/false || (!!window.MSInputMethodContext && !!document.documentMode)) {
window.open("https://support.dmeng.net/upgrade-your-browser.html?referrer="+encodeURIComponent(window.location.href));
};
</script>
<!-- IE 浏览器跳转提示升级页面 end -->
<script>
function isIE() {
if (!!window.ActiveXObject || "ActiveXObject" in window){
return true;
}else{
return false;
}
}
</script>
</body>
</html>

View File

@@ -16,9 +16,9 @@
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, computed, onMounted, onUnmounted } from 'vue'; import { computed, onMounted, onUnmounted } from 'vue';
import { zhCN, dateZhCN, createTheme, inputDark, datePickerDark, darkTheme } from 'naive-ui'; import { zhCN, dateZhCN, darkTheme } from 'naive-ui';
import { LockScreen } from '@/components/Lockscreen'; import { LockScreen } from '@/components/Lockscreen';
import { AppProvider } from '@/components/Application'; import { AppProvider } from '@/components/Application';
import { useLockscreenStore } from '@/store/modules/lockscreen'; import { useLockscreenStore } from '@/store/modules/lockscreen';
@@ -26,86 +26,61 @@
import { useDesignSettingStore } from '@/store/modules/designSetting'; import { useDesignSettingStore } from '@/store/modules/designSetting';
import { lighten } from '@/utils/index'; import { lighten } from '@/utils/index';
export default defineComponent({ const route = useRoute();
name: 'App', const useLockscreen = useLockscreenStore();
components: { LockScreen, AppProvider }, const designStore = useDesignSettingStore();
setup() { const isLock = computed(() => useLockscreen.isLock);
const route = useRoute(); const lockTime = computed(() => useLockscreen.lockTime);
const useLockscreen = useLockscreenStore();
const designStore = useDesignSettingStore();
const isLock = computed(() => useLockscreen.isLock);
const lockTime = computed(() => useLockscreen.lockTime);
/** /**
* @type import('naive-ui').GlobalThemeOverrides * @type import('naive-ui').GlobalThemeOverrides
*/ */
const getThemeOverrides = computed(() => { const getThemeOverrides = computed(() => {
const appTheme = designStore.appTheme; const appTheme = designStore.appTheme;
const lightenStr = lighten(designStore.appTheme, 6); const lightenStr = lighten(designStore.appTheme, 6);
return { return {
common: { common: {
primaryColor: appTheme, primaryColor: appTheme,
primaryColorHover: lightenStr, primaryColorHover: lightenStr,
primaryColorPressed: lightenStr, primaryColorPressed: lightenStr,
}, },
LoadingBar: { LoadingBar: {
colorLoading: appTheme, colorLoading: appTheme,
}, },
}; };
}); });
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined)); const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined));
let timer; let timer;
const timekeeping = () => { const timekeeping = () => {
clearInterval(timer); clearInterval(timer);
if (route.name == 'login' || isLock.value) return; if (route.name == 'login' || isLock.value) return;
// 设置不锁屏 // 设置不锁屏
useLockscreen.setLock(false); useLockscreen.setLock(false);
// 重置锁屏时间 // 重置锁屏时间
useLockscreen.setLockTime(); useLockscreen.setLockTime();
timer = setInterval(() => { timer = setInterval(() => {
// 锁屏倒计时递减 // 锁屏倒计时递减
useLockscreen.setLockTime(lockTime.value - 1); useLockscreen.setLockTime(lockTime.value - 1);
if (lockTime.value <= 0) { if (lockTime.value <= 0) {
// 设置锁屏 // 设置锁屏
useLockscreen.setLock(true); useLockscreen.setLock(true);
return clearInterval(timer); return clearInterval(timer);
} }
}, 1000); }, 1000);
}; };
onMounted(() => { onMounted(() => {
document.addEventListener('mousedown', timekeeping); document.addEventListener('mousedown', timekeeping);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('mousedown', timekeeping); document.removeEventListener('mousedown', timekeeping);
});
return {
darkTheme: createTheme([inputDark, datePickerDark]),
getDarkTheme,
zhCN,
dateZhCN,
isLock,
getThemeOverrides,
};
},
}); });
</script> </script>
<style lang="less"> <style lang="less">
@import 'styles/common.less'; @import 'styles/index.less';
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.35s ease-in;
}
.slide-up-enter-form,
.slide-up-leave-to {
transform: translateY(-100%);
}
</style> </style>

View File

@@ -1,4 +1,4 @@
import http from '@/utils/http/axios'; import { http } from '@/utils/http/axios';
//获取主控台信息 //获取主控台信息
export function getConsoleInfo() { export function getConsoleInfo() {

View File

@@ -1,4 +1,4 @@
import http from '@/utils/http/axios'; import { http } from '@/utils/http/axios';
/** /**
* @description: 根据用户id获取用户菜单 * @description: 根据用户id获取用户菜单

View File

@@ -1,4 +1,4 @@
import http from '@/utils/http/axios'; import { http } from '@/utils/http/axios';
/** /**
* @description: 角色列表 * @description: 角色列表

View File

@@ -1,4 +1,4 @@
import http from '@/utils/http/axios'; import { http } from '@/utils/http/axios';
export interface BasicResponseModel<T = any> { export interface BasicResponseModel<T = any> {
code: number; code: number;

View File

@@ -1,4 +1,4 @@
import http from '@/utils/http/axios'; import { http } from '@/utils/http/axios';
//获取table //获取table
export function getTableList(params) { export function getTableList(params) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -102,11 +102,7 @@
type="primary" type="primary"
text text
icon-placement="right" icon-placement="right"
v-if=" v-if="isInline && getProps.showAdvancedButton"
isInline &&
getSchema.length > (getProps.gridProps?.cols || 0) &&
getProps.showAdvancedButton
"
@click="unfoldToggle" @click="unfoldToggle"
> >
<template #icon> <template #icon>
@@ -212,6 +208,7 @@
return { return {
...gridProps, ...gridProps,
collapsed: isInline.value ? gridCollapsed.value : false, collapsed: isInline.value ? gridCollapsed.value : false,
responsive: 'screen',
}; };
}); });

View File

@@ -1,5 +1,4 @@
import type { FormProps, FormActionType, UseFormReturnType } from '../types/form'; import type { FormProps, FormActionType, UseFormReturnType } from '../types/form';
// @ts-ignore
import type { DynamicProps } from '/#/utils'; import type { DynamicProps } from '/#/utils';
import { ref, onUnmounted, unref, nextTick, watch } from 'vue'; import { ref, onUnmounted, unref, nextTick, watch } from 'vue';

View File

@@ -49,6 +49,7 @@
type="password" type="password"
autofocus autofocus
v-model:value="loginParams.password" v-model:value="loginParams.password"
@keyup.enter="onLogin"
placeholder="请输入登录密码" placeholder="请输入登录密码"
> >
<template #suffix> <template #suffix>

View File

@@ -20,104 +20,94 @@
</n-modal> </n-modal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { import {
defineComponent,
getCurrentInstance, getCurrentInstance,
ref, ref,
nextTick, nextTick,
unref, unref,
toRefs,
reactive,
computed, computed,
useAttrs,
defineEmits,
defineProps,
} from 'vue'; } from 'vue';
import { basicProps } from './props'; import { basicProps } from './props';
import startDrag from '@/utils/Drag'; import startDrag from '@/utils/Drag';
import { deepMerge } from '@/utils'; import { deepMerge } from '@/utils';
import { FormProps } from '@/components/Form'; import { FormProps } from '@/components/Form';
export default defineComponent({ import { ModalProps, ModalMethods } from './type';
name: 'BasicModal',
components: {},
props: {
...basicProps,
},
emits: ['on-close', 'on-ok', 'register'],
setup(props, { emit, attrs }) {
const propsRef = ref<Partial>({});
const state = reactive({ const attrs = useAttrs();
isModal: false, const props = defineProps({ ...basicProps });
subLoading: false, const emit = defineEmits(['on-close', 'on-ok', 'register']);
});
const getProps = computed((): FormProps => { const propsRef = ref(<Partial<ModalProps> | null>null);
const modalProps = { ...props, ...unref(propsRef) };
return { ...modalProps };
});
async function setProps(modalProps: Partial): Promise<void> { const isModal = ref(false);
propsRef.value = deepMerge(unref(propsRef) || {}, modalProps); const subLoading = ref(false);
}
const getBindValue = computed(() => { const getProps = computed((): FormProps => {
return { return { ...props, ...(unref(propsRef) as any) };
...attrs,
...unref(getProps),
};
});
function setSubLoading(status: boolean) {
state.subLoading = status;
}
function openModal() {
state.isModal = true;
nextTick(() => {
const oBox = document.getElementById('basic-modal');
const oBar = document.getElementById('basic-modal-bar');
startDrag(oBar, oBox);
});
}
function closeModal() {
state.isModal = false;
state.subLoading = false;
emit('on-close');
}
function onCloseModal() {
state.isModal = false;
emit('on-close');
}
function handleSubmit() {
state.subLoading = true;
emit('on-ok');
}
const modalMethods: ModalMethods = {
setProps,
openModal,
closeModal,
setSubLoading,
};
const instance = getCurrentInstance();
if (instance) {
emit('register', modalMethods);
}
return {
...toRefs(state),
getBindValue,
openModal,
closeModal,
onCloseModal,
handleSubmit,
setProps,
};
},
}); });
const subBtuText = computed(() => {
const { subBtuText } = propsRef.value as any;
return subBtuText || props.subBtuText;
});
async function setProps(modalProps: Partial<ModalProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || ({} as any), modalProps);
}
const getBindValue = computed(() => {
return {
...attrs,
...unref(getProps),
...unref(propsRef),
};
});
function setSubLoading(status: boolean) {
subLoading.value = status;
}
function openModal() {
isModal.value = true;
nextTick(() => {
const oBox = document.getElementById('basic-modal');
const oBar = document.getElementById('basic-modal-bar');
startDrag(oBar, oBox);
});
}
function closeModal() {
isModal.value = false;
subLoading.value = false;
emit('on-close');
}
function onCloseModal() {
isModal.value = false;
emit('on-close');
}
function handleSubmit() {
subLoading.value = true;
console.log(subLoading.value);
emit('on-ok');
}
const modalMethods: ModalMethods = {
setProps,
openModal,
closeModal,
setSubLoading,
};
const instance = getCurrentInstance();
if (instance) {
emit('register', modalMethods);
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -1,46 +1,41 @@
import { ref, onUnmounted, unref, getCurrentInstance, watch } from 'vue'; import { ref, onUnmounted, unref, getCurrentInstance, watch, nextTick } from 'vue';
import { isProdMode } from '@/utils/env'; import { isProdMode } from '@/utils/env';
import { ReturnMethods } from '../type'; import { ModalMethods, UseModalReturnType } from '../type';
import { getDynamicProps } from '@/utils'; import { getDynamicProps } from '@/utils';
export function useModal(props): (((modalMethod: ReturnMethods) => any) | ReturnMethods)[] { import { tryOnUnmounted } from '@vueuse/core';
const modal = ref<Nullable<ReturnMethods>>(null); export function useModal(props): UseModalReturnType {
const loaded = ref<Nullable<boolean>>(false); const modalRef = ref<Nullable<ModalMethods>>(null);
const currentInstance = getCurrentInstance();
function register(modalMethod: ReturnMethods) {
if (!getCurrentInstance()) {
throw new Error('useModal() can only be used inside setup() or functional components!');
}
isProdMode() &&
onUnmounted(() => {
modal.value = null;
loaded.value = false;
});
if (unref(loaded) && isProdMode() && modalMethod === unref(modal)) return;
modal.value = modalMethod;
watch(
() => props,
() => {
// @ts-ignore
const { setProps } = modal.value;
props && setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
}
);
}
const getInstance = () => { const getInstance = () => {
const instance = unref(modal); const instance = unref(modalRef.value);
if (!instance) { if (!instance) {
console.error('useModal instance is undefined!'); console.error('useModal instance is undefined!');
} }
return instance; return instance;
}; };
const methods: ReturnMethods = { const register = (modalInstance: ModalMethods) => {
isProdMode() &&
tryOnUnmounted(() => {
modalRef.value = null;
});
modalRef.value = modalInstance;
currentInstance?.emit('register', modalInstance);
watch(
() => props,
() => {
props && modalInstance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
}
);
};
const methods: ModalMethods = {
setProps: (props): void => { setProps: (props): void => {
getInstance()?.setProps(props); getInstance()?.setProps(props);
}, },
@@ -54,5 +49,6 @@ export function useModal(props): (((modalMethod: ReturnMethods) => any) | Return
getInstance()?.setSubLoading(status); getInstance()?.setSubLoading(status);
}, },
}; };
return [register, methods]; return [register, methods];
} }

View File

@@ -1,9 +1,19 @@
import type { DialogOptions } from 'naive-ui/lib/dialog';
/** /**
* @description: 弹窗对外暴露的方法 * @description: 弹窗对外暴露的方法
*/ */
export interface ReturnMethods { export interface ModalMethods {
setProps: (props) => void; setProps: (props) => void;
openModal: () => void; openModal: () => void;
closeModal: () => void; closeModal: () => void;
setSubLoading: (status) => void; setSubLoading: (status) => void;
} }
/**
* 支持修改DialogOptions 參數
*/
export type ModalProps = DialogOptions;
export type RegisterFn = (ModalInstance: ModalMethods) => void;
export type UseModalReturnType = [RegisterFn, ModalMethods];

View File

@@ -7,7 +7,7 @@
{{ title }} {{ title }}
<n-tooltip trigger="hover" v-if="titleTooltip"> <n-tooltip trigger="hover" v-if="titleTooltip">
<template #trigger> <template #trigger>
<n-icon size="18" class="ml-1 cursor-pointer text-gray-400"> <n-icon size="18" class="ml-1 text-gray-400 cursor-pointer">
<QuestionCircleOutlined /> <QuestionCircleOutlined />
</n-icon> </n-icon>
</template> </template>
@@ -156,7 +156,7 @@
const { getPaginationInfo, setPagination } = usePagination(getProps); const { getPaginationInfo, setPagination } = usePagination(getProps);
const { getDataSourceRef, getRowKey, reload } = useDataSource( const { getDataSourceRef, getDataSource, getRowKey, reload } = useDataSource(
getProps, getProps,
{ {
getPaginationInfo, getPaginationInfo,
@@ -171,7 +171,7 @@
useColumns(getProps); useColumns(getProps);
const state = reactive({ const state = reactive({
tableSize: 'medium', tableSize: unref(getProps as any).size || 'medium',
isColumnSetting: false, isColumnSetting: false,
}); });
@@ -233,9 +233,6 @@
getCacheColumns, getCacheColumns,
setCacheColumnsField, setCacheColumnsField,
emit, emit,
getSize: () => {
return unref(getBindValues).size;
},
}; };
const getCanResize = computed(() => { const getCanResize = computed(() => {
@@ -283,12 +280,12 @@
...toRefs(state), ...toRefs(state),
tableElRef, tableElRef,
getBindValues, getBindValues,
getDataSource,
densityOptions, densityOptions,
reload, reload,
densitySelect, densitySelect,
updatePage, updatePage,
updatePageSize, updatePageSize,
updateCheckedRowKeys,
pagination, pagination,
tableAction, tableAction,
}; };

View File

@@ -7,23 +7,24 @@
</n-icon> </n-icon>
</div> </div>
<div class="flex editable-cell-content" v-show="isEdit" v-click-outside="onClickOutside"> <div class="flex editable-cell-content" v-show="isEdit" v-click-outside="onClickOutside">
<CellComponent <div class="editable-cell-content-comp">
v-bind="getComponentProps" <CellComponent
:component="getComponent" v-bind="getComponentProps"
:style="getWrapperStyle" :component="getComponent"
:popoverVisible="getRuleVisible" :popoverVisible="getRuleVisible"
:ruleMessage="ruleMessage" :ruleMessage="ruleMessage"
:rule="getRule" :rule="getRule"
:class="getWrapperClass" :class="getWrapperClass"
ref="elRef" ref="elRef"
@options-change="handleOptionsChange" @options-change="handleOptionsChange"
@pressEnter="handleEnter" @pressEnter="handleEnter"
/> />
</div>
<div class="editable-cell-action" v-if="!getRowEditable"> <div class="editable-cell-action" v-if="!getRowEditable">
<n-icon class="cursor-pointer mx-2"> <n-icon class="mx-2 cursor-pointer">
<CheckOutlined @click="handleSubmit" /> <CheckOutlined @click="handleSubmit" />
</n-icon> </n-icon>
<n-icon class="cursor-pointer mx-2"> <n-icon class="mx-2 cursor-pointer">
<CloseOutlined @click="handleCancel" /> <CloseOutlined @click="handleCancel" />
</n-icon> </n-icon>
</div> </div>
@@ -31,7 +32,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import type { CSSProperties, PropType } from 'vue'; import type { PropType } from 'vue';
import type { BasicColumn } from '../../types/table'; import type { BasicColumn } from '../../types/table';
import type { EditRecordRow } from './index'; import type { EditRecordRow } from './index';
@@ -49,7 +50,8 @@
import { set, omit } from 'lodash-es'; import { set, omit } from 'lodash-es';
import { EventEnum } from '@/components/Table/src/componentMap'; import { EventEnum } from '@/components/Table/src/componentMap';
import { milliseconds } from 'date-fns'; import { parseISO, format } from 'date-fns';
import { Fn, LabelValueOptions } from '/#/index';
export default defineComponent({ export default defineComponent({
name: 'EditableCell', name: 'EditableCell',
@@ -92,7 +94,7 @@
const getIsCheckComp = computed(() => { const getIsCheckComp = computed(() => {
const component = unref(getComponent); const component = unref(getComponent);
return ['NCheckbox', 'NSwitch'].includes(component); return ['NCheckbox', 'NRadio'].includes(component);
}); });
const getComponentProps = computed(() => { const getComponentProps = computed(() => {
@@ -103,16 +105,28 @@
const isCheckValue = unref(getIsCheckComp); const isCheckValue = unref(getIsCheckComp);
const valueField = isCheckValue ? 'checked' : 'value'; let valueField = isCheckValue ? 'checked' : 'value';
const val = unref(currentValueRef); const val = unref(currentValueRef);
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val; let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
if (isString(value) && component === 'NDatePicker') { //TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
value = milliseconds(value as Duration); if (component === 'NDatePicker') {
} else if (isArray(value) && component === 'NDatePicker') { if (isString(value)) {
value = value.map((item) => milliseconds(item)); if (compProps.valueFormat) {
valueField = 'formatted-value';
} else {
value = parseISO(value as any).getTime();
}
} else if (isArray(value)) {
if (compProps.valueFormat) {
valueField = 'formatted-value';
} else {
value = value.map((item) => parseISO(item).getTime());
}
}
} }
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined; const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
return { return {
@@ -144,15 +158,6 @@
return option?.label ?? value; return option?.label ?? value;
}); });
const getWrapperStyle = computed((): CSSProperties => {
// if (unref(getIsCheckComp) || unref(getRowEditable)) {
// return {};
// }
return {
width: 'calc(100% - 48px)',
};
});
const getWrapperClass = computed(() => { const getWrapperClass = computed(() => {
const { align = 'center' } = props.column; const { align = 'center' } = props.column;
return `edit-cell-align-${align}`; return `edit-cell-align-${align}`;
@@ -186,6 +191,7 @@
async function handleChange(e: any) { async function handleChange(e: any) {
const component = unref(getComponent); const component = unref(getComponent);
const compProps = props.column?.editComponentProps ?? {};
if (!e) { if (!e) {
currentValueRef.value = e; currentValueRef.value = e;
} else if (e?.target && Reflect.has(e.target, 'value')) { } else if (e?.target && Reflect.has(e.target, 'value')) {
@@ -196,13 +202,20 @@
currentValueRef.value = e; currentValueRef.value = e;
} }
//TODO 这里组件参数格式和dayjs格式不一致 //TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
// if (component === 'NDatePicker') { if (component === 'NDatePicker') {
// let format = (props.column.editComponentProps?.format) if (isNumber(currentValueRef.value)) {
// .replace(/yyyy/g, 'YYYY') if (compProps.valueFormat) {
// .replace(/dd/g, 'DD'); currentValueRef.value = format(currentValueRef.value, compProps.valueFormat);
// currentValueRef.value = dayjs(currentValueRef.value).format(format); }
// } } else if (isArray(currentValueRef.value)) {
if (compProps.valueFormat) {
currentValueRef.value = currentValueRef.value.map((item) => {
format(item, compProps.valueFormat);
});
}
}
}
const onChange = props.column?.editComponentProps?.onChange; const onChange = props.column?.editComponentProps?.onChange;
if (onChange && isFunction(onChange)) onChange(...arguments); if (onChange && isFunction(onChange)) onChange(...arguments);
@@ -304,100 +317,103 @@
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) { function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
if (props.record) { if (props.record) {
/* eslint-disable */ /* eslint-disable */
isArray(props.record[cbs]) isArray(props.record[cbs])
? props.record[cbs]?.push(handle) ? props.record[cbs]?.push(handle)
: (props.record[cbs] = [handle]); : (props.record[cbs] = [handle]);
} }
}
if (props.record) {
initCbs('submitCbs', handleSubmit);
initCbs('validCbs', handleSubmiRule);
initCbs('cancelCbs', handleCancel);
if (props.column.key) {
if (!props.record.editValueRefs) props.record.editValueRefs = {};
props.record.editValueRefs[props.column.key] = currentValueRef;
}
/* eslint-disable */
props.record.onCancelEdit = () => {
isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
};
/* eslint-disable */
props.record.onSubmitEdit = async() => {
if (isArray(props.record?.submitCbs)) {
const validFns = (props.record?.validCbs || []).map((fn) => fn());
const res = await Promise.all(validFns);
const pass = res.every((item) => !!item);
if (!pass) return;
const submitFns = props.record?.submitCbs || [];
submitFns.forEach((fn) => fn(false, false));
table.emit?.('edit-row-end');
return true;
} }
};
}
return { if (props.record) {
isEdit, initCbs('submitCbs', handleSubmit);
handleEdit, initCbs('validCbs', handleSubmiRule);
currentValueRef, initCbs('cancelCbs', handleCancel);
handleSubmit,
handleChange, if (props.column.key) {
handleCancel, if (!props.record.editValueRefs) props.record.editValueRefs = {};
elRef, props.record.editValueRefs[props.column.key] = currentValueRef;
getComponent, }
getRule, /* eslint-disable */
onClickOutside, props.record.onCancelEdit = () => {
ruleMessage, isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
getRuleVisible, };
getComponentProps, /* eslint-disable */
handleOptionsChange, props.record.onSubmitEdit = async () => {
getWrapperStyle, if (isArray(props.record?.submitCbs)) {
getWrapperClass, const validFns = (props.record?.validCbs || []).map((fn) => fn());
getRowEditable,
getValues, const res = await Promise.all(validFns);
handleEnter,
// getSize, const pass = res.every((item) => !!item);
};
}, if (!pass) return;
const submitFns = props.record?.submitCbs || [];
submitFns.forEach((fn) => fn(false, false));
table.emit?.('edit-row-end');
return true;
}
};
}
return {
isEdit,
handleEdit,
currentValueRef,
handleSubmit,
handleChange,
handleCancel,
elRef,
getComponent,
getRule,
onClickOutside,
ruleMessage,
getRuleVisible,
getComponentProps,
handleOptionsChange,
getWrapperClass,
getRowEditable,
getValues,
handleEnter,
// getSize,
};
},
}); });
</script> </script>
<style lang="less"> <style lang="less">
.editable-cell { .editable-cell {
&-content { &-content {
position: relative; position: relative;
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-word; word-break: break-word;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
.edit-icon { &-comp {
font-size: 14px; flex: 1;
//position: absolute; }
//top: 4px;
//right: 0; .edit-icon {
display: none; font-size: 14px;
width: 20px; //position: absolute;
cursor: pointer; //top: 4px;
//right: 0;
display: none;
width: 20px;
cursor: pointer;
}
&:hover {
.edit-icon {
display: inline-block;
}
}
}
&-action {
display: flex;
align-items: center;
justify-content: center;
}
} }
&:hover {
.edit-icon {
display: inline-block;
}
}
}
&-action {
display: flex;
align-items: center;
justify-content: center;
}
}
</style> </style>

View File

@@ -25,13 +25,26 @@
</template> </template>
<div class="table-toolbar-inner"> <div class="table-toolbar-inner">
<n-checkbox-group v-model:value="checkList" @update:value="onChange"> <n-checkbox-group v-model:value="checkList" @update:value="onChange">
<Draggable v-model="columnsList" animation="300" item-key="key" @end="draggableEnd"> <Draggable
v-model="columnsList"
animation="300"
item-key="key"
filter=".no-draggable"
:move="onMove"
@end="draggableEnd"
>
<template #item="{ element }"> <template #item="{ element }">
<div <div
class="table-toolbar-inner-checkbox" class="table-toolbar-inner-checkbox"
:class="{ 'table-toolbar-inner-checkbox-dark': getDarkTheme === true }" :class="{
'table-toolbar-inner-checkbox-dark': getDarkTheme === true,
'no-draggable': element.draggable === false,
}"
> >
<span class="drag-icon"> <span
class="drag-icon"
:class="{ 'drag-icon-hidden': element.draggable === false }"
>
<n-icon size="18"> <n-icon size="18">
<DragOutlined /> <DragOutlined />
</n-icon> </n-icon>
@@ -88,7 +101,6 @@
VerticalRightOutlined, VerticalRightOutlined,
VerticalLeftOutlined, VerticalLeftOutlined,
} from '@vicons/antd'; } from '@vicons/antd';
// @ts-ignore
import Draggable from 'vuedraggable/src/vuedraggable'; import Draggable from 'vuedraggable/src/vuedraggable';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting'; import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
@@ -212,6 +224,11 @@
} }
} }
function onMove(e) {
if (e.draggedContext.element.draggable === false) return false;
return true;
}
//固定 //固定
function fixedColumn(item, fixed) { function fixedColumn(item, fixed) {
if (!state.checkList.includes(item.key)) return; if (!state.checkList.includes(item.key)) return;
@@ -233,6 +250,7 @@
onChange, onChange,
onCheckAll, onCheckAll,
onSelection, onSelection,
onMove,
resetColumns, resetColumns,
fixedColumn, fixedColumn,
draggableEnd, draggableEnd,
@@ -276,6 +294,10 @@
display: inline-flex; display: inline-flex;
margin-right: 8px; margin-right: 8px;
cursor: move; cursor: move;
&-hidden {
visibility: hidden;
cursor: default;
}
} }
.fixed-item { .fixed-item {

View File

@@ -48,10 +48,11 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columns = cloneDeep(pageColumns); const columns = cloneDeep(pageColumns);
return columns return columns
.filter((column) => { .filter((column) => {
// @ts-ignore return hasPermission(column.auth as string[]) && isIfShow(column);
return hasPermission(column.auth) && isIfShow(column);
}) })
.map((column) => { .map((column) => {
//默认 ellipsis 为true
column.ellipsis = typeof column.ellipsis === 'undefined' ? { tooltip: true } : false;
const { edit } = column; const { edit } = column;
if (edit) { if (edit) {
column.render = renderEditCell(column); column.render = renderEditCell(column);
@@ -91,10 +92,10 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) { function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
const { actionColumn } = unref(propsRef); const { actionColumn } = unref(propsRef);
if (!actionColumn) return; if (!actionColumn) return;
// @ts-ignore !columns.find((col) => col.key === 'action') &&
columns.push({ columns.push({
...actionColumn, ...(actionColumn as any),
}); });
} }
//设置 //设置
@@ -127,7 +128,7 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
} }
//获取 //获取
function getColumns() { function getColumns(): BasicColumn[] {
const columns = toRaw(unref(getColumnsRef)); const columns = toRaw(unref(getColumnsRef));
return columns.map((item) => { return columns.map((item) => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }; return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined };

View File

@@ -9,7 +9,7 @@ export function useDataSource(
{ getPaginationInfo, setPagination, setLoading, tableData }, { getPaginationInfo, setPagination, setLoading, tableData },
emit emit
) { ) {
const dataSourceRef = ref([]); const dataSourceRef = ref<Recordable[]>([]);
watchEffect(() => { watchEffect(() => {
tableData.value = unref(dataSourceRef); tableData.value = unref(dataSourceRef);
@@ -47,6 +47,7 @@ export function useDataSource(
try { try {
setLoading(true); setLoading(true);
const { request, pagination }: any = unref(propsRef); const { request, pagination }: any = unref(propsRef);
if (!request) return;
//组装分页信息 //组装分页信息
const pageField = APISETTING.pageField; const pageField = APISETTING.pageField;
const sizeField = APISETTING.sizeField; const sizeField = APISETTING.sizeField;

View File

@@ -3,7 +3,7 @@ import type { BasicTableProps } from '../types/table';
import { computed, unref, ref, ComputedRef } from 'vue'; import { computed, unref, ref, ComputedRef } from 'vue';
import { isBoolean } from '@/utils/is'; import { isBoolean } from '@/utils/is';
import { DEFAULTPAGESIZE, PAGESIZES } from '../const'; import { APISETTING, DEFAULTPAGESIZE, PAGESIZES } from '../const';
export function usePagination(refProps: ComputedRef<BasicTableProps>) { export function usePagination(refProps: ComputedRef<BasicTableProps>) {
const configRef = ref<PaginationProps>({}); const configRef = ref<PaginationProps>({});
@@ -14,6 +14,7 @@ export function usePagination(refProps: ComputedRef<BasicTableProps>) {
if (!unref(show) || (isBoolean(pagination) && !pagination)) { if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false; return false;
} }
const { totalField } = APISETTING;
return { return {
pageSize: DEFAULTPAGESIZE, pageSize: DEFAULTPAGESIZE,
pageSizes: PAGESIZES, pageSizes: PAGESIZES,
@@ -21,6 +22,7 @@ export function usePagination(refProps: ComputedRef<BasicTableProps>) {
showQuickJumper: true, showQuickJumper: true,
...(isBoolean(pagination) ? {} : pagination), ...(isBoolean(pagination) ? {} : pagination),
...unref(configRef), ...unref(configRef),
pageCount: unref(configRef)[totalField],
}; };
}); });

View File

@@ -16,7 +16,7 @@ export const basicProps = {
type: String, type: String,
default: 'medium', default: 'medium',
}, },
tableData: { dataSource: {
type: [Object], type: [Object],
default: () => [], default: () => [],
}, },
@@ -28,7 +28,6 @@ export const basicProps = {
request: { request: {
type: Function as PropType<(...arg: any[]) => Promise<any>>, type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null, default: null,
required: true,
}, },
rowKey: { rowKey: {
type: [String, Function] as PropType<string | ((record) => string)>, type: [String, Function] as PropType<string | ((record) => string)>,

View File

@@ -14,6 +14,8 @@ export interface BasicColumn extends TableBaseColumn {
auth?: string[]; auth?: string[];
// 业务控制是否显示 // 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean); ifShow?: boolean | ((column: BasicColumn) => boolean);
// 控制是否支持拖拽,默认支持
draggable?: boolean;
} }
export interface TableActionType { export interface TableActionType {

View File

@@ -1,7 +1,5 @@
// @ts-ignore
import { NButton } from 'naive-ui'; import { NButton } from 'naive-ui';
import { RoleEnum } from '@/enums/roleEnum'; import { PermissionsEnum } from '@/enums/permissionsEnum';
// @ts-ignore
export interface ActionItem extends NButton.props { export interface ActionItem extends NButton.props {
onClick?: Fn; onClick?: Fn;
label?: string; label?: string;
@@ -11,7 +9,7 @@ export interface ActionItem extends NButton.props {
disabled?: boolean; disabled?: boolean;
divider?: boolean; divider?: boolean;
// 权限编码控制是否显示 // 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[]; auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
// 业务控制是否显示 // 业务控制是否显示
ifShow?: boolean | ((action: ActionItem) => boolean); ifShow?: boolean | ((action: ActionItem) => boolean);
} }

View File

@@ -14,10 +14,10 @@
<img :src="item" /> <img :src="item" />
</div> </div>
<div class="img-box-actions"> <div class="img-box-actions">
<n-icon size="18" class="action-icon mx-2" @click="preview(item)"> <n-icon size="18" class="mx-2 action-icon" @click="preview(item)">
<EyeOutlined /> <EyeOutlined />
</n-icon> </n-icon>
<n-icon size="18" class="action-icon mx-2" @click="remove(index)"> <n-icon size="18" class="mx-2 action-icon" @click="remove(index)">
<DeleteOutlined /> <DeleteOutlined />
</n-icon> </n-icon>
</div> </div>
@@ -36,7 +36,7 @@
@before-upload="beforeUpload" @before-upload="beforeUpload"
@finish="finish" @finish="finish"
> >
<div class="flex justify-center flex-col"> <div class="flex flex-col justify-center">
<n-icon size="18" class="m-auto"> <n-icon size="18" class="m-auto">
<PlusOutlined /> <PlusOutlined />
</n-icon> </n-icon>
@@ -68,7 +68,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, toRefs, reactive, computed } from 'vue'; import { defineComponent, toRefs, reactive, computed, watch } from 'vue';
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd'; import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { basicProps } from './props'; import { basicProps } from './props';
import { useMessage, useDialog } from 'naive-ui'; import { useMessage, useDialog } from 'naive-ui';
@@ -101,16 +101,19 @@
const state = reactive({ const state = reactive({
showModal: false, showModal: false,
previewUrl: '', previewUrl: '',
originalImgList: [], originalImgList: [] as string[],
imgList: [], imgList: [] as string[],
}); });
//赋值默认图片显示 //赋值默认图片显示
if (props.value.length) { watch(
state.imgList = props.value.map((item) => { () => props.value,
return getImgUrl(item); () => {
}); imgList.value = props.value.map((item) => {
} return getImgUrl(item);
});
}
);
//预览 //预览
function preview(url: string) { function preview(url: string) {
@@ -176,7 +179,7 @@
const result = res[infoField]; const result = res[infoField];
//成功 //成功
if (code === ResultEnum.SUCCESS) { if (code === ResultEnum.SUCCESS) {
let imgUrl = getImgUrl(result.photo); let imgUrl: string = getImgUrl(result.photo);
state.imgList.push(imgUrl); state.imgList.push(imgUrl);
state.originalImgList.push(result.photo); state.originalImgList.push(result.photo);
emit('uploadChange', state.originalImgList); emit('uploadChange', state.originalImgList);
@@ -220,6 +223,7 @@
&:hover { &:hover {
background: 0 0; background: 0 0;
.upload-card-item-info::before { .upload-card-item-info::before {
opacity: 1; opacity: 1;
} }

View File

@@ -0,0 +1,4 @@
export interface PermissionsEnum {
value: string;
label: string;
}

View File

@@ -8,6 +8,8 @@ export function useProjectSetting() {
const getNavTheme = computed(() => projectStore.navTheme); const getNavTheme = computed(() => projectStore.navTheme);
const getIsMobile = computed(() => projectStore.isMobile);
const getHeaderSetting = computed(() => projectStore.headerSetting); const getHeaderSetting = computed(() => projectStore.headerSetting);
const getMultiTabsSetting = computed(() => projectStore.multiTabsSetting); const getMultiTabsSetting = computed(() => projectStore.multiTabsSetting);
@@ -20,14 +22,21 @@ export function useProjectSetting() {
const getShowFooter = computed(() => projectStore.showFooter); const getShowFooter = computed(() => projectStore.showFooter);
const getIsPageAnimate = computed(() => projectStore.isPageAnimate);
const getPageAnimateType = computed(() => projectStore.pageAnimateType);
return { return {
getNavMode, getNavMode,
getNavTheme, getNavTheme,
getIsMobile,
getHeaderSetting, getHeaderSetting,
getMultiTabsSetting, getMultiTabsSetting,
getMenuSetting, getMenuSetting,
getCrumbsSetting, getCrumbsSetting,
getPermissionMode, getPermissionMode,
getShowFooter, getShowFooter,
getIsPageAnimate,
getPageAnimateType,
}; };
} }

62
src/hooks/web/usePage.ts Normal file
View File

@@ -0,0 +1,62 @@
import type { RouteLocationRaw, Router } from 'vue-router';
import { PageEnum } from '@/enums/pageEnum';
import { RedirectName } from '@/router/constant';
import { useRouter } from 'vue-router';
import { isString } from '@/utils/is';
import { unref } from 'vue';
export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum };
function handleError(e: Error) {
console.error(e);
}
/**
* 页面切换
*/
export function useGo(_router?: Router) {
let router;
if (!_router) {
router = useRouter();
}
const { push, replace } = _router || router;
function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) {
if (!opt) {
return;
}
if (isString(opt)) {
isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError);
} else {
const o = opt as RouteLocationRaw;
isReplace ? replace(o).catch(handleError) : push(o).catch(handleError);
}
}
return go;
}
/**
* 重做当前页面
*/
export const useRedo = (_router?: Router) => {
const { push, currentRoute } = _router || useRouter();
const { query, params = {}, name, fullPath } = unref(currentRoute.value);
function redo(): Promise<boolean> {
return new Promise((resolve) => {
if (name === RedirectName) {
resolve(false);
return;
}
if (name && Object.keys(params).length > 0) {
params['_redirect_type'] = 'name';
params['path'] = String(name);
} else {
params['_redirect_type'] = 'path';
params['path'] = fullPath;
}
push({ name: RedirectName, params, query }).then(() => resolve(true));
});
}
return redo;
};

View File

@@ -7,8 +7,8 @@ export function usePermission() {
* 检查权限 * 检查权限
* @param accesses * @param accesses
*/ */
function _someRoles(accesses: string[]) { function _somePermissions(accesses: string[]) {
return userStore.getRoles.some((item) => { return userStore.getPermissions.some((item) => {
const { value }: any = item; const { value }: any = item;
return accesses.includes(value); return accesses.includes(value);
}); });
@@ -20,7 +20,7 @@ export function usePermission() {
* */ * */
function hasPermission(accesses: string[]): boolean { function hasPermission(accesses: string[]): boolean {
if (!accesses || !accesses.length) return true; if (!accesses || !accesses.length) return true;
return _someRoles(accesses); return _somePermissions(accesses);
} }
/** /**
@@ -28,9 +28,9 @@ export function usePermission() {
* @param accesses * @param accesses
*/ */
function hasEveryPermission(accesses: string[]): boolean { function hasEveryPermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles; const permissionsList = userStore.getPermissions;
if (Array.isArray(accesses)) { if (Array.isArray(accesses)) {
return accesses.every((access) => !!rolesList[access]); return permissionsList.every((access: any) => accesses.includes(access.value));
} }
throw new Error(`[hasEveryPermission]: ${accesses} should be a array !`); throw new Error(`[hasEveryPermission]: ${accesses} should be a array !`);
} }
@@ -41,9 +41,9 @@ export function usePermission() {
* @param accessMap * @param accessMap
*/ */
function hasSomePermission(accesses: string[]): boolean { function hasSomePermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles; const permissionsList = userStore.getPermissions;
if (Array.isArray(accesses)) { if (Array.isArray(accesses)) {
return accesses.some((access) => !!rolesList[access]); return permissionsList.some((access: any) => accesses.includes(access.value));
} }
throw new Error(`[hasSomePermission]: ${accesses} should be a array !`); throw new Error(`[hasSomePermission]: ${accesses} should be a array !`);
} }

View File

@@ -1,10 +1,10 @@
<template> <template>
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement" :native-scrollbar="false"> <n-drawer v-model:show="isDrawer" :width="width" :placement="placement">
<n-drawer-content :title="title"> <n-drawer-content :title="title" :native-scrollbar="false">
<div class="drawer"> <div class="drawer">
<n-divider title-placement="center">主题</n-divider> <n-divider title-placement="center">主题</n-divider>
<div class="drawer-setting-item justify-center dark-switch"> <div class="justify-center drawer-setting-item dark-switch">
<n-tooltip placement="bottom"> <n-tooltip placement="bottom">
<template #trigger> <template #trigger>
<n-switch v-model:value="designStore.darkTheme" class="dark-theme-switch"> <n-switch v-model:value="designStore.darkTheme" class="dark-theme-switch">
@@ -20,7 +20,7 @@
</template> </template>
</n-switch> </n-switch>
</template> </template>
<span>色主题</span> <span>{{ designStore.darkTheme ? '深' : '浅' }}色主题</span>
</n-tooltip> </n-tooltip>
</div> </div>
@@ -116,9 +116,7 @@
</n-tooltip> </n-tooltip>
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'light'" /> <n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'light'" />
</div> </div>
</div>
<div class="drawer-setting-item align-items-top">
<div class="drawer-setting-item-style"> <div class="drawer-setting-item-style">
<n-tooltip placement="top"> <n-tooltip placement="top">
<template #trigger> <template #trigger>
@@ -133,14 +131,13 @@
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'header-dark'" /> <n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'header-dark'" />
</div> </div>
</div> </div>
<n-divider title-placement="center">界面功能</n-divider> <n-divider title-placement="center">界面功能</n-divider>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> 分割菜单 </div> <div class="drawer-setting-item-title"> 分割菜单 </div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
<n-switch <n-switch
:disabled="settingStore.navMode === 'horizontal-mix' ? false : true" :disabled="settingStore.navMode !== 'horizontal-mix'"
v-model:value="settingStore.menuSetting.mixMenu" v-model:value="settingStore.menuSetting.mixMenu"
/> />
</div> </div>
@@ -206,6 +203,22 @@
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<n-divider title-placement="center">动画</n-divider>
<div class="drawer-setting-item">
<div class="drawer-setting-item-title"> 禁用动画 </div>
<div class="drawer-setting-item-action">
<n-switch v-model:value="settingStore.isPageAnimate" />
</div>
</div>
<div class="drawer-setting-item">
<div class="drawer-setting-item-title"> 动画类型 </div>
<div class="drawer-setting-item-select">
<n-select v-model:value="settingStore.pageAnimateType" :options="animateOptions" />
</div>
</div>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<n-alert type="warning" :showIcon="false"> <n-alert type="warning" :showIcon="false">
<p>{{ alertText }}</p> <p>{{ alertText }}</p>
@@ -217,12 +230,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, watch } from 'vue'; import { defineComponent, reactive, toRefs, unref, watch, computed } from 'vue';
import { useProjectSettingStore } from '@/store/modules/projectSetting'; import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useDesignSettingStore } from '@/store/modules/designSetting'; import { useDesignSettingStore } from '@/store/modules/designSetting';
import { CheckOutlined } from '@vicons/antd'; import { CheckOutlined } from '@vicons/antd';
import { Moon, SunnySharp } from '@vicons/ionicons5'; import { Moon, SunnySharp } from '@vicons/ionicons5';
import { darkTheme } from 'naive-ui'; import { darkTheme } from 'naive-ui';
import { animates as animateOptions } from '@/settings/animateSetting';
export default defineComponent({ export default defineComponent({
name: 'ProjectSetting', name: 'ProjectSetting',
@@ -257,6 +271,10 @@
} }
); );
const directionsOptions = computed(() => {
return animateOptions.find((item) => item.value == unref(settingStore.pageAnimateType));
});
function openDrawer() { function openDrawer() {
state.isDrawer = true; state.isDrawer = true;
} }
@@ -278,11 +296,7 @@
function togNavMode(mode) { function togNavMode(mode) {
settingStore.navMode = mode; settingStore.navMode = mode;
// if (mode === 'header-dark') { settingStore.menuSetting.mixMenu = false;
// settingStore.setNavTheme('dark');
// } else {
// settingStore.setNavTheme('light');
// }
} }
return { return {
@@ -295,6 +309,8 @@
darkTheme, darkTheme,
openDrawer, openDrawer,
closeDrawer, closeDrawer,
animateOptions,
directionsOptions,
}; };
}, },
}); });
@@ -329,6 +345,10 @@
flex: 0 0 auto; flex: 0 0 auto;
} }
&-select {
flex: 1;
}
.theme-item { .theme-item {
width: 20px; width: 20px;
min-width: 20px; min-width: 20px;
@@ -338,6 +358,7 @@
border-radius: 2px; border-radius: 2px;
margin: 0 5px 5px 0; margin: 0 5px 5px 0;
text-align: center; text-align: center;
line-height: 14px;
.n-icon { .n-icon {
color: #fff; color: #fff;

View File

@@ -5,6 +5,10 @@
class="layout-header-left" class="layout-header-left"
v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)" v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
> >
<div class="logo" v-if="navMode === 'horizontal'">
<img src="~@/assets/images/logo.png" alt="" />
<h2 v-show="!collapsed" class="title">NaiveUiAdmin</h2>
</div>
<AsideMenu <AsideMenu
v-model:collapsed="collapsed" v-model:collapsed="collapsed"
v-model:location="getMenuLocation" v-model:location="getMenuLocation"
@@ -353,6 +357,27 @@
display: flex; display: flex;
align-items: center; align-items: center;
.logo {
display: flex;
align-items: center;
justify-content: center;
height: 64px;
line-height: 64px;
overflow: hidden;
white-space: nowrap;
padding-left: 10px;
img {
width: auto;
height: 32px;
margin-right: 10px;
}
.title {
margin-bottom: 0;
}
}
::v-deep(.ant-breadcrumb span:last-child .link-text) { ::v-deep(.ant-breadcrumb span:last-child .link-text) {
color: #515a6e; color: #515a6e;
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="logo"> <div class="logo">
<img src="~@/assets/images/logo.png" alt="" /> <img src="~@/assets/images/logo.png" alt="" :class="{ 'mr-2': !collapsed }" />
<h2 v-show="!collapsed" class="title">&nbsp;NaiveUiAdmin</h2> <h2 v-show="!collapsed" class="title">NaiveUiAdmin</h2>
</div> </div>
</template> </template>
@@ -32,7 +32,6 @@
} }
.title { .title {
color: white;
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<RouterView> <RouterView>
<template #default="{ Component, route }"> <template #default="{ Component, route }">
<transition name="zoom-fade" mode="out-in" appear> <transition :name="getTransitionName" mode="out-in" appear>
<keep-alive v-if="keepAliveComponents" :include="keepAliveComponents"> <keep-alive v-if="keepAliveComponents" :include="keepAliveComponents">
<component :is="Component" :key="route.fullPath" /> <component :is="Component" :key="route.fullPath" />
</keep-alive> </keep-alive>
@@ -12,8 +12,9 @@
</template> </template>
<script> <script>
import { defineComponent, computed } from 'vue'; import { defineComponent, computed, unref } from 'vue';
import { useAsyncRouteStore } from '@/store/modules/asyncRoute'; import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
export default defineComponent({ export default defineComponent({
name: 'MainView', name: 'MainView',
@@ -29,11 +30,18 @@
}, },
}, },
setup() { setup() {
const { getIsPageAnimate, getPageAnimateType } = useProjectSetting();
const asyncRouteStore = useAsyncRouteStore(); const asyncRouteStore = useAsyncRouteStore();
// 需要缓存的路由组件 // 需要缓存的路由组件
const keepAliveComponents = computed(() => asyncRouteStore.keepAliveComponents); const keepAliveComponents = computed(() => asyncRouteStore.keepAliveComponents);
const getTransitionName = computed(() => {
return unref(getIsPageAnimate) ? unref(getPageAnimateType) : '';
});
return { return {
keepAliveComponents, keepAliveComponents,
getTransitionName,
}; };
}, },
}); });

View File

@@ -20,6 +20,7 @@
import { useAsyncRouteStore } from '@/store/modules/asyncRoute'; import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { generatorMenu, generatorMenuMix } from '@/utils'; import { generatorMenu, generatorMenuMix } from '@/utils';
import { useProjectSettingStore } from '@/store/modules/projectSetting'; import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
export default defineComponent({ export default defineComponent({
name: 'Menu', name: 'Menu',
@@ -40,7 +41,8 @@
default: 'left', default: 'left',
}, },
}, },
setup(props) { emits: ['update:collapsed', 'clickMenuItem'],
setup(props, { emit }) {
// 当前路由 // 当前路由
const currentRoute = useRoute(); const currentRoute = useRoute();
const router = useRouter(); const router = useRouter();
@@ -50,6 +52,10 @@
const selectedKeys = ref<string>(currentRoute.name as string); const selectedKeys = ref<string>(currentRoute.name as string);
const headerMenuSelectKey = ref<string>(''); const headerMenuSelectKey = ref<string>('');
const { getNavMode } = useProjectSetting();
const navMode = getNavMode;
// 获取当前打开的子菜单 // 获取当前打开的子菜单
const matched = currentRoute.matched; const matched = currentRoute.matched;
@@ -64,7 +70,10 @@
}); });
const getSelectedKeys = computed(() => { const getSelectedKeys = computed(() => {
return props.location === 'left' ? unref(selectedKeys) : unref(headerMenuSelectKey); let location = props.location;
return location === 'left' || (location === 'header' && unref(navMode) === 'horizontal')
? unref(selectedKeys)
: unref(headerMenuSelectKey);
}); });
// 监听分割菜单 // 监听分割菜单
@@ -72,17 +81,18 @@
() => settingStore.menuSetting.mixMenu, () => settingStore.menuSetting.mixMenu,
() => { () => {
updateMenu(); updateMenu();
if (props.collapsed) {
emit('update:collapsed', !props.collapsed);
}
} }
); );
// 监听菜单收缩状态 // 监听菜单收缩状态
watch( // watch(
() => props.collapsed, // () => props.collapsed,
(newVal) => { // (newVal) => {
state.openKeys = newVal ? [] : getOpenKeys; // }
selectedKeys.value = currentRoute.name as string; // );
}
);
// 跟随页面路由变化,切换菜单选中状态 // 跟随页面路由变化,切换菜单选中状态
watch( watch(
@@ -115,6 +125,7 @@
} else { } else {
router.push({ name: key }); router.push({ name: key });
} }
emit('clickMenuItem' as any, key);
} }
//展开菜单 //展开菜单

View File

@@ -5,6 +5,7 @@
'tabs-view-fix': multiTabsSetting.fixed, 'tabs-view-fix': multiTabsSetting.fixed,
'tabs-view-fixed-header': isMultiHeaderFixed, 'tabs-view-fixed-header': isMultiHeaderFixed,
'tabs-view-default-background': getDarkTheme === false, 'tabs-view-default-background': getDarkTheme === false,
'tabs-view-dark-background': getDarkTheme === true,
}" }"
:style="getChangeStyle" :style="getChangeStyle"
> >
@@ -29,27 +30,22 @@
</n-icon> </n-icon>
</span> </span>
<div ref="navScroll" class="tabs-card-scroll"> <div ref="navScroll" class="tabs-card-scroll">
<div ref="navRef" class="tabs-card-nav" :style="getNavStyle"> <Draggable :list="tabsList" animation="300" item-key="fullPath" class="flex">
<Draggable :list="tabsList" animation="300" item-key="fullPath" class="flex"> <template #item="{ element }">
<template #item="{ element }"> <div
<div :id="`tag${element.fullPath.split('/').join('\/')}`"
class="tabs-card-scroll-item" class="tabs-card-scroll-item"
:class="{ 'active-item': activeKey === element.path }" :class="{ 'active-item': activeKey === element.path }"
@click.stop="goPage(element)" @click.stop="goPage(element)"
@contextmenu="handleContextMenu($event, element)" @contextmenu="handleContextMenu($event, element)"
> >
<span>{{ element.meta.title }}</span> <span>{{ element.meta.title }}</span>
<n-icon <n-icon size="14" @click.stop="closeTabItem(element)" v-if="!element.meta.affix">
size="14" <CloseOutlined />
@click.stop="closeTabItem(element)" </n-icon>
v-if="element.path != baseHome" </div>
> </template>
<CloseOutlined /> </Draggable>
</n-icon>
</div>
</template>
</Draggable>
</div>
</div> </div>
</div> </div>
<div class="tabs-close"> <div class="tabs-close">
@@ -59,7 +55,7 @@
placement="bottom-end" placement="bottom-end"
:options="TabsMenuOptions" :options="TabsMenuOptions"
> >
<div class="tabs-close-btn" @click.prevent> <div class="tabs-close-btn">
<n-icon size="16" color="#515a6e"> <n-icon size="16" color="#515a6e">
<DownOutlined /> <DownOutlined />
</n-icon> </n-icon>
@@ -86,7 +82,6 @@
computed, computed,
ref, ref,
toRefs, toRefs,
toRaw,
unref, unref,
provide, provide,
watch, watch,
@@ -101,8 +96,7 @@
import { RouteItem } from '@/store/modules/tabsView'; import { RouteItem } from '@/store/modules/tabsView';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting'; import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
// @ts-ignore import Draggable from 'vuedraggable';
import Draggable from 'vuedraggable/src/vuedraggable';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { import {
DownOutlined, DownOutlined,
@@ -113,10 +107,12 @@
LeftOutlined, LeftOutlined,
RightOutlined, RightOutlined,
} from '@vicons/antd'; } from '@vicons/antd';
import { renderIcon } from '@/utils/index'; import { renderIcon } from '@/utils';
import elementResizeDetectorMaker from 'element-resize-detector'; import elementResizeDetectorMaker from 'element-resize-detector';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting'; import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
import { useProjectSettingStore } from '@/store/modules/projectSetting'; import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useThemeVars } from 'naive-ui';
import { useGo } from '@/hooks/web/usePage';
export default defineComponent({ export default defineComponent({
name: 'TabsView', name: 'TabsView',
@@ -133,8 +129,8 @@
}, },
}, },
setup(props) { setup(props) {
const { getDarkTheme } = useDesignSetting(); const { getDarkTheme, getAppTheme } = useDesignSetting();
const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting } = const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting, getIsMobile } =
useProjectSetting(); useProjectSetting();
const settingStore = useProjectSettingStore(); const settingStore = useProjectSettingStore();
@@ -143,17 +139,24 @@
const router = useRouter(); const router = useRouter();
const tabsViewStore = useTabsViewStore(); const tabsViewStore = useTabsViewStore();
const asyncRouteStore = useAsyncRouteStore(); const asyncRouteStore = useAsyncRouteStore();
const navRef: any = ref(null);
const navScroll: any = ref(null); const navScroll: any = ref(null);
const navWrap: any = ref(null); const navWrap: any = ref(null);
const isCurrent = ref(false); const isCurrent = ref(false);
const go = useGo();
const themeVars = useThemeVars();
const getCardColor = computed(() => {
return themeVars.value.cardColor;
});
const getBaseColor = computed(() => {
return themeVars.value.textColor1;
});
const state = reactive({ const state = reactive({
activeKey: route.fullPath, activeKey: route.fullPath,
scrollable: false, scrollable: false,
navStyle: {
transform: '',
},
dropdownX: 0, dropdownX: 0,
dropdownY: 0, dropdownY: 0,
showDropdown: false, showDropdown: false,
@@ -172,10 +175,7 @@
const currentRoute = useRoute(); const currentRoute = useRoute();
const navMode = unref(getNavMode); const navMode = unref(getNavMode);
if (unref(navMode) != 'horizontal-mix') return true; if (unref(navMode) != 'horizontal-mix') return true;
if (unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot) { return !(unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot);
return false;
}
return true;
}); });
//动态组装样式 菜单缩进 //动态组装样式 菜单缩进
@@ -190,6 +190,13 @@
: collapsed : collapsed
? `${minMenuWidth}px` ? `${minMenuWidth}px`
: `${menuWidth}px`; : `${menuWidth}px`;
if (getIsMobile.value) {
return {
left: '0px',
width: '100%',
};
}
return { return {
left: lenNum, left: lenNum,
width: `calc(100% - ${!fixed ? '0px' : lenNum})`, width: `calc(100% - ${!fixed ? '0px' : lenNum})`,
@@ -198,7 +205,7 @@
//tags 右侧下拉菜单 //tags 右侧下拉菜单
const TabsMenuOptions = computed(() => { const TabsMenuOptions = computed(() => {
const isDisabled = unref(tabsList).length <= 1 ? true : false; const isDisabled = unref(tabsList).length <= 1;
return [ return [
{ {
label: '刷新当前', label: '刷新当前',
@@ -226,17 +233,27 @@
]; ];
}); });
let routes: RouteItem[] = []; let cacheRoutes: RouteItem[] = [];
const simpleRoute = getSimpleRoute(route);
try { try {
const routesStr = storage.get(TABS_ROUTES) as string | null | undefined; const routesStr = storage.get(TABS_ROUTES) as string | null | undefined;
routes = routesStr ? JSON.parse(routesStr) : [getSimpleRoute(route)]; cacheRoutes = routesStr ? JSON.parse(routesStr) : [simpleRoute];
} catch (e) { } catch (e) {
routes = [getSimpleRoute(route)]; cacheRoutes = [simpleRoute];
} }
// 将最新的路由信息同步到 localStorage 中
const routes = router.getRoutes();
cacheRoutes.forEach((cacheRoute) => {
const route = routes.find((route) => route.path === cacheRoute.path);
if (route) {
cacheRoute.meta = route.meta || cacheRoute.meta;
cacheRoute.name = (route.name || cacheRoute.name) as string;
}
});
// 初始化标签页 // 初始化标签页
tabsViewStore.initTabs(routes); tabsViewStore.initTabs(cacheRoutes);
//监听滚动条 //监听滚动条
function onScroll(e) { function onScroll(e) {
@@ -245,11 +262,11 @@
document.documentElement.scrollTop || document.documentElement.scrollTop ||
window.pageYOffset || window.pageYOffset ||
document.body.scrollTop; // 滚动条偏移量 document.body.scrollTop; // 滚动条偏移量
if (!getHeaderSetting.fixed && getMultiTabsSetting.fixed && scrollTop >= 64) { state.isMultiHeaderFixed = !!(
state.isMultiHeaderFixed = true; !getHeaderSetting.value.fixed &&
} else { getMultiTabsSetting.value.fixed &&
state.isMultiHeaderFixed = false; scrollTop >= 64
} );
} }
window.addEventListener('scroll', onScroll, true); window.addEventListener('scroll', onScroll, true);
@@ -281,7 +298,7 @@
if (whiteList.includes(route.name as string)) return; if (whiteList.includes(route.name as string)) return;
state.activeKey = to; state.activeKey = to;
tabsViewStore.addTabs(getSimpleRoute(route)); tabsViewStore.addTabs(getSimpleRoute(route));
updateNavScroll(); updateNavScroll(true);
}, },
{ immediate: true } { immediate: true }
); );
@@ -344,7 +361,6 @@
// 关闭全部 // 关闭全部
const closeAll = () => { const closeAll = () => {
localStorage.removeItem('routes');
tabsViewStore.closeAllTabs(); tabsViewStore.closeAllTabs();
router.replace(PageEnum.BASE_HOME); router.replace(PageEnum.BASE_HOME);
updateNavScroll(); updateNavScroll();
@@ -371,66 +387,76 @@
break; break;
} }
updateNavScroll(); updateNavScroll();
state.showDropdown = false;
}; };
function getCurrentScrollOffset() { /**
const { navStyle } = state; * @param value 要滚动到的位置
const transform: any = toRaw(navStyle.transform); * @param amplitude 每次滚动的长度
return transform ? Number(transform.match(/translateX\(-(\d+(\.\d+)*)px\)/)[1]) : 0; */
} function scrollTo(value: number, amplitude: number) {
const currentScroll = navScroll.value.scrollLeft;
function setOffset(value) { const scrollWidth =
state.navStyle.transform = `translateX(-${value}px)`; (amplitude > 0 && currentScroll + amplitude >= value) ||
(amplitude < 0 && currentScroll + amplitude <= value)
? value
: currentScroll + amplitude;
navScroll.value && navScroll.value.scrollTo(scrollWidth, 0);
if (scrollWidth === value) return;
return window.requestAnimationFrame(() => scrollTo(value, amplitude));
} }
function scrollPrev() { function scrollPrev() {
const containerWidth = navScroll.value.offsetWidth; const containerWidth = navScroll.value.offsetWidth;
const currentOffset = getCurrentScrollOffset(); const currentScroll = navScroll.value.scrollLeft;
if (!currentOffset) return;
let newOffset = currentOffset > containerWidth ? currentOffset - containerWidth : 0; if (!currentScroll) return;
setOffset(newOffset); const scrollLeft = currentScroll > containerWidth ? currentScroll - containerWidth : 0;
scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20);
} }
function scrollNext() { function scrollNext() {
const navWidth = navRef.value.scrollWidth;
const containerWidth = navScroll.value.offsetWidth; const containerWidth = navScroll.value.offsetWidth;
const currentOffset = getCurrentScrollOffset(); const navWidth = navScroll.value.scrollWidth;
if (navWidth - currentOffset <= containerWidth) return; const currentScroll = navScroll.value.scrollLeft;
let newOffset = if (navWidth - currentScroll <= containerWidth) return;
navWidth - currentOffset > containerWidth * 2 const scrollLeft =
? currentOffset + containerWidth navWidth - currentScroll > containerWidth * 2
? currentScroll + containerWidth
: navWidth - containerWidth; : navWidth - containerWidth;
scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20);
setOffset(newOffset);
} }
function updateNavScroll() { /**
if (!navRef.value) return; * @param autoScroll 是否开启自动滚动功能
let navWidth = navRef.value.scrollWidth; */
let containerWidth = navScroll.value.offsetWidth; async function updateNavScroll(autoScroll?: boolean) {
const currentOffset = getCurrentScrollOffset(); await nextTick();
if (!navScroll.value) return;
const containerWidth = navScroll.value.offsetWidth;
const navWidth = navScroll.value.scrollWidth;
if (containerWidth < navWidth) { if (containerWidth < navWidth) {
state.scrollable = true; state.scrollable = true;
if (navWidth - currentOffset < containerWidth) { if (autoScroll) {
setOffset(navWidth - containerWidth); let tagList = navScroll.value.querySelectorAll('.tabs-card-scroll-item') || [];
[...tagList].forEach((tag: HTMLElement) => {
// fix SyntaxError
if (tag.id === `tag${state.activeKey.split('/').join('\/')}`) {
tag.scrollIntoView && tag.scrollIntoView();
}
});
} }
} else { } else {
state.scrollable = false; state.scrollable = false;
if (currentOffset > 0) {
setOffset(0);
}
} }
} }
function handleResize() { function handleResize() {
updateNavScroll(); updateNavScroll(true);
} }
const getNavStyle = computed(() => {
return state.navStyle;
});
function handleContextMenu(e, item) { function handleContextMenu(e, item) {
e.preventDefault(); e.preventDefault();
isCurrent.value = PageEnum.BASE_HOME_REDIRECT === item.path; isCurrent.value = PageEnum.BASE_HOME_REDIRECT === item.path;
@@ -451,7 +477,7 @@
const { fullPath } = e; const { fullPath } = e;
if (fullPath === route.fullPath) return; if (fullPath === route.fullPath) return;
state.activeKey = fullPath; state.activeKey = fullPath;
router.push({ path: fullPath }); go(e, true);
} }
//删除tab //删除tab
@@ -474,11 +500,9 @@
return { return {
...toRefs(state), ...toRefs(state),
navWrap, navWrap,
navRef,
navScroll, navScroll,
route, route,
tabsList, tabsList,
baseHome: PageEnum.BASE_HOME_REDIRECT,
goPage, goPage,
closeTabItem, closeTabItem,
closeLeft, closeLeft,
@@ -491,10 +515,12 @@
closeHandleSelect, closeHandleSelect,
scrollNext, scrollNext,
scrollPrev, scrollPrev,
getNavStyle,
handleContextMenu, handleContextMenu,
onClickOutside, onClickOutside,
getDarkTheme, getDarkTheme,
getAppTheme,
getCardColor,
getBaseColor,
}; };
}, },
}); });
@@ -551,22 +577,12 @@
} }
&-scroll { &-scroll {
overflow: hidden;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
.tabs-card-nav {
padding-left: 0;
margin: 0;
float: left;
list-style: none;
box-sizing: border-box;
position: relative;
transition: transform 0.5s ease-in-out;
}
&-item { &-item {
background: var(--color); background: v-bind(getCardColor);
color: var(--text-color); color: v-bind(getBaseColor);
height: 32px; height: 32px;
padding: 6px 16px 4px; padding: 6px 16px 4px;
border-radius: 3px; border-radius: 3px;
@@ -606,7 +622,7 @@
} }
.active-item { .active-item {
color: #2d8cf0; color: v-bind(getAppTheme);
} }
} }
} }
@@ -642,6 +658,10 @@
background: #f5f7f9; background: #f5f7f9;
} }
.tabs-view-dark-background {
background: #101014;
}
.tabs-view-fix { .tabs-view-fix {
position: fixed; position: fixed;
z-index: 5; z-index: 5;

View File

@@ -1,8 +1,10 @@
<template> <template>
<NLayout class="layout" :position="fixedMenu" has-sider> <n-layout class="layout" :position="fixedMenu" has-sider>
<NLayoutSider <n-layout-sider
v-if="isMixMenuNoneSub && (navMode === 'vertical' || navMode === 'horizontal-mix')" v-if="
show-trigger !isMobile && isMixMenuNoneSub && (navMode === 'vertical' || navMode === 'horizontal-mix')
"
show-trigger="bar"
@collapse="collapsed = true" @collapse="collapsed = true"
:position="fixedMenu" :position="fixedMenu"
@expand="collapsed = false" @expand="collapsed = false"
@@ -16,14 +18,24 @@
> >
<Logo :collapsed="collapsed" /> <Logo :collapsed="collapsed" />
<AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" /> <AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" />
</NLayoutSider> </n-layout-sider>
<NLayout :inverted="inverted"> <n-drawer
<NLayoutHeader :inverted="getHeaderInverted" :position="fixedHeader"> v-model:show="showSideDrawder"
:width="menuWidth"
:placement="'left'"
class="layout-side-drawer"
>
<Logo :collapsed="collapsed" />
<AsideMenu @clickMenuItem="collapsed = false" />
</n-drawer>
<n-layout :inverted="inverted">
<n-layout-header :inverted="getHeaderInverted" :position="fixedHeader">
<PageHeader v-model:collapsed="collapsed" :inverted="inverted" /> <PageHeader v-model:collapsed="collapsed" :inverted="inverted" />
</NLayoutHeader> </n-layout-header>
<NLayoutContent <n-layout-content
class="layout-content" class="layout-content"
:class="{ 'layout-default-background': getDarkTheme === false }" :class="{ 'layout-default-background': getDarkTheme === false }"
> >
@@ -50,13 +62,14 @@
<!-- <NLayoutFooter v-if="getShowFooter">--> <!-- <NLayoutFooter v-if="getShowFooter">-->
<!-- <PageFooter />--> <!-- <PageFooter />-->
<!-- </NLayoutFooter>--> <!-- </NLayoutFooter>-->
</NLayoutContent> </n-layout-content>
</NLayout> <n-back-top :right="100" />
</NLayout> </n-layout>
</n-layout>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, unref, computed, onMounted } from 'vue'; import { ref, unref, computed, onMounted } from 'vue';
import { Logo } from './components/Logo'; import { Logo } from './components/Logo';
import { TabsView } from './components/TagsView'; import { TabsView } from './components/TagsView';
import { MainView } from './components/Main'; import { MainView } from './components/Main';
@@ -68,119 +81,129 @@
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useProjectSettingStore } from '@/store/modules/projectSetting'; import { useProjectSettingStore } from '@/store/modules/projectSetting';
export default defineComponent({ const { getDarkTheme } = useDesignSetting();
name: 'Layout', const {
components: { // getShowFooter,
TabsView, getNavMode,
MainView, getNavTheme,
PageHeader, getHeaderSetting,
AsideMenu, getMenuSetting,
Logo, getMultiTabsSetting,
}, } = useProjectSetting();
setup() {
const { getDarkTheme } = useDesignSetting();
const {
getShowFooter,
getNavMode,
getNavTheme,
getHeaderSetting,
getMenuSetting,
getMultiTabsSetting,
} = useProjectSetting();
const settingStore = useProjectSettingStore(); const settingStore = useProjectSettingStore();
const navMode = getNavMode; const navMode = getNavMode;
const collapsed = ref<boolean>(false); const collapsed = ref<boolean>(false);
const fixedHeader = computed(() => { const { mobileWidth, menuWidth } = unref(getMenuSetting);
const { fixed } = unref(getHeaderSetting);
return fixed ? 'absolute' : 'static';
});
const isMixMenuNoneSub = computed(() => { const isMobile = computed<boolean>({
const mixMenu = settingStore.menuSetting.mixMenu; get: () => settingStore.getIsMobile,
const currentRoute = useRoute(); set: (val) => settingStore.setIsMobile(val),
if (unref(navMode) != 'horizontal-mix') return true; });
if (unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot) {
return false;
}
return true;
});
const fixedMenu = computed(() => { const fixedHeader = computed(() => {
const { fixed } = unref(getHeaderSetting); const { fixed } = unref(getHeaderSetting);
return fixed ? 'absolute' : 'static'; return fixed ? 'absolute' : 'static';
}); });
const isMultiTabs = computed(() => { const isMixMenuNoneSub = computed(() => {
return unref(getMultiTabsSetting).show; const mixMenu = settingStore.menuSetting.mixMenu;
}); const currentRoute = useRoute();
if (unref(navMode) != 'horizontal-mix') return true;
if (unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot) {
return false;
}
return true;
});
const fixedMulti = computed(() => { const fixedMenu = computed(() => {
return unref(getMultiTabsSetting).fixed; const { fixed } = unref(getHeaderSetting);
}); return fixed ? 'absolute' : 'static';
});
const inverted = computed(() => { const isMultiTabs = computed(() => {
return ['dark', 'header-dark'].includes(unref(getNavTheme)); return unref(getMultiTabsSetting).show;
}); });
const getHeaderInverted = computed(() => { const fixedMulti = computed(() => {
const navTheme = unref(getNavTheme); return unref(getMultiTabsSetting).fixed;
return ['light', 'header-dark'].includes(navTheme) ? unref(inverted) : !unref(inverted); });
});
const leftMenuWidth = computed(() => { const inverted = computed(() => {
const { minMenuWidth, menuWidth } = unref(getMenuSetting); return ['dark', 'header-dark'].includes(unref(getNavTheme));
return collapsed.value ? minMenuWidth : menuWidth; });
});
const getChangeStyle = computed(() => { const getHeaderInverted = computed(() => {
const { minMenuWidth, menuWidth } = unref(getMenuSetting); const navTheme = unref(getNavTheme);
return { return ['light', 'header-dark'].includes(navTheme) ? unref(inverted) : !unref(inverted);
'padding-left': collapsed.value ? `${minMenuWidth}px` : `${menuWidth}px`, });
};
});
const getMenuLocation = computed(() => { const leftMenuWidth = computed(() => {
return 'left'; const { minMenuWidth, menuWidth } = unref(getMenuSetting);
}); return collapsed.value ? minMenuWidth : menuWidth;
});
function watchWidth() { // const getChangeStyle = computed(() => {
const Width = document.body.clientWidth; // const { minMenuWidth, menuWidth } = unref(getMenuSetting);
if (Width <= 950) { // return {
collapsed.value = true; // 'padding-left': collapsed.value ? `${minMenuWidth}px` : `${menuWidth}px`,
} else collapsed.value = false; // };
} // });
onMounted(() => { const getMenuLocation = computed(() => {
window.addEventListener('resize', watchWidth); return 'left';
//挂载在 window 方便与在js中使用 });
window['$loading'] = useLoadingBar();
window['$loading'].finish();
});
return { // 控制显示或隐藏移动端侧边栏
fixedMenu, const showSideDrawder = computed({
fixedMulti, get: () => isMobile.value && collapsed.value,
fixedHeader, set: (val) => (collapsed.value = val),
collapsed, });
inverted,
isMultiTabs, //判断是否触发移动端模式
leftMenuWidth, const checkMobileMode = () => {
getChangeStyle, if (document.body.clientWidth <= mobileWidth) {
navMode, isMobile.value = true;
getShowFooter, } else {
getDarkTheme, isMobile.value = false;
getHeaderInverted, }
getMenuLocation, collapsed.value = false;
isMixMenuNoneSub, };
};
}, const watchWidth = () => {
const Width = document.body.clientWidth;
if (Width <= 950) {
collapsed.value = true;
} else collapsed.value = false;
checkMobileMode();
};
onMounted(() => {
checkMobileMode();
window.addEventListener('resize', watchWidth);
//挂载在 window 方便与在js中使用
window['$loading'] = useLoadingBar();
window['$loading'].finish();
}); });
</script> </script>
<style lang="less">
.layout-side-drawer {
background-color: rgb(0, 20, 40);
.layout-sider {
min-height: 100vh;
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
position: relative;
z-index: 13;
transition: all 0.2s ease-in-out;
}
}
</style>
<style lang="less" scoped> <style lang="less" scoped>
.layout { .layout {
display: flex; display: flex;
@@ -241,7 +264,7 @@
} }
.fluid-header { .fluid-header {
padding-top: 0px; padding-top: 0;
} }
.main-view-fix { .main-view-fix {

View File

@@ -3,8 +3,6 @@ import { createApp } from 'vue';
import App from './App.vue'; import App from './App.vue';
import router, { setupRouter } from './router'; import router, { setupRouter } from './router';
import { setupStore } from '@/store'; import { setupStore } from '@/store';
import MakeitCaptcha from 'makeit-captcha';
import 'makeit-captcha/dist/captcha.min.css';
import { setupNaive, setupDirectives } from '@/plugins'; import { setupNaive, setupDirectives } from '@/plugins';
import { AppProvider } from '@/components/Application'; import { AppProvider } from '@/components/Application';
@@ -13,8 +11,6 @@ async function bootstrap() {
const app = createApp(App); const app = createApp(App);
app.use(MakeitCaptcha);
// 注册全局常用的 naive-ui 组件 // 注册全局常用的 naive-ui 组件
setupNaive(app); setupNaive(app);

View File

@@ -65,6 +65,7 @@ import {
NSpin, NSpin,
NTimePicker, NTimePicker,
NBackTop, NBackTop,
NSkeleton,
} from 'naive-ui'; } from 'naive-ui';
const naive = create({ const naive = create({
@@ -133,6 +134,7 @@ const naive = create({
NSpin, NSpin,
NTimePicker, NTimePicker,
NBackTop, NBackTop,
NSkeleton,
], ],
}); });

View File

@@ -13,7 +13,7 @@ export const ErrorPageRoute: AppRouteRecordRaw = {
children: [ children: [
{ {
path: '/:path(.*)*', path: '/:path(.*)*',
name: 'ErrorPage', name: 'ErrorPageSon',
component: ErrorPage, component: ErrorPage,
meta: { meta: {
title: 'ErrorPage', title: 'ErrorPage',

View File

@@ -1,16 +0,0 @@
import { renderIcon } from '@/utils/index';
import { DashboardOutlined } from '@vicons/antd';
// import { RouterTransition } from '@/components/transition'
//前端路由映射表
export const constantRouterComponents = {
Layout: () => import('@/layout/index.vue'), //布局
DashboardConsole: () => import('@/views/dashboard/console/console.vue'), // 主控台
DashboardMonitor: () => import('@/views/dashboard/monitor/monitor.vue'), // 监控页
DashboardWorkplace: () => import('@/views/dashboard/workplace/workplace.vue'), // 工作台
};
//前端路由图标映射表
export const constantRouterIcon = {
DashboardOutlined: renderIcon(DashboardOutlined),
};

View File

@@ -1,12 +1,17 @@
import { adminMenus } from '@/api/system/menu'; import { adminMenus } from '@/api/system/menu';
import { constantRouterComponents, constantRouterIcon } from './constantRouterComponents'; import { constantRouterIcon } from './router-icons';
import router from '@/router/index';
import { constantRouter } from '@/router/index';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Layout, ParentLayout } from '@/router/constant';
import type { AppRouteRecordRaw } from '@/router/types';
const Iframe = () => import('@/views/iframe/index.vue');
const LayoutMap = new Map<string, () => Promise<typeof import('*.vue')>>();
LayoutMap.set('LAYOUT', Layout);
LayoutMap.set('IFRAME', Iframe);
/** /**
* 格式化 后端 结构信息并递归生成层级路由表 * 格式化 后端 结构信息并递归生成层级路由表
*
* @param routerMap * @param routerMap
* @param parent * @param parent
* @returns {*} * @returns {*}
@@ -19,21 +24,24 @@ export const routerGenerator = (routerMap, parent?): any[] => {
// 路由名称,建议唯一 // 路由名称,建议唯一
name: item.name || '', name: item.name || '',
// 该路由对应页面的 组件 // 该路由对应页面的 组件
component: constantRouterComponents[item.component], component: item.component,
// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉) // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
meta: { meta: {
...item.meta, ...item.meta,
label: item.meta.title, label: item.meta.title,
icon: constantRouterIcon[item.meta.icon] || null, icon: constantRouterIcon[item.meta.icon] || null,
permission: item.meta.permission || null, permissions: item.meta.permissions || null,
}, },
}; };
// 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠 // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
currentRouter.path = currentRouter.path.replace('//', '/'); currentRouter.path = currentRouter.path.replace('//', '/');
// 重定向 // 重定向
item.redirect && (currentRouter.redirect = item.redirect); item.redirect && (currentRouter.redirect = item.redirect);
// 是否有子菜单,并递归处理 // 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
//如果未定义 redirect 默认第一个子路由为 redirect
!item.redirect && (currentRouter.redirect = `${item.path}/${item.children[0].path}`);
// Recursion // Recursion
currentRouter.children = routerGenerator(item.children, currentRouter); currentRouter.children = routerGenerator(item.children, currentRouter);
} }
@@ -43,7 +51,6 @@ export const routerGenerator = (routerMap, parent?): any[] => {
/** /**
* 动态生成菜单 * 动态生成菜单
* @param token
* @returns {Promise<Router>} * @returns {Promise<Router>}
*/ */
export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => { export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => {
@@ -51,14 +58,65 @@ export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => {
adminMenus() adminMenus()
.then((result) => { .then((result) => {
const routeList = routerGenerator(result); const routeList = routerGenerator(result);
const asyncRoutesList = [...constantRouter, ...routeList]; asyncImportRoute(routeList);
asyncRoutesList.forEach((item) => {
router.addRoute(item); resolve(routeList);
});
resolve(asyncRoutesList);
}) })
.catch((err) => { .catch((err) => {
reject(err); reject(err);
}); });
}); });
}; };
/**
* 查找views中对应的组件文件
* */
let viewsModules: Record<string, () => Promise<Recordable>>;
export const asyncImportRoute = (routes: AppRouteRecordRaw[] | undefined): void => {
viewsModules = viewsModules || import.meta.glob('../views/**/*.{vue,tsx}');
if (!routes) return;
routes.forEach((item) => {
if (!item.component && item.meta?.frameSrc) {
item.component = 'IFRAME';
}
const { component, name } = item;
const { children } = item;
if (component) {
const layoutFound = LayoutMap.get(component as string);
if (layoutFound) {
item.component = layoutFound;
} else {
item.component = dynamicImport(viewsModules, component as string);
}
} else if (name) {
item.component = ParentLayout;
}
children && asyncImportRoute(children);
});
};
/**
* 动态导入
* */
export const dynamicImport = (
viewsModules: Record<string, () => Promise<Recordable>>,
component: string
) => {
const keys = Object.keys(viewsModules);
const matchKeys = keys.filter((key) => {
let k = key.replace('../views', '');
const lastIndex = k.lastIndexOf('.');
k = k.substring(0, lastIndex);
return k === component;
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];
return viewsModules[matchKey];
}
if (matchKeys?.length > 1) {
console.warn(
'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure'
);
return;
}
};

View File

@@ -1,10 +1,9 @@
import { App } from 'vue'; import { App } from 'vue';
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { ErrorPageRoute, RedirectRoute } from '@/router/base'; import { RedirectRoute } from '@/router/base';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { createRouterGuards } from './router-guards'; import { createRouterGuards } from './router-guards';
// @ts-ignore
const modules = import.meta.globEager('./modules/**/*.ts'); const modules = import.meta.globEager('./modules/**/*.ts');
const routeModuleList: RouteRecordRaw[] = []; const routeModuleList: RouteRecordRaw[] = [];
@@ -40,7 +39,7 @@ export const LoginRoute: RouteRecordRaw = {
}; };
//需要验证权限 //需要验证权限
export const asyncRoutes = [ErrorPageRoute, ...routeModuleList]; export const asyncRoutes = [...routeModuleList];
//普通路由 无需验证权限 //普通路由 无需验证权限
export const constantRouter: any[] = [LoginRoute, RootRoute, RedirectRoute]; export const constantRouter: any[] = [LoginRoute, RootRoute, RedirectRoute];

View File

@@ -9,9 +9,10 @@ const routes: Array<RouteRecordRaw> = [
name: 'about', name: 'about',
component: Layout, component: Layout,
meta: { meta: {
sort: 9, sort: 10,
isRoot: true, isRoot: true,
activeMenu: 'about_index', activeMenu: 'about_index',
icon: renderIcon(ProjectOutlined),
}, },
children: [ children: [
{ {
@@ -19,7 +20,6 @@ const routes: Array<RouteRecordRaw> = [
name: `about_index`, name: `about_index`,
meta: { meta: {
title: '关于', title: '关于',
icon: renderIcon(ProjectOutlined),
extra: renderNew(), extra: renderNew(),
activeMenu: 'about_index', activeMenu: 'about_index',
}, },

View File

@@ -1,7 +1,7 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Layout, ParentLayout } from '@/router/constant'; import { Layout, ParentLayout } from '@/router/constant';
import { WalletOutlined } from '@vicons/antd'; import { WalletOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index'; import { renderIcon, renderNew } from '@/utils';
const routeName = 'comp'; const routeName = 'comp';
@@ -106,6 +106,24 @@ const routes: Array<RouteRecordRaw> = [
}, },
component: () => import('@/views/comp/modal/index.vue'), component: () => import('@/views/comp/modal/index.vue'),
}, },
{
path: 'richtext',
name: `richtext`,
meta: {
title: '富文本',
extra: renderNew(),
},
component: () => import('@/views/comp/richtext/vue-quill.vue'),
},
{
path: 'drag',
name: `Drag`,
meta: {
title: '拖拽',
extra: renderNew(),
},
component: () => import('@/views/comp/drag/index.vue'),
},
], ],
}, },
]; ];

View File

@@ -24,7 +24,7 @@ const routes: Array<RouteRecordRaw> = [
meta: { meta: {
title: 'Dashboard', title: 'Dashboard',
icon: renderIcon(DashboardOutlined), icon: renderIcon(DashboardOutlined),
permission: ['dashboard_console', 'dashboard_console', 'dashboard_workplace'], permissions: ['dashboard_console', 'dashboard_console', 'dashboard_workplace'],
sort: 0, sort: 0,
}, },
children: [ children: [
@@ -33,7 +33,8 @@ const routes: Array<RouteRecordRaw> = [
name: `${routeName}_console`, name: `${routeName}_console`,
meta: { meta: {
title: '主控台', title: '主控台',
permission: ['dashboard_console'], permissions: ['dashboard_console'],
affix: true,
}, },
component: () => import('@/views/dashboard/console/console.vue'), component: () => import('@/views/dashboard/console/console.vue'),
}, },
@@ -42,7 +43,7 @@ const routes: Array<RouteRecordRaw> = [
// name: `${ routeName }_monitor`, // name: `${ routeName }_monitor`,
// meta: { // meta: {
// title: '监控页', // title: '监控页',
// permission: ['dashboard_monitor'] // permissions: ['dashboard_monitor']
// }, // },
// component: () => import('@/views/dashboard/monitor/monitor.vue') // component: () => import('@/views/dashboard/monitor/monitor.vue')
// }, // },
@@ -52,7 +53,7 @@ const routes: Array<RouteRecordRaw> = [
meta: { meta: {
title: '工作台', title: '工作台',
keepAlive: true, keepAlive: true,
permission: ['dashboard_workplace'], permissions: ['dashboard_workplace'],
}, },
component: () => import('@/views/dashboard/workplace/workplace.vue'), component: () => import('@/views/dashboard/workplace/workplace.vue'),
}, },

View File

@@ -6,12 +6,12 @@ import { renderIcon } from '@/utils/index';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/external', path: '/external',
name: 'https://jekip.github.io/docs/', name: 'https://naive-ui-admin-docs.vercel.app',
component: Layout, component: Layout,
meta: { meta: {
title: '项目文档', title: '项目文档',
icon: renderIcon(DocumentTextOutline), icon: renderIcon(DocumentTextOutline),
sort: 8, sort: 9,
}, },
}, },
]; ];

View File

@@ -0,0 +1,51 @@
import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant';
import { DesktopOutline } from '@vicons/ionicons5';
import { renderIcon } from '@/utils/index';
const IFrame = () => import('@/views/iframe/index.vue');
const routes: Array<RouteRecordRaw> = [
{
path: '/frame',
name: 'Frame',
redirect: '/frame/docs',
component: Layout,
meta: {
title: '外部页面',
sort: 8,
icon: renderIcon(DesktopOutline),
},
children: [
{
path: 'naive-admin',
name: 'naive-admin',
meta: {
title: 'NaiveAdmin',
frameSrc: 'https://www.naiveadmin.com',
},
component: IFrame,
},
{
path: 'docs',
name: 'frame-docs',
meta: {
title: '项目文档(内嵌)',
frameSrc: 'https://naive-ui-admin-docs.vercel.app',
},
component: IFrame,
},
{
path: 'naive',
name: 'frame-naive',
meta: {
title: 'NaiveUi(内嵌)',
frameSrc: 'https://www.naiveui.com',
},
component: IFrame,
},
],
},
];
export default routes;

View File

@@ -1,7 +1,7 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant'; import { Layout } from '@/router/constant';
import { TableOutlined } from '@vicons/antd'; import { TableOutlined } from '@vicons/antd';
import { renderIcon, renderNew } from '@/utils/index'; import { renderIcon } from '@/utils/index';
/** /**
* @param name 路由名称, 必须设置,且不能重名 * @param name 路由名称, 必须设置,且不能重名
@@ -31,7 +31,6 @@ const routes: Array<RouteRecordRaw> = [
name: 'basic-list', name: 'basic-list',
meta: { meta: {
title: '基础列表', title: '基础列表',
extra: renderNew(),
}, },
component: () => import('@/views/list/basicList/index.vue'), component: () => import('@/views/list/basicList/index.vue'),
}, },

View File

@@ -1,9 +1,11 @@
import type { RouteRecordRaw } from 'vue-router';
import { isNavigationFailure, Router } from 'vue-router'; import { isNavigationFailure, Router } from 'vue-router';
import { useUserStoreWidthOut } from '@/store/modules/user'; import { useUserStoreWidthOut } from '@/store/modules/user';
import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute'; import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute';
import { ACCESS_TOKEN } from '@/store/mutation-types'; import { ACCESS_TOKEN } from '@/store/mutation-types';
import { storage } from '@/utils/Storage'; import { storage } from '@/utils/Storage';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { ErrorPageRoute } from '@/router/base';
const LOGIN_PATH = PageEnum.BASE_LOGIN; const LOGIN_PATH = PageEnum.BASE_LOGIN;
@@ -29,7 +31,7 @@ export function createRouterGuards(router: Router) {
const token = storage.get(ACCESS_TOKEN); const token = storage.get(ACCESS_TOKEN);
if (!token) { if (!token) {
// You can access without permission. You need to set the routing meta.ignoreAuth to true // You can access without permissions. You need to set the routing meta.ignoreAuth to true
if (to.meta.ignoreAuth) { if (to.meta.ignoreAuth) {
next(); next();
return; return;
@@ -60,9 +62,15 @@ export function createRouterGuards(router: Router) {
// 动态添加可访问路由表 // 动态添加可访问路由表
routes.forEach((item) => { routes.forEach((item) => {
router.addRoute(item); router.addRoute(item as unknown as RouteRecordRaw);
}); });
//添加404
const isErrorPage = router.getRoutes().findIndex((item) => item.name === ErrorPageRoute.name);
if (isErrorPage === -1) {
router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw);
}
const redirectPath = (from.query.redirect || to.path) as string; const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath); const redirect = decodeURIComponent(redirectPath);
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }; const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };

View File

@@ -0,0 +1,7 @@
import { renderIcon } from '@/utils/index';
import { DashboardOutlined } from '@vicons/antd';
//前端路由图标映射表
export const constantRouterIcon = {
DashboardOutlined: renderIcon(DashboardOutlined),
};

View File

@@ -1,13 +1,11 @@
import type { RouteRecordRaw, RouteMeta } from 'vue-router'; import type { RouteRecordRaw, RouteMeta } from 'vue-router';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { RoleEnum } from '@/enums/roleEnum';
export type Component<T extends any = any> = export type Component<T extends any = any> =
| ReturnType<typeof defineComponent> | ReturnType<typeof defineComponent>
| (() => Promise<typeof import('*.vue')>) | (() => Promise<typeof import('*.vue')>)
| (() => Promise<T>); | (() => Promise<T>);
// @ts-ignore
export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string; name: string;
meta: RouteMeta; meta: RouteMeta;
@@ -23,7 +21,7 @@ export interface Meta {
title: string; title: string;
// 是否忽略权限 // 是否忽略权限
ignoreAuth?: boolean; ignoreAuth?: boolean;
roles?: RoleEnum[]; permissions?: string[];
// 是否不缓存 // 是否不缓存
noKeepAlive?: boolean; noKeepAlive?: boolean;
// 是否固定在tab上 // 是否固定在tab上
@@ -34,4 +32,24 @@ export interface Meta {
frameSrc?: string; frameSrc?: string;
// 外链跳转地址 // 外链跳转地址
externalLink?: string; externalLink?: string;
//隐藏
hidden?: boolean;
}
export interface Menu {
title: string;
label: string;
key: string;
meta: RouteMeta;
name: string;
component?: Component | string;
components?: Component;
children?: AppRouteRecordRaw[];
props?: Recordable;
fullPath?: string;
icon?: any;
path: string;
permissions?: string[];
redirect?: string;
sort?: number;
} }

View File

@@ -0,0 +1,8 @@
export const animates = [
{ value: 'zoom-fade', label: '渐变' },
{ value: 'zoom-out', label: '闪现' },
{ value: 'fade-slide', label: '滑动' },
{ value: 'fade', label: '消退' },
{ value: 'fade-bottom', label: '底部消退' },
{ value: 'fade-scale', label: '缩放消退' },
];

View File

@@ -3,6 +3,8 @@ const setting = {
navMode: 'vertical', navMode: 'vertical',
//导航风格 dark 暗色侧边栏 light 白色侧边栏 header-dark 暗色顶栏 //导航风格 dark 暗色侧边栏 light 白色侧边栏 header-dark 暗色顶栏
navTheme: 'dark', navTheme: 'dark',
// 是否处于移动端模式
isMobile: false,
//顶部 //顶部
headerSetting: { headerSetting: {
//背景色 //背景色
@@ -33,6 +35,8 @@ const setting = {
fixed: true, fixed: true,
//分割菜单 //分割菜单
mixMenu: false, mixMenu: false,
//触发移动端侧边栏的宽度
mobileWidth: 800,
}, },
//面包屑 //面包屑
crumbsSetting: { crumbsSetting: {
@@ -41,7 +45,11 @@ const setting = {
//显示图标 //显示图标
showIcon: false, showIcon: false,
}, },
//菜单权限模式 ROLE 前端固定角色 BACK 动态获取 //菜单权限模式 FIXED 前端固定路由 BACK 动态获取
permissionMode: 'ROLE', permissionMode: 'FIXED',
//是否开启路由动画
isPageAnimate: true,
//路由动画类型
pageAnimateType: 'zoom-fade',
}; };
export default setting; export default setting;

View File

@@ -88,12 +88,12 @@ export const useAsyncRouteStore = defineStore({
}, },
async generateRoutes(data) { async generateRoutes(data) {
let accessedRouters; let accessedRouters;
const roleList = data.roles || []; const permissionsList = data.permissions || [];
const routeFilter = (route) => { const routeFilter = (route) => {
const { meta } = route; const { meta } = route;
const { permission } = meta || {}; const { permissions } = meta || {};
if (!permission) return true; if (!permissions) return true;
return roleList.some((role) => permission.includes(role.value)); return permissionsList.some((item) => permissions.includes(item.value));
}; };
const { getPermissionMode } = useProjectSetting(); const { getPermissionMode } = useProjectSetting();
const permissionMode = unref(getPermissionMode); const permissionMode = unref(getPermissionMode);
@@ -107,7 +107,7 @@ export const useAsyncRouteStore = defineStore({
} else { } else {
try { try {
//过滤账户是否拥有某一个权限,并将菜单从加载列表移除 //过滤账户是否拥有某一个权限,并将菜单从加载列表移除
accessedRouters = filter([...asyncRoutes, ...constantRouter], routeFilter); accessedRouters = filter(asyncRoutes, routeFilter);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -6,12 +6,15 @@ import type { IheaderSetting, ImenuSetting, ImultiTabsSetting, IcrumbsSetting }
const { const {
navMode, navMode,
navTheme, navTheme,
isMobile,
headerSetting, headerSetting,
showFooter, showFooter,
menuSetting, menuSetting,
multiTabsSetting, multiTabsSetting,
crumbsSetting, crumbsSetting,
permissionMode, permissionMode,
isPageAnimate,
pageAnimateType,
} = projectSetting; } = projectSetting;
interface ProjectSettingState { interface ProjectSettingState {
@@ -23,6 +26,9 @@ interface ProjectSettingState {
multiTabsSetting: ImultiTabsSetting; //多标签 multiTabsSetting: ImultiTabsSetting; //多标签
crumbsSetting: IcrumbsSetting; //面包屑 crumbsSetting: IcrumbsSetting; //面包屑
permissionMode: string; //权限模式 permissionMode: string; //权限模式
isPageAnimate: boolean; //是否开启路由动画
pageAnimateType: string; //路由动画类型
isMobile: boolean; // 是否处于移动端模式
} }
export const useProjectSettingStore = defineStore({ export const useProjectSettingStore = defineStore({
@@ -30,12 +36,15 @@ export const useProjectSettingStore = defineStore({
state: (): ProjectSettingState => ({ state: (): ProjectSettingState => ({
navMode: navMode, navMode: navMode,
navTheme, navTheme,
isMobile,
headerSetting, headerSetting,
showFooter, showFooter,
menuSetting, menuSetting,
multiTabsSetting, multiTabsSetting,
crumbsSetting, crumbsSetting,
permissionMode, permissionMode,
isPageAnimate,
pageAnimateType,
}), }),
getters: { getters: {
getNavMode(): string { getNavMode(): string {
@@ -44,6 +53,9 @@ export const useProjectSettingStore = defineStore({
getNavTheme(): string { getNavTheme(): string {
return this.navTheme; return this.navTheme;
}, },
getIsMobile(): boolean {
return this.isMobile;
},
getHeaderSetting(): object { getHeaderSetting(): object {
return this.headerSetting; return this.headerSetting;
}, },
@@ -62,11 +74,20 @@ export const useProjectSettingStore = defineStore({
getPermissionMode(): string { getPermissionMode(): string {
return this.permissionMode; return this.permissionMode;
}, },
getIsPageAnimate(): boolean {
return this.isPageAnimate;
},
getPageAnimateType(): string {
return this.pageAnimateType;
},
}, },
actions: { actions: {
setNavTheme(value: string): void { setNavTheme(value: string): void {
this.navTheme = value; this.navTheme = value;
}, },
setIsMobile(value: boolean): void {
this.isMobile = value;
},
}, },
}); });

View File

@@ -7,13 +7,23 @@ const whiteList = ['Redirect', 'login'];
export type RouteItem = Partial<RouteLocationNormalized> & { export type RouteItem = Partial<RouteLocationNormalized> & {
fullPath: string; fullPath: string;
path: string;
name: string; name: string;
hash: string;
meta: object;
params: object;
query: object;
}; };
export type ITabsViewState = { export type ITabsViewState = {
tabsList: RouteItem[]; // 标签页 tabsList: RouteItem[]; // 标签页
}; };
//保留固定路由
function retainAffixRoute(list: any[]) {
return list.filter((item) => item?.meta?.affix ?? false);
}
export const useTabsViewStore = defineStore({ export const useTabsViewStore = defineStore({
id: 'app-tabs-view', id: 'app-tabs-view',
state: (): ITabsViewState => ({ state: (): ITabsViewState => ({
@@ -55,8 +65,8 @@ export const useTabsViewStore = defineStore({
}, },
closeAllTabs() { closeAllTabs() {
// 关闭全部 // 关闭全部
this.tabsList = []; console.log(retainAffixRoute(this.tabsList));
localStorage.removeItem(TABS_ROUTES); this.tabsList = retainAffixRoute(this.tabsList);
}, },
}, },
}); });

View File

@@ -13,7 +13,7 @@ export interface IUserState {
username: string; username: string;
welcome: string; welcome: string;
avatar: string; avatar: string;
roles: any[]; permissions: any[];
info: any; info: any;
} }
@@ -24,7 +24,7 @@ export const useUserStore = defineStore({
username: '', username: '',
welcome: '', welcome: '',
avatar: '', avatar: '',
roles: [], permissions: [],
info: Storage.get(CURRENT_USER, {}), info: Storage.get(CURRENT_USER, {}),
}), }),
getters: { getters: {
@@ -37,8 +37,8 @@ export const useUserStore = defineStore({
getNickname(): string { getNickname(): string {
return this.username; return this.username;
}, },
getRoles(): [any][] { getPermissions(): [any][] {
return this.roles; return this.permissions;
}, },
getUserInfo(): object { getUserInfo(): object {
return this.info; return this.info;
@@ -51,8 +51,8 @@ export const useUserStore = defineStore({
setAvatar(avatar: string) { setAvatar(avatar: string) {
this.avatar = avatar; this.avatar = avatar;
}, },
setRoles(roles) { setPermissions(permissions) {
this.roles = roles; this.permissions = permissions;
}, },
setUserInfo(info) { setUserInfo(info) {
this.info = info; this.info = info;
@@ -83,12 +83,12 @@ export const useUserStore = defineStore({
getUserInfo() getUserInfo()
.then((res) => { .then((res) => {
const result = res; const result = res;
if (result.roles && result.roles.length) { if (result.permissions && result.permissions.length) {
const roles = result.roles; const permissionsList = result.permissions;
that.setRoles(roles); that.setPermissions(permissionsList);
that.setUserInfo(result); that.setUserInfo(result);
} else { } else {
reject(new Error('getInfo: roles must be a non-null array !')); reject(new Error('getInfo: permissionsList must be a non-null array !'));
} }
that.setAvatar(result.avatar); that.setAvatar(result.avatar);
resolve(res); resolve(res);
@@ -101,7 +101,7 @@ export const useUserStore = defineStore({
// 登出 // 登出
async logout() { async logout() {
this.setRoles([]); this.setPermissions([]);
this.setUserInfo(''); this.setUserInfo('');
storage.remove(ACCESS_TOKEN); storage.remove(ACCESS_TOKEN);
storage.remove(CURRENT_USER); storage.remove(CURRENT_USER);

View File

@@ -1,4 +1,4 @@
export const ACCESS_TOKEN = 'Access-Token'; // 用户token export const ACCESS_TOKEN = 'ACCESS-TOKEN'; // 用户token
export const CURRENT_USER = 'Current-User'; // 当前用户信息 export const CURRENT_USER = 'CURRENT-USER'; // 当前用户信息
export const IS_LOCKSCREEN = 'Is-Lockscreen'; // 是否锁屏 export const IS_LOCKSCREEN = 'IS-LOCKSCREEN'; // 是否锁屏
export const TABS_ROUTES = 'Tabs-Routes'; // 标签页 export const TABS_ROUTES = 'TABS-ROUTES'; // 标签页

View File

@@ -1,9 +1,12 @@
#app, body, html { #app,
body,
html {
height: 100%; height: 100%;
} }
body { body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, "\5FAE\8F6F\96C5\9ED1", Arial, sans-serif; font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei,
'\5FAE\8F6F\96C5\9ED1', Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
color: #515a6e; color: #515a6e;
font-size: 14px; font-size: 14px;
@@ -15,7 +18,7 @@ body {
//重置样式 //重置样式
.anticon { .anticon {
svg { svg {
vertical-align: initial vertical-align: initial;
} }
} }
@@ -25,10 +28,11 @@ a {
text-decoration: none; text-decoration: none;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
transition: color .2s ease; transition: color 0.2s ease;
} }
a:active, a:hover { a:active,
a:hover {
outline-width: 0; outline-width: 0;
} }
@@ -40,12 +44,13 @@ a:active {
color: #2b85e4; color: #2b85e4;
} }
a:active, a:hover { a:active,
a:hover {
outline: 0; outline: 0;
text-decoration: none; text-decoration: none;
} }
/*滚动条凹槽的颜色,还可以设置边框属性 */ /* 滚动条凹槽的颜色,还可以设置边框属性 */
*::-webkit-scrollbar-track-piece { *::-webkit-scrollbar-track-piece {
background-color: #f8f8f8; background-color: #f8f8f8;
-webkit-border-radius: 2em; -webkit-border-radius: 2em;
@@ -53,22 +58,22 @@ a:active, a:hover {
border-radius: 2em; border-radius: 2em;
} }
/*滚动条的宽度*/ /* 滚动条的宽度 */
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 9px; width: 9px;
height: 9px; height: 9px;
} }
/*滚动条的设置*/ /* 滚动条的设置 */
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
background-color: #dddddd; background-color: #ddd;
background-clip: padding-box; background-clip: padding-box;
-webkit-border-radius: 2em; -webkit-border-radius: 2em;
-moz-border-radius: 2em; -moz-border-radius: 2em;
border-radius: 2em; border-radius: 2em;
} }
/*滚动条鼠标移上去*/ /* 滚动条鼠标移上去 */
*::-webkit-scrollbar-thumb:hover { *::-webkit-scrollbar-thumb:hover {
background-color: #bbb; background-color: #bbb;
} }
@@ -91,7 +96,7 @@ a:active, a:hover {
//antd 卡片样式定制 //antd 卡片样式定制
body .n-card { body .n-card {
transition: all .2s ease-in-out; transition: all 0.2s ease-in-out;
} }
body .n-icon { body .n-icon {
@@ -110,7 +115,7 @@ body .proCard {
} }
} }
body .n-modal{ body .n-modal {
border-radius: 6px; border-radius: 6px;
} }

3
src/styles/index.less Normal file
View File

@@ -0,0 +1,3 @@
@import 'transition/index.less';
@import './var.less';
@import './common.less';

View File

@@ -0,0 +1,18 @@
.transition-default() {
&-enter-active,
&-leave-active {
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1) !important;
}
&-move {
transition: transform 0.4s;
}
}
.expand-transition {
.transition-default();
}
.expand-x-transition {
.transition-default();
}

View File

@@ -0,0 +1,81 @@
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(30px);
}
// ///////////////////////////////////////////////
// Fade Bottom
// ///////////////////////////////////////////////
// Speed: 1x
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: opacity 0.25s, transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
// fade-scale
.fade-scale-leave-active,
.fade-scale-enter-active {
transition: all 0.28s;
}
.fade-scale-enter-from {
opacity: 0;
transform: scale(1.2);
}
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
// ///////////////////////////////////////////////
// Fade Top
// ///////////////////////////////////////////////
// Speed: 1x
.fade-top-enter-active,
.fade-top-leave-active {
transition: opacity 0.2s, transform 0.25s;
}
.fade-top-enter-from {
opacity: 0;
transform: translateY(8%);
}
.fade-top-leave-to {
opacity: 0;
transform: translateY(-8%);
}

View File

@@ -0,0 +1,10 @@
@import './base.less';
@import './fade.less';
@import './scale.less';
@import './slide.less';
@import './scroll.less';
@import './zoom.less';
.collapse-transition {
transition: 0.2s height ease-in-out, 0.2s padding-top ease-in-out, 0.2s padding-bottom ease-in-out;
}

View File

@@ -0,0 +1,21 @@
.scale-transition {
.transition-default();
&-enter-from,
&-leave,
&-leave-to {
opacity: 0;
transform: scale(0);
}
}
.scale-rotate-transition {
.transition-default();
&-enter-from,
&-leave,
&-leave-to {
opacity: 0;
transform: scale(0) rotate(-45deg);
}
}

View File

@@ -0,0 +1,67 @@
.scroll-y-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-from {
transform: translateY(-15px);
}
&-leave-to {
transform: translateY(15px);
}
}
.scroll-y-reverse-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-from {
transform: translateY(15px);
}
&-leave-to {
transform: translateY(-15px);
}
}
.scroll-x-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-from {
transform: translateX(-15px);
}
&-leave-to {
transform: translateX(15px);
}
}
.scroll-x-reverse-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-from {
transform: translateX(15px);
}
&-leave-to {
transform: translateX(-15px);
}
}

View File

@@ -0,0 +1,39 @@
.slide-y-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(-15px);
}
}
.slide-y-reverse-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(15px);
}
}
.slide-x-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateX(-15px);
}
}
.slide-x-reverse-transition {
.transition-default();
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateX(15px);
}
}

View File

@@ -0,0 +1,27 @@
// zoom-out
.zoom-out-enter-active,
.zoom-out-leave-active {
transition: opacity 0.1 ease-in-out, transform 0.15s ease-out;
}
.zoom-out-enter-from,
.zoom-out-leave-to {
opacity: 0;
transform: scale(0);
}
// zoom-fade
.zoom-fade-enter-active,
.zoom-fade-leave-active {
transition: transform 0.2s, opacity 0.3s ease-out;
}
.zoom-fade-enter-from {
opacity: 0;
transform: scale(0.92);
}
.zoom-fade-leave-to {
opacity: 0;
transform: scale(1.06);
}

View File

@@ -1,6 +1,5 @@
//获取相关CSS属性 //获取相关CSS属性
const getCss = function (o, key) { const getCss = function (o, key) {
// @ts-ignore
return o.currentStyle return o.currentStyle
? o.currentStyle[key] ? o.currentStyle[key]
: document.defaultView?.getComputedStyle(o, null)[key]; : document.defaultView?.getComputedStyle(o, null)[key];
@@ -14,7 +13,7 @@ const params = {
flag: false, flag: false,
}; };
const startDrag = function (bar, target, callback) { const startDrag = function (bar, target, callback?) {
const screenWidth = document.body.clientWidth; // body当前宽度 const screenWidth = document.body.clientWidth; // body当前宽度
const screenHeight = document.documentElement.clientHeight; // 可见区域高度 const screenHeight = document.documentElement.clientHeight; // 可见区域高度

View File

@@ -48,7 +48,7 @@ export const createStorage = ({ prefixKey = '', storage = localStorage } = {}) =
if (expire === null || expire >= Date.now()) { if (expire === null || expire >= Date.now()) {
return value; return value;
} }
this.remove(this.getKey(key)); this.remove(key);
} catch (e) { } catch (e) {
return def; return def;
} }

View File

@@ -5,8 +5,8 @@ import { AxiosCanceler } from './axiosCancel';
import { isFunction } from '@/utils/is'; import { isFunction } from '@/utils/is';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import type { RequestOptions, CreateAxiosOptions, Result } from './types'; import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
// import { ContentTypeEnum } from '/@/enums/httpEnum'; import { ContentTypeEnum } from '@/enums/httpEnum';
export * from './axiosTransform'; export * from './axiosTransform';
@@ -23,18 +23,6 @@ export class VAxios {
this.setupInterceptors(); this.setupInterceptors();
} }
/**
* @description: 创建axios实例
*/
private createAxios(config: CreateAxiosOptions): void {
this.axiosInstance = axios.create(config);
}
private getTransform() {
const { transform } = this.options;
return transform;
}
getAxios(): AxiosInstance { getAxios(): AxiosInstance {
return this.axiosInstance; return this.axiosInstance;
} }
@@ -59,6 +47,103 @@ export class VAxios {
Object.assign(this.axiosInstance.defaults.headers, headers); Object.assign(this.axiosInstance.defaults.headers, headers);
} }
/**
* @description: 请求方法
*/
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: AxiosRequestConfig = cloneDeep(config);
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { beforeRequestHook, requestCatch, transformRequestData } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
//这里重新 赋值成最新的配置
// @ts-ignore
conf.requestOptions = opt;
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
// 请求是否被取消
const isCancel = axios.isCancel(res);
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
try {
const ret = transformRequestData(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('request error!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error) => {
if (requestCatch && isFunction(requestCatch)) {
reject(requestCatch(e));
return;
}
reject(e);
});
});
}
/**
* @description: 创建axios实例
*/
private createAxios(config: CreateAxiosOptions): void {
this.axiosInstance = axios.create(config);
}
private getTransform() {
const { transform } = this.options;
return transform;
}
/**
* @description: 文件上传
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
const customFilename = params.name || 'file';
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
} else {
formData.append(customFilename, params.file);
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key];
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, params.data![key]);
});
}
return this.axiosInstance.request<T>({
method: 'POST',
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
ignoreCancelToken: true,
},
...config,
});
}
/** /**
* @description: 拦截器配置 * @description: 拦截器配置
*/ */
@@ -78,10 +163,17 @@ export class VAxios {
// 请求拦截器配置处理 // 请求拦截器配置处理
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => { this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
const { headers: { ignoreCancelToken } = { ignoreCancelToken: false } } = config; const {
!ignoreCancelToken && axiosCanceler.addPending(config); headers: { ignoreCancelToken },
} = config;
const ignoreCancel =
ignoreCancelToken !== undefined
? ignoreCancelToken
: this.options.requestOptions?.ignoreCancelToken;
!ignoreCancel && axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) { if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config); config = requestInterceptors(config, this.options);
} }
return config; return config;
}, undefined); }, undefined);
@@ -105,62 +197,4 @@ export class VAxios {
isFunction(responseInterceptorsCatch) && isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch); this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
} }
// /**
// * @description: 文件上传
// */
// uploadFiles(config: AxiosRequestConfig, params: File[]) {
// const formData = new FormData();
// Object.keys(params).forEach((key) => {
// formData.append(key, params[key as any]);
// });
// return this.request({
// ...config,
// method: 'POST',
// data: formData,
// headers: {
// 'Content-type': ContentTypeEnum.FORM_DATA,
// },
// });
// }
/**
* @description: 请求方法
*/
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: AxiosRequestConfig = cloneDeep(config);
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { beforeRequestHook, requestCatch, transformRequestData } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
// 请求是否被取消
const isCancel = axios.isCancel(res);
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
const ret = transformRequestData(res, opt);
// ret !== undefined ? resolve(ret) : reject(new Error('request error!'));
return resolve(ret);
}
reject(res as unknown as Promise<T>);
})
.catch((e: Error) => {
if (requestCatch && isFunction(requestCatch)) {
reject(requestCatch(e));
return;
}
reject(e);
});
});
}
} }

View File

@@ -4,6 +4,12 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios'; import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestOptions, Result } from './types'; import type { RequestOptions, Result } from './types';
export interface CreateAxiosOptions extends AxiosRequestConfig {
authenticationScheme?: string;
transform?: AxiosTransform;
requestOptions?: RequestOptions;
}
export abstract class AxiosTransform { export abstract class AxiosTransform {
/** /**
* @description: 请求之前处理配置 * @description: 请求之前处理配置
@@ -24,7 +30,10 @@ export abstract class AxiosTransform {
/** /**
* @description: 请求之前的拦截器 * @description: 请求之前的拦截器
*/ */
requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig; requestInterceptors?: (
config: AxiosRequestConfig,
options: CreateAxiosOptions
) => AxiosRequestConfig;
/** /**
* @description: 请求之后的拦截器 * @description: 请求之后的拦截器

View File

@@ -1,46 +1,47 @@
export function checkStatus(status: number, msg: string, message: any): void { export function checkStatus(status: number, msg: string): void {
const $message = window['$message'];
switch (status) { switch (status) {
case 400: case 400:
message.error(`${msg}`); $message.error(msg);
break; break;
// 401: 未登录 // 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径 // 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。 // 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401: case 401:
message.error('用户没有权限(令牌、用户名、密码错误)!'); $message.error('用户没有权限(令牌、用户名、密码错误)!');
break; break;
case 403: case 403:
message.error('用户得到授权,但是访问是被禁止的。!'); $message.error('用户得到授权,但是访问是被禁止的。!');
break; break;
// 404请求不存在 // 404请求不存在
case 404: case 404:
message.error('网络请求错误,未找到该资源!'); $message.error('网络请求错误未找到该资源!');
break; break;
case 405: case 405:
message.error('网络请求错误,请求方法未允许!'); $message.error('网络请求错误请求方法未允许!');
break; break;
case 408: case 408:
message.error('网络请求超时!'); $message.error('网络请求超时');
break; break;
case 500: case 500:
message.error('服务器错误,请联系管理员!'); $message.error('服务器错误,请联系管理员!');
break; break;
case 501: case 501:
message.error('网络未实现!'); $message.error('网络未实现');
break; break;
case 502: case 502:
message.error('网络错误!'); $message.error('网络错误');
break; break;
case 503: case 503:
message.error('服务不可用,服务器暂时过载或维护!'); $message.error('服务不可用,服务器暂时过载或维护!');
break; break;
case 504: case 504:
message.error('网络超时!'); $message.error('网络超时');
break; break;
case 505: case 505:
message.error('http版本不支持该请求!'); $message.error('http版本不支持该请求!');
break; break;
default: default:
message.error(msg); $message.error(msg);
} }
} }

View File

@@ -36,7 +36,7 @@ export function formatRequestDate(params: Recordable) {
try { try {
params[key] = isString(value) ? value.trim() : value; params[key] = isString(value) ? value.trim() : value;
} catch (error) { } catch (error) {
throw new Error(error); throw new Error(error as any);
} }
} }
} }

View File

@@ -5,13 +5,15 @@ import axios, { AxiosResponse } from 'axios';
import { checkStatus } from './checkStatus'; import { checkStatus } from './checkStatus';
import { joinTimestamp, formatRequestDate } from './helper'; import { joinTimestamp, formatRequestDate } from './helper';
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum'; import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum';
import { PageEnum } from '@/enums/pageEnum';
import { useGlobSetting } from '@/hooks/setting'; import { useGlobSetting } from '@/hooks/setting';
import { isString } from '@/utils/is/'; import { isString } from '@/utils/is/';
import { deepMerge, isUrl } from '@/utils';
import { setObjToUrlParams } from '@/utils/urlUtils'; import { setObjToUrlParams } from '@/utils/urlUtils';
import { RequestOptions, Result } from './types'; import { RequestOptions, Result, CreateAxiosOptions } from './types';
import { useUserStoreWidthOut } from '@/store/modules/user'; import { useUserStoreWidthOut } from '@/store/modules/user';
@@ -29,7 +31,6 @@ const transform: AxiosTransform = {
* @description: 处理请求数据 * @description: 处理请求数据
*/ */
transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => { transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { $message: Message, $dialog: Modal } = window;
const { const {
isShowMessage = true, isShowMessage = true,
isShowErrorMessage, isShowErrorMessage,
@@ -50,15 +51,16 @@ const transform: AxiosTransform = {
return res.data; return res.data;
} }
const reject = Promise.reject;
const { data } = res; const { data } = res;
const $dialog = window['$dialog'];
const $message = window['$message'];
if (!data) { if (!data) {
// return '[HTTP] Request has no return value'; // return '[HTTP] Request has no return value';
return reject(data); throw new Error('请求出错,请稍候重试');
} }
// 这里 coderesultmessage为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式 // 这里 coderesultmessage为 后台统一的字段,需要修改为项目自己的接口返回格式
const { code, result, message } = data; const { code, result, message } = data;
// 请求成功 // 请求成功
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS; const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
@@ -66,13 +68,16 @@ const transform: AxiosTransform = {
if (isShowMessage) { if (isShowMessage) {
if (hasSuccess && (successMessageText || isShowSuccessMessage)) { if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
// 是否显示自定义信息提示 // 是否显示自定义信息提示
Message.success(successMessageText || message || '操作成功!'); $dialog.success({
type: 'success',
content: successMessageText || message || '操作成功!',
});
} else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) { } else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
// 是否显示自定义信息提示 // 是否显示自定义信息提示
Message.error(message || errorMessageText || '操作失败!'); $message.error(message || errorMessageText || '操作失败!');
} else if (!hasSuccess && options.errorMessageMode === 'modal') { } else if (!hasSuccess && options.errorMessageMode === 'modal') {
// errorMessageMode=custom-modal的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误 // errorMessageMode=custom-modal的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
Modal.info({ $dialog.info({
title: '提示', title: '提示',
content: message, content: message,
positiveText: '确定', positiveText: '确定',
@@ -85,63 +90,53 @@ const transform: AxiosTransform = {
if (code === ResultEnum.SUCCESS) { if (code === ResultEnum.SUCCESS) {
return result; return result;
} }
// 接口请求错误,统一提示错误信息 // 接口请求错误,统一提示错误信息 这里逻辑可以根据项目进行修改
if (code === ResultEnum.ERROR) { let errorMsg = message;
if (message) { switch (code) {
Message.error(data.message); // 请求失败
Promise.reject(new Error(message)); case ResultEnum.ERROR:
} else { $message.error(errorMsg);
const msg = '操作失败,系统异常!'; break;
Message.error(msg); // 登录超时
Promise.reject(new Error(msg)); case ResultEnum.TIMEOUT:
} const LoginName = PageEnum.BASE_LOGIN_NAME;
return reject(); const LoginPath = PageEnum.BASE_LOGIN;
if (router.currentRoute.value?.name === LoginName) return;
// 到登录页
errorMsg = '登录超时,请重新登录!';
$dialog.warning({
title: '提示',
content: '登录身份已失效,请重新登录!',
positiveText: '确定',
//negativeText: '取消',
closable: false,
maskClosable: false,
onPositiveClick: () => {
storage.clear();
window.location.href = LoginPath;
},
onNegativeClick: () => {},
});
break;
} }
throw new Error(errorMsg);
// 登录超时
if (code === ResultEnum.TIMEOUT) {
if (router.currentRoute.value.name == 'login') return;
// 到登录页
const timeoutMsg = '登录超时,请重新登录!';
Modal.warning({
title: '提示',
content: '登录身份已失效,请重新登录!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
storage.clear();
router.replace({
name: 'login',
query: {
redirect: router.currentRoute.value.fullPath,
},
});
},
onNegativeClick: () => {},
});
return reject(new Error(timeoutMsg));
}
// 这里逻辑可以根据项目进行修改
if (!hasSuccess) {
return reject(new Error(message));
}
return data;
}, },
// 请求之前处理config // 请求之前处理config
beforeRequestHook: (config, options) => { beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options; const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
if (joinPrefix) { const isUrlStr = isUrl(config.url as string);
if (!isUrlStr && joinPrefix) {
config.url = `${urlPrefix}${config.url}`; config.url = `${urlPrefix}${config.url}`;
} }
if (apiUrl && isString(apiUrl)) { if (!isUrlStr && apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`; config.url = `${apiUrl}${config.url}`;
} }
const params = config.params || {}; const params = config.params || {};
const data = config.data || false;
if (config.method?.toUpperCase() === RequestEnum.GET) { if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isString(params)) { if (!isString(params)) {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。 // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
@@ -154,10 +149,18 @@ const transform: AxiosTransform = {
} else { } else {
if (!isString(params)) { if (!isString(params)) {
formatDate && formatRequestDate(params); formatDate && formatRequestDate(params);
config.data = params; if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
config.params = undefined; config.data = data;
config.params = params;
} else {
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) { if (joinParamsToUrl) {
config.url = setObjToUrlParams(config.url as string, config.data); config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data)
);
} }
} else { } else {
// 兼容restful风格 // 兼容restful风格
@@ -171,13 +174,15 @@ const transform: AxiosTransform = {
/** /**
* @description: 请求拦截器处理 * @description: 请求拦截器处理
*/ */
requestInterceptors: (config) => { requestInterceptors: (config, options) => {
// 请求之前处理config // 请求之前处理config
const userStore = useUserStoreWidthOut(); const userStore = useUserStoreWidthOut();
const token = userStore.getToken; const token = userStore.getToken;
if (token) { if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
// jwt token // jwt token
config.headers.token = token; (config as Recordable).headers.Authorization = options.authenticationScheme
? `${options.authenticationScheme} ${token}`
: token;
} }
return config; return config;
}, },
@@ -186,7 +191,8 @@ const transform: AxiosTransform = {
* @description: 响应错误处理 * @description: 响应错误处理
*/ */
responseInterceptorsCatch: (error: any) => { responseInterceptorsCatch: (error: any) => {
const { $message: Message, $dialog: Modal } = window; const $dialog = window['$dialog'];
const $message = window['$message'];
const { response, code, message } = error || {}; const { response, code, message } = error || {};
// TODO 此处要根据后端接口返回格式修改 // TODO 此处要根据后端接口返回格式修改
const msg: string = const msg: string =
@@ -194,57 +200,88 @@ const transform: AxiosTransform = {
const err: string = error.toString(); const err: string = error.toString();
try { try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) { if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
Message.error('接口请求超时,请刷新页面重试!'); $message.error('接口请求超时请刷新页面重试!');
return; return;
} }
if (err && err.includes('Network Error')) { if (err && err.includes('Network Error')) {
Modal.info({ $dialog.info({
title: '网络异常', title: '网络异常',
content: '请检查您的网络连接是否正常!', content: '请检查您的网络连接是否正常',
positiveText: '确定', positiveText: '确定',
//negativeText: '取消',
closable: false,
maskClosable: false,
onPositiveClick: () => {}, onPositiveClick: () => {},
onNegativeClick: () => {},
}); });
return; return Promise.reject(error);
} }
} catch (error) { } catch (error) {
throw new Error(error); throw new Error(error as any);
} }
// 请求是否被取消 // 请求是否被取消
const isCancel = axios.isCancel(error); const isCancel = axios.isCancel(error);
if (!isCancel) { if (!isCancel) {
checkStatus(error.response && error.response.status, msg, Message); checkStatus(error.response && error.response.status, msg);
} else { } else {
console.warn(error, '请求被取消!'); console.warn(error, '请求被取消!');
} }
return error; //return Promise.reject(error);
return Promise.reject(response?.data);
}, },
}; };
const Axios = new VAxios({ function createAxios(opt?: Partial<CreateAxiosOptions>) {
timeout: 10 * 1000, return new VAxios(
// 接口前缀 deepMerge(
prefixUrl: urlPrefix, {
headers: { 'Content-Type': ContentTypeEnum.JSON }, timeout: 10 * 1000,
// 数据处理方式 authenticationScheme: '',
transform, // 接口前缀
// 配置项,下面的选项都可以在独立的接口请求中覆盖 prefixUrl: urlPrefix,
requestOptions: { headers: { 'Content-Type': ContentTypeEnum.JSON },
// 默认将prefix 添加到url // 数据处理方式
joinPrefix: true, transform,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性 // 配置项,下面的选项都可以在独立的接口请求中覆盖
isReturnNativeResponse: false, requestOptions: {
// 需要对返回数据进行处理 // 默认将prefix 添加到url
isTransformResponse: true, joinPrefix: true,
// post请求的时候添加参数到url // 是否返回原生响应头 比如:需要获取响应头时使用该属性
joinParamsToUrl: false, isReturnNativeResponse: false,
// 格式化提交参数时间 // 需要对返回数据进行处理
formatDate: true, isTransformResponse: true,
// 消息提示类型 // post请求的时候添加参数到url
errorMessageMode: 'none', joinParamsToUrl: false,
// 接口地址 // 格式化提交参数时间
apiUrl: globSetting.apiUrl as string, formatDate: true,
}, // 消息提示类型
withCredentials: false, errorMessageMode: 'none',
}); // 接口地址
apiUrl: globSetting.apiUrl,
// 接口拼接地址
urlPrefix: urlPrefix,
// 是否加入时间戳
joinTime: true,
// 忽略重复请求
ignoreCancelToken: true,
// 是否携带token
withToken: true,
},
withCredentials: false,
},
opt || {}
)
);
}
export default Axios; export const http = createAxios();
// 项目,多个不同 api 地址,直接在这里导出多个
// src/api ts 里面接口,就可以单独使用这个请求,
// import { httpTwo } from '@/utils/http/axios'
// export const httpTwo = createAxios({
// requestOptions: {
// apiUrl: 'http://localhost:9001',
// urlPrefix: 'api',
// },
// });

View File

@@ -2,9 +2,22 @@ import { AxiosRequestConfig } from 'axios';
import { AxiosTransform } from './axiosTransform'; import { AxiosTransform } from './axiosTransform';
export interface CreateAxiosOptions extends AxiosRequestConfig { export interface CreateAxiosOptions extends AxiosRequestConfig {
prefixUrl?: string;
transform?: AxiosTransform; transform?: AxiosTransform;
requestOptions?: RequestOptions; requestOptions?: RequestOptions;
authenticationScheme?: string;
}
// 上传文件
export interface UploadFileParams {
// 其他参数
data?: Recordable;
// 文件参数接口字段名
name?: string;
// 文件
file: File | Blob;
// 文件名称
filename?: string;
[key: string]: any;
} }
export interface RequestOptions { export interface RequestOptions {
@@ -12,8 +25,6 @@ export interface RequestOptions {
joinParamsToUrl?: boolean; joinParamsToUrl?: boolean;
// 格式化请求参数时间 // 格式化请求参数时间
formatDate?: boolean; formatDate?: boolean;
// 是否处理请求结果
isTransformResponse?: boolean;
// 是否显示提示信息 // 是否显示提示信息
isShowMessage?: boolean; isShowMessage?: boolean;
// 是否解析成JSON // 是否解析成JSON
@@ -30,6 +41,8 @@ export interface RequestOptions {
joinPrefix?: boolean; joinPrefix?: boolean;
// 接口地址, 不填则使用默认apiUrl // 接口地址, 不填则使用默认apiUrl
apiUrl?: string; apiUrl?: string;
// 请求拼接路径
urlPrefix?: string;
// 错误消息提示类型 // 错误消息提示类型
errorMessageMode?: 'none' | 'modal'; errorMessageMode?: 'none' | 'modal';
// 是否添加时间戳 // 是否添加时间戳
@@ -38,6 +51,10 @@ export interface RequestOptions {
isTransformResponse?: boolean; isTransformResponse?: boolean;
// 是否返回原生响应头 // 是否返回原生响应头
isReturnNativeResponse?: boolean; isReturnNativeResponse?: boolean;
//忽略重复请求
ignoreCancelToken?: boolean;
// 是否携带token
withToken?: boolean;
} }
export interface Result<T = any> { export interface Result<T = any> {

View File

@@ -41,6 +41,7 @@ export function generatorMenu(routerMap: Array<any>) {
...info.meta, ...info.meta,
label: info.meta?.title, label: info.meta?.title,
key: info.name, key: info.name,
icon: isRoot ? item.meta?.icon : info.meta?.icon,
}; };
// 是否有子菜单,并递归处理 // 是否有子菜单,并递归处理
if (info.children && info.children.length > 0) { if (info.children && info.children.length > 0) {
@@ -81,7 +82,7 @@ export function generatorMenuMix(routerMap: Array<any>, routerName: string, loca
* 递归组装子菜单 * 递归组装子菜单
* */ * */
export function getChildrenRouter(routerMap: Array<any>) { export function getChildrenRouter(routerMap: Array<any>) {
return routerMap.map((item) => { return filterRouter(routerMap).map((item) => {
const isRoot = isRootRouter(item); const isRoot = isRootRouter(item);
const info = isRoot ? item.children[0] : item; const info = isRoot ? item.children[0] : item;
const currentMenu = { const currentMenu = {
@@ -205,3 +206,10 @@ export function lighten(color: string, amount: number) {
amount amount
)}${addLight(color.substring(4, 6), amount)}`; )}${addLight(color.substring(4, 6), amount)}`;
} }
/**
* 判断是否 url
* */
export function isUrl(url: string) {
return /(^http|https:\/\/)/g.test(url);
}

View File

@@ -9,7 +9,7 @@
<n-card <n-card
:bordered="false" :bordered="false"
title="项目信息" title="项目信息"
class="proCard mt-4" class="mt-4 proCard"
size="small" size="small"
:segmented="{ content: 'hard' }" :segmented="{ content: 'hard' }"
> >
@@ -22,12 +22,16 @@
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="文档地址"> <n-descriptions-item label="文档地址">
<div class="flex items-center"> <div class="flex items-center">
<a href="https://jekip.github.io/docs/" class="py-2" target="_blank">查看文档地址</a> <a href="https://naive-ui-admin-docs.vercel.app" class="py-2" target="_blank"
>查看文档地址</a
>
</div> </div>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="预览地址"> <n-descriptions-item label="预览地址">
<div class="flex items-center"> <div class="flex items-center">
<a href="https://jekip.github.io/" class="py-2" target="_blank">查看预览地址</a> <a href="https://naive-ui-admin.vercel.app" class="py-2" target="_blank"
>查看预览地址</a
>
</div> </div>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="Github"> <n-descriptions-item label="Github">
@@ -50,7 +54,7 @@
<n-card <n-card
:bordered="false" :bordered="false"
title="开发环境依赖" title="开发环境依赖"
class="proCard mt-4" class="mt-4 proCard"
size="small" size="small"
:segmented="{ content: 'hard' }" :segmented="{ content: 'hard' }"
> >
@@ -64,7 +68,7 @@
<n-card <n-card
:bordered="false" :bordered="false"
title="生产环境依赖" title="生产环境依赖"
class="proCard mt-4" class="mt-4 proCard"
size="small" size="small"
:segmented="{ content: 'hard' }" :segmented="{ content: 'hard' }"
> >
@@ -77,40 +81,24 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue';
export interface schemaItem { export interface schemaItem {
field: string; field: string;
label: string; label: string;
} }
export default defineComponent({ const { pkg, lastBuildTime } = __APP_INFO__;
setup() { const { dependencies, devDependencies, name, version } = pkg;
const { pkg, lastBuildTime } = __APP_INFO__;
const { dependencies, devDependencies, name, version } = pkg;
const schema: schemaItem[] = []; const schema: schemaItem[] = [];
const devSchema: schemaItem[] = []; const devSchema: schemaItem[] = [];
Object.keys(dependencies).forEach((key) => { Object.keys(dependencies).forEach((key) => {
schema.push({ field: key, label: dependencies[key] }); schema.push({ field: key, label: dependencies[key] });
}); });
Object.keys(devDependencies).forEach((key) => { Object.keys(devDependencies).forEach((key) => {
devSchema.push({ field: key, label: devDependencies[key] }); devSchema.push({ field: key, label: devDependencies[key] });
});
return {
lastBuildTime,
dependencies,
devDependencies,
name,
version,
schema,
devSchema,
};
},
}); });
</script> </script>

View File

@@ -0,0 +1,162 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="拖拽"> 常用于卡片事项预约流程计划等 </n-card>
</div>
<n-alert title="花式拖拽演示" type="info" class="mt-4">
每个卡片都可以上下拖拽顺序另外不同卡片也可以拖拽过去拖拽过来都不在话下呢快试试O(_)O哈哈~
</n-alert>
<n-grid
cols="1 s:2 m:3 l:4 xl:4 2xl:4"
class="mt-4 proCard"
responsive="screen"
:x-gap="12"
:y-gap="8"
>
<n-grid-item>
<NCard
title="需求池"
:segmented="{ content: 'hard', footer: 'hard' }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="demandList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="info">需求</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="开发中"
:segmented="{ content: 'hard', footer: 'hard' }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="exploitList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="warning">开发中</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="已完成"
:segmented="{ content: 'hard', footer: 'hard' }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="completeList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="error">已完成</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="已验收"
:segmented="{ content: 'hard', footer: 'hard' }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="approvedList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="success">已验收</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import Draggable from 'vuedraggable';
const demandList = reactive([
{ name: '预约表单页面,能填写预约相关信息', id: 1 },
{ name: '促销活动页面,包含促销广告展示', id: 2 },
{ name: '商品列表,需要一个到货提醒功能', id: 3 },
{ name: '商品需要一个评价功能', id: 4 },
{ name: '商品图片需要提供放大镜', id: 5 },
{ name: '订单需要提供删除到回收站', id: 6 },
{ name: '用户头像上传,需要支持裁剪', id: 7 },
{ name: '据说Vue3.2发布了setup啥时候支持', id: 8 },
]);
const exploitList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
const completeList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
const approvedList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
</script>
<style lang="less" scoped>
.draggable-ul {
width: 100%;
overflow: hidden;
margin-top: -16px;
.draggable-li {
width: 100%;
padding: 16px 10px;
color: #333;
border-bottom: 1px solid #efeff5;
}
}
</style>

View File

@@ -3,13 +3,15 @@
<div class="n-layout-page-header"> <div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> 基础表单用于向用户收集表单信息 </n-card> <n-card :bordered="false" title="基础表单"> 基础表单用于向用户收集表单信息 </n-card>
</div> </div>
<n-card :bordered="false" class="proCard mt-4"> <n-card :bordered="false" class="mt-4 proCard">
<div class="BasicForm"> <div class="BasicForm">
<BasicForm <BasicForm
submitButtonText="提交预约" submitButtonText="提交预约"
layout="horizontal" layout="horizontal"
:gridProps="{ cols: 1 }" :gridProps="{ cols: 1 }"
:schemas="schemas" :schemas="schemas"
@submit="handleSubmit"
@reset="handleReset"
> >
<template #statusSlot="{ model, field }"> <template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" /> <n-input v-model:value="model[field]" />
@@ -20,12 +22,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref } from 'vue'; import { BasicForm } from '@/components/Form/index';
import { BasicForm, FormSchema } from '@/components/Form/index';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
const schemas: FormSchema[] = [ const schemas = [
{ {
field: 'name', field: 'name',
component: 'NInput', component: 'NInput',
@@ -76,10 +77,10 @@
field: 'makeDate', field: 'makeDate',
component: 'NDatePicker', component: 'NDatePicker',
label: '预约时间', label: '预约时间',
defaultValue: 1183135260000,
componentProps: { componentProps: {
type: 'date', type: 'date',
clearable: true, clearable: true,
defaultValue: 1183135260000,
onUpdateValue: (e: any) => { onUpdateValue: (e: any) => {
console.log(e); console.log(e);
}, },
@@ -149,29 +150,16 @@
}, },
]; ];
export default defineComponent({ const message = useMessage();
components: { BasicForm },
setup() {
const formRef: any = ref(null);
const message = useMessage();
function handleSubmit(values: Recordable) { function handleSubmit(values: Recordable) {
console.log(values); console.log(values);
message.success(JSON.stringify(values)); message.success(JSON.stringify(values));
} }
function handleReset(values: Recordable) { function handleReset(values: Recordable) {
console.log(values); console.log(values);
} }
return {
schemas,
formRef,
handleSubmit,
handleReset,
};
},
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -3,7 +3,7 @@
<div class="n-layout-page-header"> <div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> useForm 表单用于向用户收集表单信息 </n-card> <n-card :bordered="false" title="基础表单"> useForm 表单用于向用户收集表单信息 </n-card>
</div> </div>
<n-card :bordered="false" class="proCard mt-4"> <n-card :bordered="false" class="mt-4 proCard">
<div class="BasicForm"> <div class="BasicForm">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset"> <BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }"> <template #statusSlot="{ model, field }">
@@ -15,12 +15,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref } from 'vue'; import { BasicForm, useForm } from '@/components/Form/index';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
const schemas: FormSchema[] = [ const schemas = [
{ {
field: 'name', field: 'name',
component: 'NInput', component: 'NInput',
@@ -80,10 +79,10 @@
giProps: { giProps: {
//span: 24, //span: 24,
}, },
defaultValue: 1183135260000,
componentProps: { componentProps: {
type: 'date', type: 'date',
clearable: true, clearable: true,
defaultValue: 1183135260000,
onUpdateValue: (e: any) => { onUpdateValue: (e: any) => {
console.log(e); console.log(e);
}, },
@@ -165,43 +164,25 @@
}, },
]; ];
export default defineComponent({ const message = useMessage();
components: { BasicForm },
setup() {
const formRef: any = ref(null);
const message = useMessage();
const [register, { setFieldsValue }] = useForm({ const [register, {}] = useForm({
gridProps: { cols: 1 }, gridProps: { cols: 1 },
collapsedRows: 3, collapsedRows: 3,
labelWidth: 120, labelWidth: 120,
layout: 'horizontal', layout: 'horizontal',
submitButtonText: '提交预约', submitButtonText: '提交预约',
schemas, schemas,
});
function setName() {
setFieldsValue({ name: '小马哥' });
}
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
return {
register,
formRef,
handleSubmit,
handleReset,
setName,
};
},
}); });
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -120,10 +120,10 @@
giProps: { giProps: {
//span: 24, //span: 24,
}, },
defaultValue: 1183135260000,
componentProps: { componentProps: {
type: 'date', type: 'date',
clearable: true, clearable: true,
defaultValue: 1183135260000,
onUpdateValue: (e: any) => { onUpdateValue: (e: any) => {
console.log(e); console.log(e);
}, },

View File

@@ -0,0 +1,114 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="富文本">
富文本用于展示图文信息比如商品详情文章详情等...
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<QuillEditor
ref="quillEditor"
:options="options"
v-model:content="myContent"
style="height: 350px"
@ready="readyQuill"
class="quillEditor"
/>
<template #footer>
<n-space>
<n-button @click="addText">增加文本</n-button>
<n-button @click="addImg">增加图片</n-button>
<n-button @click="getHtml">获取HTML</n-button>
</n-space>
</template>
</n-card>
<n-card :bordered="false" class="mt-4 proCard" title="HTML 内容">
<n-input
v-model:value="myContentHtml"
type="textarea"
placeholder="html"
:autosize="{
minRows: 3,
maxRows: 6,
}"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
const quillEditor = ref();
const myContent = ref(
'<h4>Naive Ui Admin 是一个基于 vue3,vite2,TypeScript 的中后台解决方案</h4>'
);
const myContentHtml = ref(
'<h4>Naive Ui Admin 是一个基于 vue3,vite2,TypeScript 的中后台解决方案</h4>'
);
const options = reactive({
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
[{ direction: 'rtl' }], // text direction
[{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
['clean'],
['image'],
],
},
theme: 'snow',
placeholder: '输入您喜欢的内容吧!',
});
function readyQuill() {
console.log('Quill准备好了');
}
function getHtml() {
myContentHtml.value = getHtmlVal();
}
function addText() {
const html = getHtmlVal() + '新增加的内容';
quillEditor.value.setHTML(html);
}
function addImg() {
const html =
getHtmlVal() +
'<img style="width:100px" src="https://www.baidu.com/img/flexible/logo/pc/result.png"/>';
quillEditor.value.setHTML(html);
}
function getHtmlVal() {
return quillEditor.value.getHTML();
}
</script>
<style lang="less">
.ql-toolbar.ql-snow {
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid #eee;
margin-top: -10px;
}
.ql-container.ql-snow {
border: none;
}
</style>

View File

@@ -5,10 +5,12 @@ export const columns = [
{ {
title: 'id', title: 'id',
key: 'id', key: 'id',
width: 100,
}, },
{ {
title: '编码', title: '编码',
key: 'no', key: 'no',
width: 100,
}, },
{ {
title: '名称', title: '名称',
@@ -22,6 +24,7 @@ export const columns = [
{ {
title: '头像', title: '头像',
key: 'avatar', key: 'avatar',
width: 100,
render(row) { render(row) {
return h(NAvatar, { return h(NAvatar, {
size: 48, size: 48,
@@ -47,29 +50,34 @@ export const columns = [
}, },
edit: true, edit: true,
width: 200, width: 200,
ellipsis: false,
}, },
{ {
title: '开始日期', title: '开始日期',
key: 'beginTime', key: 'beginTime',
edit: true, edit: true,
width: 250, width: 160,
editComponent: 'NDatePicker', editComponent: 'NDatePicker',
editComponentProps: { editComponentProps: {
type: 'datetime', type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss', format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
}, },
ellipsis: false,
}, },
{ {
title: '结束日期', title: '结束日期',
key: 'endTime', key: 'endTime',
width: 200, width: 160,
}, },
{ {
title: '创建时间', title: '创建时间',
key: 'date', key: 'date',
width: 160,
}, },
{ {
title: '停留时间', title: '停留时间',
key: 'time', key: 'time',
width: 80,
}, },
]; ];

View File

@@ -8,6 +8,7 @@
:row-key="(row) => row.id" :row-key="(row) => row.id"
ref="actionRef" ref="actionRef"
:actionColumn="actionColumn" :actionColumn="actionColumn"
:scroll-x="1360"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
> >
<template #toolbar> <template #toolbar>
@@ -17,105 +18,90 @@
</n-card> </n-card>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, reactive, toRefs, ref, h } from 'vue'; import { reactive, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table'; import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list'; import { getTableList } from '@/api/table/list';
import { columns } from './basicColumns'; import { columns } from './basicColumns';
import { useDialog, useMessage } from 'naive-ui'; import { useDialog, useMessage } from 'naive-ui';
export default defineComponent({ const message = useMessage();
components: { BasicTable }, const dialog = useDialog();
setup() { const actionRef = ref();
const message = useMessage();
const dialog = useDialog(); const params = reactive({
const actionRef = ref(); pageSize: 5,
const state = reactive({ name: 'xiaoMa',
params: { });
pageSize: 5,
name: 'xiaoMa', const actionColumn = reactive({
}, width: 150,
actionColumn: { title: '操作',
width: 150, key: 'action',
title: '操作', fixed: 'right',
key: 'action', align: 'center',
fixed: 'right', render(record) {
align: 'center', return h(TableAction, {
render(record) { style: 'button',
return h(TableAction, { actions: createActions(record),
style: 'button',
actions: createActions(record),
});
},
},
}); });
function createActions(record) {
return [
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
];
}
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function handleDelete(record) {
console.log(record);
dialog.info({
title: '提示',
content: `您想删除${record.name}`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('删除成功');
},
onNegativeClick: () => {},
});
}
function handleEdit(record) {
console.log(record);
message.success('您点击了编辑按钮');
}
return {
...toRefs(state),
columns,
actionRef,
loadDataTable,
onCheckedRow,
reloadTable,
};
}, },
}); });
function createActions(record) {
return [
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
];
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function handleDelete(record) {
console.log(record);
dialog.info({
title: '提示',
content: `您想删除${record.name}`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('删除成功');
},
onNegativeClick: () => {},
});
}
function handleEdit(record) {
console.log(record);
message.success('您点击了编辑按钮');
}
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@@ -5,19 +5,22 @@ export const columns = [
{ {
title: 'id', title: 'id',
key: 'id', key: 'id',
width: 100,
}, },
{ {
title: '编码', title: '编码',
key: 'no', key: 'no',
width: 100,
}, },
{ {
title: '名称', title: '名称',
key: 'name', key: 'name',
width: 200, width: 100,
}, },
{ {
title: '头像', title: '头像',
key: 'avatar', key: 'avatar',
width: 100,
render(row) { render(row) {
return h(NAvatar, { return h(NAvatar, {
size: 48, size: 48,
@@ -28,21 +31,22 @@ export const columns = [
{ {
title: '地址', title: '地址',
key: 'address', key: 'address',
width: 200, width: 150,
}, },
{ {
title: '开始日期', title: '开始日期',
key: 'beginTime', key: 'beginTime',
width: 200, width: 160,
}, },
{ {
title: '结束日期', title: '结束日期',
key: 'endTime', key: 'endTime',
width: 200, width: 160,
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
width: 100,
render(row) { render(row) {
return h( return h(
NTag, NTag,
@@ -58,9 +62,11 @@ export const columns = [
{ {
title: '创建时间', title: '创建时间',
key: 'date', key: 'date',
width: 160,
}, },
{ {
title: '停留时间', title: '停留时间',
key: 'time', key: 'time',
width: 80,
}, },
]; ];

View File

@@ -7,10 +7,10 @@
:request="loadDataTable" :request="loadDataTable"
:row-key="(row) => row.id" :row-key="(row) => row.id"
ref="actionRef" ref="actionRef"
:actionColumn="actionColumn"
@edit-end="editEnd" @edit-end="editEnd"
@edit-change="onEditChange" @edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
:scroll-x="1360"
> >
<template #toolbar> <template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button> <n-button type="primary" @click="reloadTable">刷新数据</n-button>
@@ -19,113 +19,41 @@
</n-card> </n-card>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, reactive, toRefs, ref, h } from 'vue'; import { reactive, ref } from 'vue';
import { BasicTable, TableAction } from '@/components/Table'; import { BasicTable } from '@/components/Table';
import { getTableList } from '@/api/table/list'; import { getTableList } from '@/api/table/list';
import { columns } from './CellColumns'; import { columns } from './CellColumns';
export default defineComponent({ const actionRef = ref();
components: { BasicTable }, const params = reactive({
setup() { pageSize: 5,
const actionRef = ref(); name: 'xiaoMa',
const currentEditKeyRef = ref('');
const state = reactive({
params: {
pageSize: 5,
name: 'xiaoMa',
},
actionColumn: {
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
},
});
function handleEdit(record) {
currentEditKeyRef.value = record.key;
record.onEdit?.(true);
}
function handleCancel(record: EditRecordRow) {
currentEditKeyRef.value = '';
record.onEdit?.(false, false);
}
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
async function handleSave(record: EditRecordRow) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
}
function createActions(record) {
if (!record.editable) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
} else {
return [
{
label: '保存',
onClick: handleSave.bind(null, record),
},
{
label: '取消',
onClick: handleCancel.bind(null, record),
},
];
}
}
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
return {
...toRefs(state),
columns,
actionRef,
loadDataTable,
onCheckedRow,
reloadTable,
editEnd,
onEditChange,
};
},
}); });
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@@ -11,6 +11,7 @@
@edit-end="editEnd" @edit-end="editEnd"
@edit-change="onEditChange" @edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
:scroll-x="1590"
> >
<template #toolbar> <template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button> <n-button type="primary" @click="reloadTable">刷新数据</n-button>
@@ -19,113 +20,95 @@
</n-card> </n-card>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, reactive, toRefs, ref, h } from 'vue'; import { reactive, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table'; import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list'; import { getTableList } from '@/api/table/list';
import { columns } from './rowColumns'; import { columns } from './rowColumns';
export default defineComponent({ const actionRef = ref();
components: { BasicTable }, const currentEditKeyRef = ref('');
setup() { const params = reactive({
const actionRef = ref(); pageSize: 5,
const currentEditKeyRef = ref(''); name: 'xiaoMa',
const state = reactive({ });
params: {
pageSize: 5, const actionColumn = reactive({
name: 'xiaoMa', width: 150,
}, title: '操作',
actionColumn: { key: 'action',
width: 150, fixed: 'right',
title: '操作', align: 'center',
key: 'action', render(record) {
fixed: 'right', return h(TableAction, {
align: 'center', style: 'button',
render(record) { actions: createActions(record),
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
},
}); });
function handleEdit(record) {
currentEditKeyRef.value = record.key;
record.onEdit?.(true);
}
function handleCancel(record: EditRecordRow) {
currentEditKeyRef.value = '';
record.onEdit?.(false, false);
}
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
async function handleSave(record: EditRecordRow) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
}
function createActions(record) {
if (!record.editable) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
} else {
return [
{
label: '保存',
onClick: handleSave.bind(null, record),
},
{
label: '取消',
onClick: handleCancel.bind(null, record),
},
];
}
}
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
return {
...toRefs(state),
columns,
actionRef,
loadDataTable,
onCheckedRow,
reloadTable,
editEnd,
onEditChange,
};
}, },
}); });
function handleEdit(record) {
currentEditKeyRef.value = record.key;
record.onEdit?.(true);
}
function handleCancel(record) {
currentEditKeyRef.value = '';
record.onEdit?.(false, false);
}
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
async function handleSave(record) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
}
function createActions(record) {
if (!record.editable) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
} else {
return [
{
label: '保存',
onClick: handleSave.bind(null, record),
},
{
label: '取消',
onClick: handleCancel.bind(null, record),
},
];
}
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

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