270 Commits
v1.5 ... v1.9.1

Author SHA1 Message Date
ahjung
ee8aed2f62 1.9.1 2024-04-09 16:28:28 +08:00
Ah jung
260397f3ae Merge pull request #279 from thatsgolden/patch-1
feat(BasicForm): trigger query via Enter key
2024-04-09 15:45:58 +08:00
Golden
fa983a9b64 feat(BasicForm): trigger query via Enter key
- Allow push Enter key to query the table form.
2024-03-04 13:36:55 +08:00
Ah jung
4c49e28596 Merge pull request #272 from Mevolove/main
style: 加宽基础表单,分步表单和上传图片中label的宽度
2024-02-18 13:38:49 +08:00
Mevol
9a5fe5249c style: 加宽基础表单,分步表单和上传图片中label的宽度 2024-01-16 15:19:10 +08:00
Ah jung
02235bf6fc Merge pull request #265 from NogiRuka/style/center-upload-icon-and-text-in-basicForm
style(BasicUpload): center upload icon and text in BasicForm
2023-10-25 19:31:11 +08:00
“乃木流架”
0656035857 style(BasicUpload): center upload icon and text in BasicForm 2023-10-21 23:54:14 +09:00
Ah jung
81e8222461 Merge pull request #257 from L1yp/fix-transition-hot-refresh
fix: 修复transition组件导致的白屏(重现: 热更新transition下的组件script代码切换tab即可)
2023-08-10 15:07:26 +08:00
Lyp
09411a927e fix: 修复transition组件导致的白屏(重现: 热更新transition下的组件script代码切换tab即可) 2023-08-10 14:55:16 +08:00
ahjung
347cd91735 1.9.0 2023-08-10 13:54:53 +08:00
ahjung
9b2effbc55 README 更新 2023-08-08 21:45:41 +08:00
Ah jung
d72f42dcd8 Merge pull request #249 from little-alei/main
优化ts类型
2023-07-29 13:16:20 +08:00
抠脚本人
165c358ef5 取消TableActionType的泛型 2023-07-29 12:31:31 +08:00
抠脚本人
fc181ce543 修改eslint 中 no-unused-vars未尤大推荐推荐,避免多余,错误的检测 2023-07-29 12:27:27 +08:00
抠脚本人
f04b9ba7f9 BasicColumn泛型增加默认类型 2023-07-29 12:25:17 +08:00
抠脚本人
ee0e507e47 Table组件columns增加泛型 2023-07-28 11:39:35 +08:00
抠脚本人
9d18715e90 调整vite/client的加载方式 2023-07-28 11:24:07 +08:00
Ah jung
74f0e4764e Merge pull request #242 from cyunrei/fix_margins
fix: adjust margins to prevent text from being cut off
2023-05-11 11:10:26 +08:00
Cyunrei
4cbdd9bc66 fix: adjust margins to prevent text from being cut off 2023-05-11 10:56:08 +08:00
Ah jung
e03b1bdfca Merge pull request #241 from MyFaith/main
fix: incorrect component name
2023-05-10 15:41:42 +08:00
MyFaith
b6a350e1be fix: incorrect component name 2023-05-09 15:34:17 +08:00
Ah jung
503437a433 Merge pull request #237 from bantangzm/fix-role
fix: formRef is not used and undefined
2023-05-04 13:38:43 +08:00
bantangzm
d4518849bf fix: formRef is not used and undefined 2023-04-28 10:12:37 +08:00
Ah jung
39178761b1 Merge pull request #233 from WangHansen/refactor/1.8.3
refactor: remove unused files, bug fixes, name changes
2023-04-24 18:33:20 +08:00
Hansen Wang
934de6b1e6 Add animation less back 2023-04-24 03:57:10 +08:00
Hansen Wang
58d13a242d Remove unused styles 2023-04-24 02:50:30 +08:00
Hansen Wang
5d891c1f44 refactor: remove unused files, bug fixes and name changes 2023-04-24 02:09:39 +08:00
Ah jung
e1528823f7 Merge pull request #232 from CodeListener/fix/utils
fix(utils): fix type error
2023-04-11 18:15:24 +08:00
yangguang
1fa35db71b fix(utils): fix type error 2023-04-11 14:20:18 +08:00
Ah jung
80729d925e Merge pull request #228 from Elben9/fix-projectStoreGetters
fix: projectSetting内getters getCrumbsSetting返回对象错误
2023-03-27 17:38:07 +08:00
chenxuehai@alltuu.com
e228184b72 fix: pinia的projectSetting内getters getCrumbsSetting返回对象错误 2023-03-24 17:42:17 +08:00
Ah jung
2f541106f2 Update README.md 2023-03-15 11:04:20 +08:00
Ah jung
5f81f4cc77 Update README.md 2023-03-15 11:03:59 +08:00
Ah jung
b4f32f50c5 Update README.md 2023-03-12 14:01:50 +08:00
Ah jung
5d180b6fd5 Merge pull request #225 from Elben9/fix-importMetaGlobTypeError
fix: add type for importMetaGlob
2023-03-12 13:58:12 +08:00
chenxuehai@alltuu.com
3d6465346c fix: add type for importMetaGlob 2023-03-10 11:23:06 +08:00
Ah jung
dc98b96ce2 Merge pull request #221 from zmlnf/fix/pageHeader
fix: reload page warning
2023-03-09 08:55:14 +08:00
Ah jung
3f84af912f Merge pull request #220 from zmlnf/fix/basicList
fix: rules type missMatch
2023-03-09 08:54:31 +08:00
Ah jung
8dcd66b654 Merge pull request #219 from zmlnf/fix/createDrawer
fix: type missMatch
2023-03-09 08:54:05 +08:00
Ah jung
4bdc54a77f Merge pull request #217 from Elben9/feat-importMetaUpdate
feat: ImportMeta中globEager已废弃,改为glob('*', { eager: true })
2023-03-09 08:53:37 +08:00
Ah jung
0ceb7437a4 Merge pull request #211 from shark-lajiao/main
fix:keepAliveComponents
2023-03-09 08:53:12 +08:00
Ah jung
b984828f33 Merge pull request #213 from Stream-web/achuang-admin
fix:修改loginDesc
2023-03-09 08:52:48 +08:00
许诺
1d10826760 Merge branch 'jekip:main' into main 2023-03-08 13:28:44 +08:00
ziming.tang
74334de7e0 fix: reload page warning 2023-02-23 14:20:06 +08:00
ziming.tang
e153837497 fix: rules type missMatch 2023-02-23 14:00:17 +08:00
ziming.tang
d0ec44b48c fix: type missMatch 2023-02-23 13:55:59 +08:00
chenxuehai@alltuu.com
e9efbb5e67 feat: ImportMeta中globEager已废弃,改为glob('*', { eager: true }) 2023-02-21 17:31:58 +08:00
Ah jung
aa90e0a468 Update README.md 2023-02-09 09:16:58 +08:00
15398271468
9049f743a8 fix:修改loginDesc 2023-01-01 22:15:07 +08:00
鲨鱼辣椒
1f858929d6 fix:keepAliveComponents 2022-12-30 10:21:03 +08:00
Ah jung
5a50bc0dc4 Merge pull request #209 from 377342263/main
修复当pagination传Boolean类型时,表格高度计算有误的问题
2022-12-21 18:02:36 +08:00
黄富鑫
a35318f808 修复当pagination传Boolean类型时,表格高度计算有误的问题 2022-12-21 16:17:53 +08:00
Ah jung
7a18f58f4a Merge pull request #207 from xiangshu233/main
perf: 单独拆分 naive 全局 API 挂载方法
2022-12-19 11:44:40 +08:00
xiangshu233
3fb8c85112 perf: 单独拆分 naive 全局 API 挂载方法 2022-12-17 15:39:40 +08:00
Ah jung
921dd91eda Merge pull request #206 from xiangshu233/main
解决 eslint 版本升级导致所有 index.vue 会报错的问题
2022-12-15 18:01:33 +08:00
xiangshu233
bcd2480f69 解决 eslint 版本升级导致所有 index.vue 会报错的问题 2022-12-15 17:53:40 +08:00
Ah jung
590d1615cb Merge pull request #205 from vastsa/main
fix:两杯都*1000,导致最终过期时间为7000天
2022-12-13 16:44:16 +08:00
lan-air
23dcfe000b fix:两杯都*1000,导致最终过期时间为7000天 2022-12-11 19:14:25 +08:00
Ah jung
47e47c8a59 Merge pull request #201 from yuhengshen/main
fix: tabs-view 宽度计算错误
2022-12-07 09:06:35 +08:00
yuhengshen
719ad34a94 fix: 类型错误修正 2022-11-28 23:40:01 +08:00
yuhengshen
a0deba504a fix: tabs-view 容器宽度计算错误 2022-11-28 23:39:09 +08:00
xiaoma
25d6134923 1.8.2 2022-11-24 09:19:19 +08:00
Ah jung
a646de72e2 Merge pull request #200 from shark-lajiao/main
feat:防抖节流等指令
2022-11-23 14:04:37 +08:00
chen shu tao
e63963cf08 feat:防抖节流等指令 2022-11-23 13:40:09 +08:00
Ah jung
c43ec3ef1c Merge pull request #198 from Qujh97/main
fix: 修复暗黑模式下左侧菜单主题色不生效问题
2022-11-18 10:31:44 +08:00
qu
4f0a7967f7 fix: 修复暗黑模式下左侧菜单主题色不生效问题 2022-11-18 10:28:51 +08:00
Ah jung
1c594b49c0 Merge pull request #196 from elonehoo/main
提升了包的版本,做了迁移
2022-11-12 08:51:25 +08:00
elonehoo
b68103455b fix:modified to the previous injection 2022-11-11 23:41:56 +08:00
elonehoo
724af94c08 revert:upgraded the version of all packages to the latest version, and made an upgrade before cross-version 2022-11-11 23:38:37 +08:00
Ah jung
af68d4bc67 Merge pull request #195 from 15982650876/feat/shark
feat:add 增加了菜单font图标的适配
2022-11-10 21:05:19 +08:00
chen shu tao
2fb36695db feat:add 增加了菜单font图标的适配 2022-11-10 13:15:17 +08:00
xiaoma
32116be1f2 fix icon error 2022-11-04 10:18:51 +08:00
Ah jung
4a47fa2f7d Merge pull request #192 from honorsuper/patch-5
feat: 左侧菜单,过滤children下面的item为hidden状态
2022-11-03 17:07:49 +08:00
Ah jung
89f68789ff Merge pull request #193 from honorsuper/patch-6
feat: 类型修改
2022-11-03 17:07:32 +08:00
honorsuper
4c0de9d5c2 feat: 类型修改 2022-11-03 11:30:59 +08:00
honorsuper
1e72351dea feat: 左侧菜单,过滤children下面的item为hidden状态 2022-11-02 10:48:12 +08:00
Ah jung
da24659944 Update Storage.ts 2022-10-31 18:23:39 +08:00
Ah jung
eb21a99df1 Merge pull request #190 from thelostword/refactor/naive-global-api
refactor: naive-ui版本升级,naive全局API挂载方式修改
2022-10-29 15:36:39 +08:00
Ah jung
c598d3f7bc Create README.md 2022-10-29 14:32:21 +08:00
losting
fa0163a385 naive全局API挂载方式修改 2022-10-28 18:26:51 +08:00
losting
57fef38bfe 删除头部注释 2022-10-28 18:25:34 +08:00
losting
d62ca84328 删除头部注释 2022-10-28 18:24:35 +08:00
thelostword
c504e61d70 refactor: naive-ui版本升级,naive全局API挂载方式修改 2022-10-28 18:15:51 +08:00
Ah jung
c0d3e11346 Merge pull request #189 from thelostword/refactor/setupNaive
refactor: naive-ui 组件注册优化
2022-10-25 16:21:21 +08:00
thelostword
4c27623a0f refactor: naive-ui 组件注册优化 2022-10-25 15:59:13 +08:00
Ah jung
0107c5f035 Update README.md 2022-10-09 14:57:55 +08:00
Ah jung
6134a4c125 Merge pull request #182 from godkun/main
fix: 替换项目文档地址
2022-10-08 08:59:25 +08:00
godkun
00dd7409b1 fix: 替换项目文档地址 2022-10-03 16:48:39 +08:00
Ah jung
a7ad4a3634 Update README.md 2022-09-30 14:59:31 +08:00
Ah jung
327f8de00b Merge pull request #179 from honorsuper/patch-4
pre: 代码优化
2022-09-24 09:04:29 +08:00
honorsuper
026e4c1acd pre: 代码优化 2022-09-23 09:28:58 +08:00
Ah jung
9ba51e4a34 Merge pull request #170 from soup-Lee/main
fix: 修复面包屑只有一个title的时候左边会多出一个空span的问题
2022-08-26 09:13:34 +08:00
“青菜白玉汤”
bc2d2b4e94 fix: 修复面包屑只有一个title的时候左边会多出一个空span的问题 2022-08-19 17:05:06 +08:00
Ah jung
e2f90cbf88 Merge pull request #169 from honorsuper/patch-3
feat: add GraphicComponent
2022-08-19 10:38:40 +08:00
honorsuper
2da9b8a465 feat: add GraphicComponent 2022-08-19 10:26:11 +08:00
Ah jung
f99ecea202 Merge pull request #163 from jerry4718/main
修正isAsyncFunction的类型描述
2022-08-17 18:02:31 +08:00
Ah jung
e3326a0f11 Merge pull request #165 from honorsuper/patch-2
docs: 文案修改
2022-08-17 18:02:05 +08:00
Ah jung
775f5b63af Merge pull request #167 from soup-Lee/main
fix: 修复顶级路由中面包屑多了一个‘/’的问题
2022-08-17 18:01:21 +08:00
“青菜白玉汤”
874fd3437d fix: 修复顶级路由中面包屑多了一个‘/’的问题 2022-08-17 17:01:05 +08:00
honorsuper
7644c59799 docs: 文案修改 2022-08-15 15:31:10 +08:00
码梦天涯
f8ed3d1607 修正isAsyncFunction的类型描述 2022-08-10 09:45:31 +08:00
Ah jung
0c8bb030de Merge pull request #161 from honorsuper/patch-1
feat: 增加主动销毁echarts实例方法
2022-08-09 10:21:54 +08:00
honorsuper
41a6f59617 feat: 增加主动销毁echarts实例方法 2022-08-09 10:13:36 +08:00
Ah jung
1a7eaf7efb Merge pull request #159 from gp0119/main
fix(BasicUpload): 修复默认图片不显示bug,默认图片加载错误时删除按钮不显示bug
2022-08-01 08:12:57 +08:00
ganpeng
5714233d57 fix(BasicUpload): 修复默认图片不显示bug,默认图片加载错误时删除按钮不显示bug 2022-07-28 15:01:17 +08:00
Ah jung
5e2452f2df Merge pull request #156 from yirandidi/main
fix: 路由配置activeMenu的bug
2022-07-25 20:11:23 +08:00
yiran
e9d3e1affe Merge branch 'main' of https://github.com/yirandidi/naive-ui-admin 2022-07-25 18:20:21 +08:00
yiran
f6d419b94f 🐞 fix: 路由配置activeMenu的bug
https://github.com/jekip/naive-ui-admin/issues/151
2022-07-25 18:20:16 +08:00
Ah jung
cc0d1824f2 Merge pull request #155 from yirandidi/main
 feat: 添加网站配置文件
2022-07-25 18:03:42 +08:00
yiran
299f8c89a5 feat: 添加网站配置文件 2022-07-25 17:52:33 +08:00
Ah jung
9e40480b9b Merge pull request #153 from aqi-lu/feat/aqi
fix: 修复ts类型错误
2022-07-24 21:19:14 +08:00
AQI
c20ee24cd8 fix: 修复ts类型错误 2022-07-23 18:15:38 +08:00
Ah jung
eb27f19087 Merge pull request #149 from xiangshu233/main
BaseTableAction 新增 icon 配置项 + 表格斑马纹
2022-07-14 16:52:21 +08:00
xiangshu233
3cbb6061c5 BaseTableActin 新增 icon 配置项 + 表格斑马纹 2022-07-14 16:31:31 +08:00
Ah jung
e07e36ce5d Merge pull request #141 from yulimchen/login-styles
fix(login): 登录页手机端样式问题
2022-06-24 16:52:56 +08:00
yulimchen
0f8b0d3557 fix(login): 登录页手机端样式问题 2022-06-24 16:36:03 +08:00
Ah jung
28d2696061 Merge pull request #139 from baixi233/fixbug
fixbug
2022-06-21 22:31:12 +08:00
baixi233
b2c22996ce fix tagsview 2022-06-21 11:30:59 +08:00
Ah jung
f616852053 Merge pull request #137 from cyunrei/fix_placeholder
fix: 修复错误的placeholder提示信息
2022-06-14 11:31:47 +08:00
Cyunrei
92316285bb fix: 修复错误的placeholder提示信息 2022-06-14 11:23:28 +08:00
Ah jung
1077a20cbd Merge pull request #136 from Zheng-Changfu/fix/table-fetch-interceptor-statement
fix(function):修复 useDataSource 中请求前请求后回调未在prop中声明、isFunction支持普通、箭头函数判断
2022-06-14 08:52:55 +08:00
郑常富
7b9b391adc fix(function):修复table中hook(useDataSource)中请求前请求后回调未在prop中声明、isFunction支持普通、箭头函数判断 2022-06-13 22:02:37 +08:00
Ah jung
88ada7ebee Merge pull request #134 from xiangshu233/main
修复 useECharts Hooks 报错和暗色模式 bug
2022-06-08 20:00:11 +08:00
xiangshu233
a7257568cf 修复 useECharts Hooks 报错和暗色模式 bug 2022-06-08 19:56:43 +08:00
Ah jung
403a844668 Merge pull request #133 from aqi-lu/feat/aqi
feat(function): add 菜单权限添加删除
2022-06-06 08:46:33 +08:00
Ah jung
197e63cb51 Merge pull request #131 from fanck0605/patch-1
fix: 修复错误的 URL 正则表达式
2022-06-06 08:45:52 +08:00
AQI
6c3273e214 feat(function): add 菜单权限添加删除 2022-06-05 21:34:55 +08:00
Chuck
71be58fc1f fix: 修复错误的 URL 正则表达式 2022-06-02 23:12:54 +08:00
Ah jung
e3e14b8259 Merge pull request #130 from gp0119/main
fix: 删除无用代码和修复一些错误
2022-06-01 16:24:55 +08:00
ganpeng
5f625aa305 fix: 删掉无用的css 2022-06-01 14:02:54 +08:00
ganpeng
8167d4165b fix: 修复card组件prop参数值类型错误 2022-06-01 14:02:11 +08:00
ganpeng
873e0d3e43 fix: 修复错误 2022-06-01 13:57:02 +08:00
ganpeng
f20f4209a3 fix: 修复ts类型错误 2022-06-01 13:54:32 +08:00
ganpeng
363ed7ae9e fix: 删除无用代码 2022-06-01 13:49:38 +08:00
Ah jung
a21cc8aec8 Merge pull request #126 from Zheng-Changfu/feat/table-fetch-interceptor
feat(function): add table-fetch(before and after interceptor)
2022-05-31 15:55:18 +08:00
Ah jung
0f229aea13 Merge pull request #128 from gp0119/main
fix: 修复ts类型找不到错误
2022-05-31 15:54:38 +08:00
ganpeng
89151afce3 fix: 修复ts类型找不到错误 2022-05-30 22:08:58 +08:00
Ah jung
ffac6c0d78 Merge pull request #127 from zhouyuf/main
fix: 修复dashboard页mock返回数据显示错误
2022-05-30 11:07:09 +08:00
zhouyuf
c257ca0948 fix: 修复dashboard页mock返回数据显示错误 2022-05-28 17:24:00 +08:00
zhouyuf
9bcd6d9700 Merge remote-tracking branch 'upstream/main' 2022-05-28 17:02:55 +08:00
郑常富
a66f56b9ce feat(function): add table-fetch(before and after interceptor) 2022-05-28 15:03:16 +08:00
xiaoma
9e58578706 version update 2022-05-11 11:45:00 +08:00
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
zhouyuf
e6505c88b7 Merge pull request #2 from jekip/main
update
2021-08-14 19:50:20 +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
zhouyuf
55ee389184 Merge pull request #1 from jekip/main
update
2021-08-08 20:18:36 +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
Ah jung
5c5c52d9fa 日常版本更新 2021-08-06 17:10:54 +08:00
Ah jung
3e0b8efe7e Fixes bug 新增 顶部混合菜单 2021-08-06 17:06:33 +08:00
Ah jung
450234e7ea Fixes bug 2021-08-05 21:38:32 +08:00
Ah jung
5116c387d5 Merge remote-tracking branch 'origin/main' 2021-08-05 17:24:55 +08:00
Ah jung
8a5f237630 Fixes bug add baseModal | baseForm 组件 2021-08-05 17:24:24 +08:00
Ah jung
1e3ccaa6dc Update README.md 2021-08-03 08:30:08 +08:00
Ah jung
98e1bf0227 Merge pull request #17 from Chika99/fix/table
fix(table): 修复表格分页计算问题
2021-08-02 09:06:43 +08:00
chika
6a290b314a fix(table): 修复表格分页计算问题 2021-08-01 23:07:37 +08:00
Ah jung
58f0997fb6 Merge pull request #16 from Chika99/fix/lockscreen
fix(lockscreen): 修复锁屏几个问题
2021-07-31 14:30:17 +08:00
chika
e602fc50c0 fix(lockscreen): 调整电量显示位置 2021-07-31 14:25:52 +08:00
chika
81a3e6d970 fix(lockscreen): 修复电池组件在宽度较小的情况下会与时间重叠 2021-07-31 14:20:47 +08:00
chika
0c709871f3 fix(lockscreen): 修复距离电池充满时间显示错误 2021-07-31 14:11:24 +08:00
Ah jung
9d9cac8064 Fixes bug add Project Docs 2021-07-30 20:00:38 +08:00
Ah jung
b8f8334539 Merge remote-tracking branch 'origin/main' 2021-07-30 19:55:54 +08:00
Ah jung
361a2a14c7 Merge pull request #15 from Chika99/feat/loadingBar
Feat/loading bar
2021-07-30 19:55:09 +08:00
chika
dd4e6c1670 feat(loadingBar): 增加加载条与主题颜色适配 2021-07-30 19:32:59 +08:00
chika
85d39add87 feat(loadingBar): 增加加载条与主题颜色适配 2021-07-30 19:26:41 +08:00
Ah jung
20da92aeab Fixes bug 2021-07-30 17:27:58 +08:00
Ah jung
57245d21ee Merge pull request #12 from it-fork/fix/naming-conv
fix(命名规范):修复store中重构之后遗留的命名错误
2021-07-30 14:57:02 +08:00
Ah jung
f97a94e74c 版本号规范 2021-07-30 14:45:26 +08:00
liujunzheng
fd6fd723d7 fix(命名规范):修复store中重构之后遗留的命名错误 2021-07-30 14:39:52 +08:00
Ah jung
da5231b384 Merge pull request #9 from Chika99/fix/basicForm
fix(basicForm): 修复基础表单在宽度小于640px显示问题
2021-07-30 13:39:14 +08:00
chika
97ae37efd0 fix(basicForm): 修复基础表单在宽度小于640px显示问题 2021-07-30 13:18:01 +08:00
Ah jung
b19430170f fix Bug 2021-07-30 11:37:09 +08:00
Ah jung
b642d28815 fix Bug 2021-07-30 10:33:24 +08:00
201 changed files with 15527 additions and 10977 deletions

View File

@@ -15,7 +15,7 @@ VITE_DROP_CONSOLE = true
# 跨域代理,可以配置多个,请注意不要换行
#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 接口地址
VITE_GLOB_API_URL =

View File

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

View File

@@ -22,9 +22,10 @@ module.exports = defineConfig({
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'plugin:jest/recommended',
],
rules: {
'vue/script-setup-uses-vars': 'error',
'vue/multi-word-component-names': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
@@ -37,19 +38,12 @@ module.exports = defineConfig({
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '.*', args: 'none' }],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
// we are only using this rule to check for unused arguments since TS
// catches unused variables but not args.
{ varsIgnorePattern: '.*', args: 'none' },
],
'space-before-function-paren': 'off',

2
.gitignore vendored
View File

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

View File

@@ -1,4 +1,163 @@
# 1.5 (2021-07-30)
# CHANGELOG
## 1.9.1
- 优化 `typeSctipt` 类型定义
- 优化 `setup` 语法
- 依赖升级
## 1.9.0
- 新增 `BasicForm` 组件,支持 `setLoading`, `setSchema` 方法
- 新增 `countField` 总数字段名配置
- 优化 `yarn` 切换至 `pnpm`
- 优化 `BasicForm` 组件,验证返回值
- 优化 `BasicTable` 组件
- 修复 `TableAction组件左右间隔不生效` 关闭[253](https://github.com/jekip/naive-ui-admin/issues/253)
- 修复 `BasicTable组件没有数据会一直请求接口` 关闭[#251](https://github.com/jekip/naive-ui-admin/issues/251)
- 修复 `useModal+useForm组件的bug` 关闭[#250](https://github.com/jekip/naive-ui-admin/issues/250)
- 修复 `手机端侧边导航风格不一致bug` 关闭[#247](https://github.com/jekip/naive-ui-admin/issues/247
- 移除 `yarn.lock` 文件
- 依赖升级
## 1.8.2
- ### ✨ Features
- 新增 `directive` 示例
- 依赖升级
### 🐛 Bug Fixes
- 修复 `样式异常` 图片
## 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)
### 🐛 Bug Fixes
- 修复已知bug
- ### ✨ Features
- 新增 `混合菜单模式`
- 新增 `根路由`
- 新增 `关于` 根路由示例页面
- 文档同步更新,组件和示例
# 1.5.1 (2021-08-05)
### 🐛 Bug Fixes
- 修复windows系统获取项目换行符问题
- 修复表格分页计算问题 [@Chika99](https://github.com/Chika99)
- 修复锁屏样式自适应问题 [@Chika99](https://github.com/Chika99)
- 依赖 dayjs 移除用date-fns和UI框架底层保持一致
- 修复已知bug
- ### ✨ Features
- 新增 `baseForm` 组件,和`基础``useForm`使用方式
- 新增 `baseModal`,组件,和 `useForm`使用方式
- 新增`子菜单` new Tag标签
- 菜单支持 `根路由`配置
# 1.5.0 (2021-07-30)
### 🐛 Bug Fixes
- 修复表格列配置,拖拽时最后的操作列重复增加
- 多标签页交互优化
@@ -15,7 +174,7 @@
- 本次更新,有破坏性更新,涉及文件重命名,增删调整
# 1.4 (2021-07-21)
# 1.4.0 (2021-07-21)
### 🐛 Bug Fixes
- vite降至2.3.6
- 多标签页交互优化
@@ -27,7 +186,7 @@
- 持续更新更多实用组件及示例感谢Star
# 1.3 (2021-07-19)
# 1.3.0 (2021-07-19)
### 🐛 Bug Fixes
- 修复多标签页左右切换按钮自适应展示
- 修复登录页面出现多标签页
@@ -40,7 +199,7 @@
- 持续更新更多实用组件及示例感谢Star
# 1.2 (2021-07-16)
# 1.2.0 (2021-07-16)
### 🐛 Bug Fixes
- 修复面包屑显示登录页面
- 菜单支持只展开当前父级菜单
@@ -54,7 +213,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.1 (2021-07-15)
# 1.1.0 (2021-07-15)
- ### ✨ Features
- 新增 `基础表单` 示例页面
- 新增 `分步表单` 示例页面
@@ -62,7 +221,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.0 (2021-07-12)
# 1.0.0 (2021-07-12)
### 🐛 Bug Fixes
- 修复页面切换面包屑未及时更新

View File

@@ -1,24 +1,58 @@
## 简介
[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 的语言
- **主题**:可配置的主题
- **Mock 数据** 内置 Mock 数据方案
- **权限** 内置完善的动态路由权限生成方案
- **组件** 二次封装了多个常用的组件
- 二次封装实用高扩展性组件
- 响应式、多主题,多配置,快速集成,开箱即用
- 最新技术栈,使用 `Vue3``Typescript``Pinia``Vite` 等前端前沿技术
- 强大的鉴权系统,对路由、菜单、功能点等支持`三种鉴权模式`,满足不同的业务鉴权需求
- 持续更新,实用性页面模板功能和交互,随意搭配组合,让构建页面变得简单化
## 在线预览
## 预览
- [naive-ui-admin](https://jekip.github.io)
账号admin密码123456随意
## 提示
如果开源版本的功能和组件,并不能满足您的需求,不妨看看,我们全新 `NaiveAdmin` 他或许能让您眼前一亮O(∩_∩)O哈哈~
[NaiveAdmin 官网](https://www.naiveadmin.com)
[NaiveAdmin 变更日志](https://www.yuque.com/u5825/zaqu0e)
[为什么选我们?](https://www.naiveadmin.com/choose/we)
### Plus
基于 `NaiveUi` 全新设计版本,新增众多特性,强烈推荐
[NaiveAdmin Plus 预览](https://plus.naiveadmin.com)
### Arco vue
智能设计体系,连接轻盈体验
[NaiveAdmin Arco 预览](https://arco.naiveadmin.com)
### Element Plus
面向设计师和开发者的组件库
[Element Plus Admin 预览](https://element.naiveadmin.com)
以上版本同时具备 `NaiveAdmin` 功能/组件/页面,一如既往、开箱即用,欢迎前往查看。
### Antd vue
新产品,如果您选的技术栈是 `Antd` 的话,不妨看看
[NaiveAdmin Antd 预览](https://antd.naiveadmin.com)
## 文档
[文档地址](https://jekip.github.io/docs/)
[文档地址](https://jekip.github.io/docs)
## 准备
@@ -31,7 +65,7 @@
- [Naive-ui-admin](https://www.naiveui.com/) - ui 基本使用
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
## 安装使用
## 使用
- 获取项目代码
@@ -64,9 +98,6 @@ yarn build
[CHANGELOG](./CHANGELOG.md)
## 感谢
[@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,再次感谢作者。
## 如何贡献
@@ -113,7 +144,13 @@ yarn build
## 交流
`Naive Ui Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供 QQ 交流群使用问题欢迎在群内提问。
`Naive Ui Admin` 使用或者其他问题,都可以在群内讨论或提问。
- QQ 群 `328347666`
![160335146-c28dd205-4600-4d62-b2c6-6456034ab7b1](https://user-images.githubusercontent.com/19426584/217689718-407e6cb9-dd3b-4a11-a025-3c58834b52ff.jpg)
## 赞助
#### 如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 🍹。
![donate](https://jekip.github.io/docs/images/sponsor.png)
[Paypal Me](https://www.paypal.com/paypalme/majunping)

View File

@@ -3,7 +3,7 @@
* @param env
*/
export const getConfigFileName = (env: Record<string, any>) => {
return `__PRODUCTION__${ env.VITE_GLOB_APP_SHORT_NAME || '__APP' }__CONF__`
return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__`
.toUpperCase()
.replace(/\s/g, '');
};

View File

@@ -18,19 +18,19 @@ function createConfig(
}: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} }
) {
try {
const windowConf = `window.${ configName }`;
const windowConf = `window.${configName}`;
// Ensure that the variable will not be modified
const configStr = `${ windowConf }=${ JSON.stringify(config) };
Object.freeze(${ windowConf });
Object.defineProperty(window, "${ configName }", {
const configStr = `${windowConf}=${JSON.stringify(config)};
Object.freeze(${windowConf});
Object.defineProperty(window, "${configName}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, '');
fs.mkdirp(getRootPath(OUTPUT_DIR));
writeFileSync(getRootPath(`${ OUTPUT_DIR }/${ configFileName }`), configStr);
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);
console.log(chalk.cyan(`✨ [${ pkg.name }]`) + ` - configuration file is build successfully:`);
console.log(chalk.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
} catch (error) {
console.log(chalk.red('configuration file configuration file failed to package:\n' + error));

View File

@@ -14,7 +14,7 @@ export const runBuild = async () => {
await runBuildConfig();
}
console.log(`${ chalk.cyan(`[${ pkg.name }]`) }` + ' - build successfully!');
console.log(`${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
} catch (error) {
console.log(chalk.red('vite build error:\n' + error));
process.exit(1);

View File

@@ -2,9 +2,9 @@
* Plugin to minimize and use ejs template syntax in index.html.
* https://github.com/anncwb/vite-plugin-html
*/
import type { Plugin } from 'vite';
import type { PluginOption } from 'vite';
import html from 'vite-plugin-html';
import { createHtmlPlugin } from 'vite-plugin-html';
import pkg from '../../../package.json';
import { GLOB_CONFIG_FILE_NAME } from '../../constant';
@@ -12,29 +12,29 @@ import { GLOB_CONFIG_FILE_NAME } from '../../constant';
export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env;
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${ VITE_PUBLIC_PATH }/`;
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`;
const getAppConfigSrc = () => {
return `${ path || '/' }${ GLOB_CONFIG_FILE_NAME }?v=${ pkg.version }-${ new Date().getTime() }`;
return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
};
const htmlPlugin: Plugin[] = html({
const htmlPlugin: PluginOption[] = createHtmlPlugin({
minify: isBuild,
inject: {
// Inject data into ejs template
injectData: {
data: {
title: VITE_GLOB_APP_TITLE,
},
// Embed the generated app.config.js file
tags: isBuild
? [
{
tag: 'script',
attrs: {
src: getAppConfigSrc(),
{
tag: 'script',
attrs: {
src: getAppConfigSrc(),
},
},
},
]
]
: [],
},
});

View File

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

View File

@@ -1,22 +0,0 @@
/**
* Introduces component library styles on demand.
* https://github.com/anncwb/vite-plugin-style-import
*/
import styleImport from 'vite-plugin-style-import';
export function configStyleImportPlugin(isBuild: boolean) {
if (!isBuild) return [];
const styleImportPlugin = styleImport({
libs: [
{
libraryName: 'ant-design-vue',
esModule: true,
resolveStyle: (name) => {
return `ant-design-vue/es/${ name }/style/index`;
},
},
],
});
return styleImportPlugin;
}

View File

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

View File

@@ -1,26 +1,122 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<html lang="zh-cmn-Hans" id="htmlRoot" data-theme="light">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="renderer" content="webkit"/>
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"/>
<meta content="webkit" name="renderer"/>
<meta
name="viewport"
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>
<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>
<body>
<div id="appProvider" style="display: none"></div>
<div id="app">
<div class="first-loading-wrp">
<div class="loading-wrp">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
<style>
.first-loading-wrap {
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>
<script type="module" src="/src/main.ts"></script>
<script>var globalThis = window;</script>
<script src="/src/main.ts" type="module"></script>
</body>
</html>

View File

@@ -1,6 +1,10 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
const modules = import.meta.globEager('./**/*.ts');
interface IModuleType {
default: any[];
}
const modules = import.meta.glob<IModuleType>('./**/*.ts', { eager: true });
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {

View File

@@ -5,7 +5,7 @@ const tableList = (pageSize) => {
const result: any[] = [];
doCustomTimes(pageSize, () => {
result.push({
id: '@integer(10,100)',
id: '@integer(10,999999)',
beginTime: '@datetime',
endTime: '@datetime',
address: '@city()',
@@ -27,12 +27,15 @@ export default [
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query;
const { page = 1, pageSize = 10, name } = query;
const list = tableList(Number(pageSize));
//并非真实,只是为了模拟搜索结果
const count = name ? 30 : 60;
return resultSuccess({
page: Number(page),
pageSize: Number(pageSize),
pageCount: 60,
pageCount: count,
itemCount: count * Number(pageSize),
list,
});
},

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "naive-ui-admin",
"version": "1.5",
"version": "1.9.1",
"author": {
"name": "Ahjung",
"email": "735878602@qq.com",
@@ -8,13 +8,16 @@
},
"private": true,
"scripts": {
"bootstrap": "yarn install",
"serve": "npm run dev",
"bootstrap": "pnpm install",
"serve": "pnpm run dev",
"dev": "vite",
"build": "vite build && esno ./build/script/postBuild.ts",
"build:no-cache": "yarn clean:cache && npm run build",
"build:no-cache": "pnpm clean:cache && 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",
"deploy": "gh-pages -d dist",
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
@@ -25,74 +28,76 @@
"test prod gzip": "http-server dist --cors --gzip -c-1"
},
"dependencies": {
"@vicons/antd": "^0.10.0",
"@vicons/ionicons5": "^0.10.0",
"@vueuse/core": "^5.0.3",
"axios": "^0.21.1",
"blueimp-md5": "^2.18.0",
"dayjs": "^1.10.5",
"echarts": "^5.1.2",
"element-resize-detector": "^1.2.3",
"lodash": "^4.17.21",
"@vicons/antd": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^9.13.0",
"axios": "^1.6.8",
"blueimp-md5": "^2.19.0",
"date-fns": "^2.30.0",
"echarts": "^5.5.0",
"element-resize-detector": "^1.2.4",
"lodash-es": "^4.17.21",
"makeit-captcha": "^1.2.5",
"mitt": "^2.1.0",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"naive-ui": "^2.15.11",
"pinia": "^2.0.0-beta.3",
"qs": "^6.10.1",
"vfonts": "^0.1.0",
"vue": "^3.1.2",
"vue-router": "^4.0.10",
"vue-types": "^4.0.0",
"vuedraggable": "^4.0.3",
"vuex": "^4.0.2"
"naive-ui": "^2.38.1",
"pinia": "^2.1.7",
"qs": "^6.12.0",
"vfonts": "^0.0.3",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vue-types": "^4.2.1"
},
"devDependencies": {
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"@types/lodash": "^4.14.170",
"@types/node": "^15.12.2",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"@vitejs/plugin-vue": "^1.2.3",
"@vitejs/plugin-vue-jsx": "^1.1.5",
"@vue/compiler-sfc": "3.1.1",
"@vue/eslint-config-typescript": "^7.0.0",
"autoprefixer": "^10.3.1",
"commitizen": "^4.2.4",
"core-js": "^3.14.0",
"dotenv": "^10.0.0",
"eslint": "^7.28.0",
"eslint-config-prettier": "^8.3.0",
"eslint-define-config": "^1.0.9",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.11.1",
"esno": "^0.7.3",
"gh-pages": "^3.2.0",
"husky": "^6.0.0",
"jest": "^27.0.6",
"less": "^4.1.1",
"less-loader": "^9.0.0",
"lint-staged": "^11.0.0",
"postcss": "^8.3.5",
"prettier": "^2.3.1",
"pretty-quick": "^3.1.0",
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@types/lodash": "^4.17.0",
"@types/node": "^18.19.31",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.1",
"@vue/compiler-sfc": "^3.4.21",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.19",
"commitizen": "^4.3.0",
"core-js": "^3.36.1",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"eslint-define-config": "1.12.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.24.1",
"esno": "^0.16.3",
"gh-pages": "^4.0.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"less": "^4.2.0",
"less-loader": "^11.1.4",
"lint-staged": "^13.3.0",
"postcss": "^8.4.38",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"rimraf": "^3.0.2",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0",
"stylelint-scss": "^3.19.0",
"tailwindcss": "^2.2.7",
"typescript": "^4.3.5",
"vite": "2.3.6",
"vite-plugin-compression": "^0.3.1",
"vite-plugin-html": "^2.0.7",
"vite-plugin-mock": "^2.9.3",
"vite-plugin-style-import": "^1.0.1",
"vue-eslint-parser": "^7.8.0"
"stylelint": "^14.16.1",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^5.0.0",
"stylelint-scss": "^4.7.0",
"tailwindcss": "^3.4.3",
"typescript": "^4.9.5",
"unplugin-vue-components": "^0.22.12",
"vite": "^3.2.10",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-mock": "^2.9.8",
"vite-plugin-style-import": "^2.0.0",
"vue-demi": "^0.13.11",
"vue-draggable-next": "^2.2.1",
"vue-eslint-parser": "^9.4.2",
"vuedraggable": "^4.1.0"
},
"lint-staged": {
"*.{vue,js,ts,tsx}": "eslint --fix"
@@ -122,6 +127,6 @@
},
"homepage": "https://github.com/jekip/naive-ui-admin",
"engines": {
"node": "^12 || >=14"
"node": ">=16"
}
}

8215
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: {},
autoprefixer: {},
},
}
};

View File

@@ -15,6 +15,6 @@ module.exports = {
requirePragma: false,
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'lf',
endOfLine: 'auto',
rangeStart: 0,
};

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,86 +16,68 @@
</transition>
</template>
<script lang="ts">
import { defineComponent, computed, onMounted, onUnmounted } from 'vue';
import { zhCN, dateZhCN, createTheme, inputDark, datePickerDark, darkTheme } from 'naive-ui';
<script lang="ts" setup>
import { computed, onMounted, onUnmounted } from 'vue';
import { zhCN, dateZhCN, darkTheme } from 'naive-ui';
import { LockScreen } from '@/components/Lockscreen';
import { AppProvider } from '@/components/Application';
import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useScreenLockStore } from '@/store/modules/screenLock.js';
import { useRoute } from 'vue-router';
import { useDesignSettingStore } from '@/store/modules/designSetting';
import { lighten } from '@/utils/index';
export default defineComponent({
name: 'App',
components: { LockScreen, AppProvider },
setup() {
const route = useRoute();
const useLockscreen = useLockscreenStore();
const designStore = useDesignSettingStore();
const isLock = computed(() => useLockscreen.isLock);
const lockTime = computed(() => useLockscreen.lockTime);
const route = useRoute();
const useScreenLock = useScreenLockStore();
const designStore = useDesignSettingStore();
const isLock = computed(() => useScreenLock.isLocked);
const lockTime = computed(() => useScreenLock.lockTime);
const getThemeOverrides = computed(() => {
return {
common: {
primaryColor: designStore.appTheme,
primaryColorHover: '#57a3f3',
},
};
});
/**
* @type import('naive-ui').GlobalThemeOverrides
*/
const getThemeOverrides = computed(() => {
const appTheme = designStore.appTheme;
const lightenStr = lighten(designStore.appTheme, 6);
return {
common: {
primaryColor: appTheme,
primaryColorHover: lightenStr,
primaryColorPressed: lightenStr,
primaryColorSuppl: appTheme,
},
LoadingBar: {
colorLoading: appTheme,
},
};
});
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined));
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined));
let timer;
let timer: NodeJS.Timer;
const timekeeping = () => {
clearInterval(timer);
if (route.name == 'login' || isLock.value) return;
// 设置不锁屏
useLockscreen.setLock(false);
// 重置锁屏时间
useLockscreen.setLockTime();
timer = setInterval(() => {
// 锁屏倒计时递减
useLockscreen.setLockTime(lockTime.value - 1);
if (lockTime.value <= 0) {
// 设置锁屏
useLockscreen.setLock(true);
return clearInterval(timer);
}
}, 1000);
};
const timekeeping = () => {
clearInterval(timer);
if (route.name == 'login' || isLock.value) return;
// 设置不锁屏
useScreenLock.setLock(false);
// 重置锁屏时间
useScreenLock.setLockTime();
timer = setInterval(() => {
// 锁屏倒计时递减
useScreenLock.setLockTime(lockTime.value - 1);
if (lockTime.value <= 0) {
// 设置锁屏
useScreenLock.setLock(true);
return clearInterval(timer);
}
}, 1000);
};
onMounted(() => {
document.addEventListener('mousedown', timekeeping);
});
onMounted(() => {
document.addEventListener('mousedown', timekeeping);
});
onUnmounted(() => {
document.removeEventListener('mousedown', timekeeping);
});
return {
darkTheme: createTheme([inputDark, datePickerDark]),
getDarkTheme,
zhCN,
dateZhCN,
isLock,
getThemeOverrides,
};
},
onUnmounted(() => {
document.removeEventListener('mousedown', timekeeping);
});
</script>
<style lang="less">
@import 'styles/common.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>

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="52px" height="45px" viewBox="0 0 52 45" enable-background="new 0 0 52 45" xml:space="preserve"> <image id="image0" width="52" height="45" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAAtCAMAAADWf7iKAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAAdVBMVEX///8AAABkbnc9RVY7
QE9fY3GIiJZjbHk9QFOCi5QAAAA9QlZfZHMAAAA4QFEAAADt7/Lf5OTt7/KChoYAAADu8PTf3+Nw
c3MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyNUkyOEn////w8vWhURXFAAAAI3RS
TlMAAE/2/uZJUP45AvfkBP4F9LrwPwP0vUkOAREsNjk0JwYHCLrjEiIAAAABYktHRACIBR1IAAAA
CXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5QgGAhE5kB5L+gAAAIZJREFUSMft1rsSgjAQhWEW
FTUYIopKlPui7/+IZqFhbMxmhm7//qvPiaKgAOIN+rbdJQCE9qO3cR2OhFTKMYgn5ZDmGcy0Q4aJ
0AhaoPdPnz8JEiRIkKCV0DkE5YQ0D12uBQ01B93uj9LSJdDPV1V71rRlMf0Iq7p+8Kw3aj4fAJYR
TCigL0lMJ5P4y7LRAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTA4LTA2VDAyOjE3OjU2KzAwOjAw
Kbo8/wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wOC0wNlQwMjoxNzo1NiswMDowMFjnhEMAAAAA
SUVORK5CYII=" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,29 +1,16 @@
<template>
<n-loading-bar-provider>
<LoadingContent />
<n-dialog-provider>
<DialogContent />
<n-notification-provider>
<n-message-provider>
<MessageContent />
<slot slot="default"></slot>
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
<n-dialog-provider>
<n-notification-provider>
<n-message-provider>
<slot name="default"></slot>
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
} from 'naive-ui';
import { LoadingContent } from '@/components/LoadingContent';
import { MessageContent } from '@/components/MessageContent';
import { DialogContent } from '@/components/DialogContent';
import { NDialogProvider, NNotificationProvider, NMessageProvider } from 'naive-ui';
export default defineComponent({
name: 'Application',
@@ -31,10 +18,6 @@
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
LoadingContent,
MessageContent,
DialogContent,
},
setup() {
return {};

View File

@@ -1,3 +0,0 @@
import DialogContent from './index.vue';
export { DialogContent };

View File

@@ -1,12 +0,0 @@
<template></template>
<script lang="ts">
import { useDialog } from 'naive-ui';
export default {
name: 'DialogContent',
setup() {
//挂载在 window 方便与在js中使用
window['$dialog'] = useDialog();
},
};
</script>

View File

@@ -0,0 +1,4 @@
export { default as BasicForm } from './src/BasicForm.vue';
export { useForm } from './src/hooks/useForm';
export * from './src/types/form';
export * from './src/types/index';

View File

@@ -0,0 +1,319 @@
<template>
<n-form v-bind="getBindValue" :model="formModel" ref="formElRef">
<n-grid v-bind="getGrid">
<n-gi v-bind="schema.giProps" v-for="schema in getSchema" :key="schema.field">
<n-form-item :label="schema.label" :path="schema.field">
<!--标签名右侧温馨提示-->
<template #label v-if="schema.labelMessage">
{{ schema.label }}
<n-tooltip trigger="hover" :style="schema.labelMessageStyle">
<template #trigger>
<n-icon size="18" class="text-gray-400 cursor-pointer">
<QuestionCircleOutlined />
</n-icon>
</template>
{{ schema.labelMessage }}
</n-tooltip>
</template>
<!--判断插槽-->
<template v-if="schema.slot">
<slot
:name="schema.slot"
:model="formModel"
:field="schema.field"
:value="formModel[schema.field]"
></slot>
</template>
<!--NCheckbox-->
<template v-else-if="schema.component === 'NCheckbox'">
<n-checkbox-group v-model:value="formModel[schema.field]">
<n-space>
<n-checkbox
v-for="item in schema.componentProps.options"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</n-space>
</n-checkbox-group>
</template>
<!--NRadioGroup-->
<template v-else-if="schema.component === 'NRadioGroup'">
<n-radio-group v-model:value="formModel[schema.field]">
<n-space>
<n-radio
v-for="item in schema.componentProps.options"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</n-radio>
</n-space>
</n-radio-group>
</template>
<!--动态渲染表单组件-->
<component
v-else
v-bind="getComponentProps(schema)"
:is="schema.component"
v-model:value="formModel[schema.field]"
:class="{ isFull: schema.isFull != false && getProps.isFull }"
/>
<!--组件后面的内容-->
<template v-if="schema.suffix">
<slot
:name="schema.suffix"
:model="formModel"
:field="schema.field"
:value="formModel[schema.field]"
></slot>
</template>
</n-form-item>
</n-gi>
<!--提交 重置 展开 收起 按钮-->
<n-gi
:span="isInline ? '' : 24"
:suffix="isInline ? true : false"
#="{ overflow }"
v-if="getProps.showActionButtonGroup"
>
<n-space
align="center"
:justify="isInline ? 'end' : 'start'"
:style="{ 'margin-left': `${isInline ? 12 : getProps.labelWidth}px` }"
>
<n-button
v-if="getProps.showSubmitButton"
v-bind="getSubmitBtnOptions"
@click="handleSubmit"
:loading="loadingSub"
attr-type="submit"
>{{ getProps.submitButtonText }}</n-button
>
<n-button
v-if="getProps.showResetButton"
v-bind="getResetBtnOptions"
@click="resetFields"
>{{ getProps.resetButtonText }}</n-button
>
<n-button
type="primary"
text
icon-placement="right"
v-if="isInline && getProps.showAdvancedButton"
@click="unfoldToggle"
>
<template #icon>
<n-icon size="14" class="unfold-icon" v-if="overflow">
<DownOutlined />
</n-icon>
<n-icon size="14" class="unfold-icon" v-else>
<UpOutlined />
</n-icon>
</template>
{{ overflow ? '展开' : '收起' }}
</n-button>
</n-space>
</n-gi>
</n-grid>
</n-form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, computed, unref, onMounted, watch } from 'vue';
import { createPlaceholderMessage } from './helper';
import { useFormEvents } from './hooks/useFormEvents';
import { useFormValues } from './hooks/useFormValues';
import { basicProps } from './props';
import { DownOutlined, UpOutlined, QuestionCircleOutlined } from '@vicons/antd';
import type { Ref } from 'vue';
import type { GridProps } from 'naive-ui/lib/grid';
import type { FormSchema, FormProps, FormActionType } from './types/form';
import { isArray } from '@/utils/is/index';
import { deepMerge } from '@/utils';
export default defineComponent({
name: 'BasicForm',
components: { DownOutlined, UpOutlined, QuestionCircleOutlined },
props: {
...basicProps,
},
emits: ['reset', 'submit', 'register'],
setup(props, { emit, attrs }) {
const defaultFormModel = ref<Recordable>({});
const formModel = reactive<Recordable>({});
const propsRef = ref<Partial<FormProps>>({});
const schemaRef = ref<Nullable<FormSchema[]>>(null);
const formElRef = ref<Nullable<FormActionType>>(null);
const gridCollapsed = ref(true);
const loadingSub = ref(false);
const isUpdateDefaultRef = ref(false);
const getSubmitBtnOptions = computed(() => {
return Object.assign(
{
size: props.size,
type: 'primary',
},
props.submitButtonOptions
);
});
const getResetBtnOptions = computed(() => {
return Object.assign(
{
size: props.size,
type: 'default',
},
props.resetButtonOptions
);
});
function getComponentProps(schema) {
const compProps = schema.componentProps ?? {};
const component = schema.component;
return {
clearable: true,
placeholder: createPlaceholderMessage(unref(component)),
...compProps,
};
}
const getProps = computed((): FormProps => {
const formProps = { ...props, ...unref(propsRef) } as FormProps;
const rulesObj: any = {
rules: {},
};
const schemas: any = formProps.schemas || [];
schemas.forEach((item) => {
if (item.rules && isArray(item.rules)) {
rulesObj.rules[item.field] = item.rules;
}
});
return { ...formProps, ...unref(rulesObj) };
});
const isInline = computed(() => {
const { layout } = unref(getProps);
return layout === 'inline';
});
const getGrid = computed((): GridProps => {
const { gridProps } = unref(getProps);
return {
...gridProps,
collapsed: isInline.value ? gridCollapsed.value : false,
responsive: 'screen',
};
});
const getBindValue = computed(
() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable)
);
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
for (const schema of schemas) {
const { defaultValue } = schema;
// handle date type
// dateItemType.includes(component as string)
if (defaultValue) {
schema.defaultValue = defaultValue;
}
}
return schemas as FormSchema[];
});
const { handleFormValues, initDefault } = useFormValues({
defaultFormModel,
getSchema,
formModel,
});
const { handleSubmit, validate, resetFields, getFieldsValue, clearValidate, setFieldsValue } =
useFormEvents({
emit,
getProps,
formModel,
getSchema,
formElRef: formElRef as Ref<FormActionType>,
defaultFormModel,
loadingSub,
handleFormValues,
});
function unfoldToggle() {
gridCollapsed.value = !gridCollapsed.value;
}
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
resetFields,
validate,
clearValidate,
setProps,
submit: handleSubmit,
};
watch(
() => getSchema.value,
(schema) => {
if (unref(isUpdateDefaultRef)) {
return;
}
if (schema?.length) {
initDefault();
isUpdateDefaultRef.value = true;
}
}
);
onMounted(() => {
initDefault();
emit('register', formActionType);
});
return {
formElRef,
formModel,
getGrid,
getProps,
getBindValue,
getSchema,
getSubmitBtnOptions,
getResetBtnOptions,
handleSubmit,
resetFields,
loadingSub,
isInline,
getComponentProps,
unfoldToggle,
};
},
});
</script>
<style lang="less" scoped>
.isFull {
width: 100%;
justify-content: flex-start;
}
.unfold-icon {
display: flex;
align-items: center;
height: 100%;
margin-left: -3px;
}
</style>

View File

@@ -0,0 +1,42 @@
import { ComponentType } from './types/index';
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component === 'NInput') return '请输入';
if (
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
component
)
)
return '请选择';
return '';
}
const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'];
function genType() {
return [...DATE_TYPE, 'RangePicker'];
}
/**
* 时间字段
*/
export const dateItemType = genType();
export function defaultType(component) {
if (component === 'NInput') return '';
if (component === 'NInputNumber') return null;
return [
'NPicker',
'NSelect',
'NCheckbox',
'NRadio',
'NSwitch',
'NDatePicker',
'NTimePicker',
].includes(component)
? ''
: undefined;
}

View File

@@ -0,0 +1,95 @@
import type { FormProps, FormActionType, UseFormReturnType } from '../types/form';
import type { DynamicProps } from '/#/utils';
import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
import { isProdMode } from '@/utils/env';
import { getDynamicProps } from '@/utils';
type Props = Partial<DynamicProps<FormProps>>;
export function useForm(props?: Props): UseFormReturnType {
const formRef = ref<Nullable<FormActionType>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
async function getForm() {
const form = unref(formRef);
if (!form) {
console.error(
'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!'
);
}
await nextTick();
return form as FormActionType;
}
function register(instance: FormActionType) {
isProdMode() &&
onUnmounted(() => {
formRef.value = null;
loadedRef.value = null;
});
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
formRef.value = instance;
loadedRef.value = true;
watch(
() => props,
() => {
props && instance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
}
);
}
const methods: FormActionType = {
setProps: async (formProps: Partial<FormProps>) => {
const form = await getForm();
await form.setProps(formProps);
},
resetFields: async () => {
getForm().then(async (form) => {
await form.resetFields();
});
},
clearValidate: async (name?: string | string[]) => {
const form = await getForm();
await form.clearValidate(name);
},
getFieldsValue: <T>() => {
return unref(formRef)?.getFieldsValue() as T;
},
setFieldsValue: async <T>(values: T) => {
const form = await getForm();
await form.setFieldsValue<T>(values);
},
submit: async (): Promise<any> => {
const form = await getForm();
return form.submit();
},
validate: async (nameList?: any[]): Promise<Recordable> => {
const form = await getForm();
return form.validate(nameList);
},
setLoading: (value: boolean) => {
loadedRef.value = value;
},
setSchema: async (values) => {
const form = await getForm();
form.setSchema(values);
},
};
return [register, methods];
}

View File

@@ -0,0 +1,11 @@
import { provide, inject } from 'vue';
const key = Symbol('formElRef');
export function createFormContext(instance) {
provide(key, instance);
}
export function useFormContext() {
return inject(key);
}

View File

@@ -0,0 +1,116 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema, FormActionType } from '../types/form';
import { unref, toRaw } from 'vue';
import { isFunction } from '@/utils/is';
declare type EmitType = (event: string, ...args: any[]) => void;
interface UseFormActionContext {
emit: EmitType;
getProps: ComputedRef<FormProps>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
formElRef: Ref<FormActionType>;
defaultFormModel: Recordable;
loadingSub: Ref<boolean>;
handleFormValues: Function;
}
export function useFormEvents({
emit,
getProps,
formModel,
getSchema,
formElRef,
defaultFormModel,
loadingSub,
handleFormValues,
}: UseFormActionContext) {
// 验证
async function validate() {
return unref(formElRef)?.validate();
}
// 提交
async function handleSubmit(e?: Event): Promise<object | boolean> {
e && e.preventDefault();
loadingSub.value = true;
const { submitFunc } = unref(getProps);
if (submitFunc && isFunction(submitFunc)) {
await submitFunc();
loadingSub.value = false;
return false;
}
const formEl = unref(formElRef);
if (!formEl) return false;
try {
await validate();
const values = getFieldsValue();
loadingSub.value = false;
emit('submit', values);
return values;
} catch (error: any) {
emit('submit', false);
loadingSub.value = false;
console.error(error);
return false;
}
}
//清空校验
async function clearValidate() {
// @ts-ignore
await unref(formElRef)?.restoreValidation();
}
//重置
async function resetFields(): Promise<void> {
const { resetFunc, submitOnReset } = unref(getProps);
resetFunc && isFunction(resetFunc) && (await resetFunc());
const formEl = unref(formElRef);
if (!formEl) return;
Object.keys(formModel).forEach((key) => {
formModel[key] = unref(defaultFormModel)[key] || null;
});
await clearValidate();
const fromValues = handleFormValues(toRaw(unref(formModel)));
emit('reset', fromValues);
submitOnReset && (await handleSubmit());
}
//获取表单值
function getFieldsValue(): Recordable {
const formEl = unref(formElRef);
if (!formEl) return {};
return handleFormValues(toRaw(unref(formModel)));
}
//设置表单字段值
async function setFieldsValue(values: Recordable): Promise<void> {
const fields = unref(getSchema)
.map((item) => item.field)
.filter(Boolean);
Object.keys(values).forEach((key) => {
const value = values[key];
if (fields.includes(key)) {
formModel[key] = value;
}
});
}
function setLoading(value: boolean): void {
loadingSub.value = value;
}
return {
handleSubmit,
validate,
resetFields,
getFieldsValue,
clearValidate,
setFieldsValue,
setLoading,
};
}

View File

@@ -0,0 +1,54 @@
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '@/utils/is';
import { unref } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import type { FormSchema } from '../types/form';
import { set } from 'lodash-es';
interface UseFormValuesContext {
defaultFormModel: Ref<any>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
}
export function useFormValues({ defaultFormModel, getSchema, formModel }: UseFormValuesContext) {
// 加工 form values
function handleFormValues(values: Recordable) {
if (!isObject(values)) {
return {};
}
const res: Recordable = {};
for (const item of Object.entries(values)) {
let [, value] = item;
const [key] = item;
if (
!key ||
(isArray(value) && value.length === 0) ||
isFunction(value) ||
isNullOrUnDef(value)
) {
continue;
}
// 删除空格
if (isString(value)) {
value = value.trim();
}
set(res, key, value);
}
return res;
}
//初始化默认值
function initDefault() {
const schemas = unref(getSchema);
const obj: Recordable = {};
schemas.forEach((item) => {
const { defaultValue } = item;
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue;
formModel[item.field] = defaultValue;
}
});
defaultFormModel.value = obj;
}
return { handleFormValues, initDefault };
}

View File

@@ -0,0 +1,82 @@
import type { CSSProperties, PropType } from 'vue';
import { FormSchema } from './types/form';
import type { GridProps, GridItemProps } from 'naive-ui/lib/grid';
import type { ButtonProps } from 'naive-ui/lib/button';
import { propTypes } from '@/utils/propTypes';
export const basicProps = {
// 标签宽度 固定宽度
labelWidth: {
type: [Number, String] as PropType<number | string>,
default: 80,
},
// 表单配置规则
schemas: {
type: [Array] as PropType<FormSchema[]>,
default: () => [],
},
//布局方式
layout: {
type: String,
default: 'inline',
},
//是否展示为行内表单
inline: {
type: Boolean,
default: false,
},
//大小
size: {
type: String,
default: 'medium',
},
//标签位置
labelPlacement: {
type: String,
default: 'left',
},
//组件是否width 100%
isFull: {
type: Boolean,
default: true,
},
//是否显示操作按钮(查询/重置)
showActionButtonGroup: propTypes.bool.def(true),
// 显示重置按钮
showResetButton: propTypes.bool.def(true),
//重置按钮配置
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
// 显示确认按钮
showSubmitButton: propTypes.bool.def(true),
// 确认按钮配置
submitButtonOptions: Object as PropType<Partial<ButtonProps>>,
//展开收起按钮
showAdvancedButton: propTypes.bool.def(true),
// 确认按钮文字
submitButtonText: {
type: String,
default: '查询',
},
//重置按钮文字
resetButtonText: {
type: String,
default: '重置',
},
//grid 配置
gridProps: Object as PropType<GridProps>,
//gi配置
giProps: Object as PropType<GridItemProps>,
//grid 样式
baseGridStyle: {
type: Object as PropType<CSSProperties>,
},
//是否折叠
collapsed: {
type: Boolean,
default: false,
},
//默认展示的行数
collapsedRows: {
type: Number,
default: 1,
},
};

View File

@@ -0,0 +1,61 @@
import { ComponentType } from './index';
import type { CSSProperties } from 'vue';
import type { GridProps, GridItemProps } from 'naive-ui/lib/grid';
import type { ButtonProps } from 'naive-ui/lib/button';
export interface FormSchema {
field: string;
label: string;
labelMessage?: string;
labelMessageStyle?: object | string;
defaultValue?: any;
component?: ComponentType;
componentProps?: object;
slot?: string;
rules?: object | object[];
giProps?: GridItemProps;
isFull?: boolean;
suffix?: string;
}
export interface FormProps {
model?: Recordable;
labelWidth?: number | string;
schemas?: FormSchema[];
inline: boolean;
layout?: string;
size: string;
labelPlacement: string;
isFull: boolean;
showActionButtonGroup?: boolean;
showResetButton?: boolean;
resetButtonOptions?: Partial<ButtonProps>;
showSubmitButton?: boolean;
showAdvancedButton?: boolean;
submitButtonOptions?: Partial<ButtonProps>;
submitButtonText?: string;
resetButtonText?: string;
gridProps?: GridProps;
giProps?: GridItemProps;
resetFunc?: () => Promise<void>;
submitFunc?: () => Promise<void>;
submitOnReset?: boolean;
baseGridStyle?: CSSProperties;
collapsedRows?: number;
}
export interface FormActionType {
submit: () => Promise<any>;
setProps: (formProps: Partial<FormProps>) => Promise<void>;
setSchema: (schemaProps: Partial<FormSchema[]>) => Promise<void>;
setFieldsValue: (values: Recordable) => void;
clearValidate: (name?: string | string[]) => Promise<void>;
getFieldsValue: () => Recordable;
resetFields: () => Promise<void>;
validate: (nameList?: any[]) => Promise<any>;
setLoading: (status: boolean) => void;
}
export type RegisterFn = (formInstance: FormActionType) => void;
export type UseFormReturnType = [RegisterFn, FormActionType];

View File

@@ -0,0 +1,28 @@
export type ComponentType =
| 'NInput'
| 'NInputGroup'
| 'NInputPassword'
| 'NInputSearch'
| 'NInputTextArea'
| 'NInputNumber'
| 'NInputCountDown'
| 'NSelect'
| 'NTreeSelect'
| 'NRadioButtonGroup'
| 'NRadioGroup'
| 'NCheckbox'
| 'NCheckboxGroup'
| 'NAutoComplete'
| 'NCascader'
| 'NDatePicker'
| 'NMonthPicker'
| 'NRangePicker'
| 'NWeekPicker'
| 'NTimePicker'
| 'NSwitch'
| 'NStrengthMeter'
| 'NUpload'
| 'NIconPicker'
| 'NRender'
| 'NSlider'
| 'NRate';

View File

@@ -1,3 +0,0 @@
import LoadingContent from './index.vue';
export { LoadingContent };

View File

@@ -1,12 +0,0 @@
<template></template>
<script lang="ts">
import { useLoadingBar } from 'naive-ui';
export default {
name: 'LoadingContent',
setup() {
//挂载在 window 方便与在js中使用
window['$loading'] = useLoadingBar();
},
};
</script>

View File

@@ -21,6 +21,7 @@
:battery="battery"
:battery-status="batteryStatus"
:calc-discharging-time="calcDischargingTime"
:calc-charging-time="calcChargingTime"
/>
<div class="local-time">
@@ -48,6 +49,7 @@
type="password"
autofocus
v-model:value="loginParams.password"
@keyup.enter="onLogin"
placeholder="请输入登录密码"
>
<template #suffix>
@@ -58,11 +60,11 @@
</template>
</n-input>
<div class="w-full flex" v-if="isLoginError">
<div class="flex w-full" v-if="isLoginError">
<span class="text-red-500">{{ errorMsg }}</span>
</div>
<div class="w-full mt-1 flex justify-around">
<div class="flex justify-around w-full mt-1">
<div><a @click="showLogin = false">返回</a></div>
<div><a @click="goLogin">重新登录</a></div>
<div><a @click="onLogin">进入系统</a></div>
@@ -89,11 +91,11 @@
import { useOnline } from '@/hooks/useOnline';
import { useTime } from '@/hooks/useTime';
import { useBattery } from '@/hooks/useBattery';
import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useUserStore } from '@/store/modules/user';
import { useScreenLockStore } from '@/store/modules/screenLock';
import { UserInfoType, useUserStore } from '@/store/modules/user';
export default defineComponent({
name: 'Lockscreen',
name: 'ScreenLock',
components: {
LockOutlined,
LoadingOutlined,
@@ -104,7 +106,7 @@
recharge,
},
setup() {
const useLockscreen = useLockscreenStore();
const useScreenLock = useScreenLockStore();
const userStore = useUserStore();
// 获取时间
@@ -114,8 +116,8 @@
const router = useRouter();
const route = useRoute();
const { battery, batteryStatus, calcDischargingTime } = useBattery();
const userInfo: object = userStore.getUserInfo || {};
const { battery, batteryStatus, calcDischargingTime, calcChargingTime } = useBattery();
const userInfo: UserInfoType = userStore.getUserInfo || {};
const username = userInfo['username'] || '';
const state = reactive({
showLogin: false,
@@ -144,7 +146,7 @@
const { code, message } = await userStore.login(params);
if (code === ResultEnum.SUCCESS) {
onLockLogin(false);
useLockscreen.setLock(false);
useScreenLock.setLock(false);
} else {
state.errorMsg = message;
state.isLoginError = true;
@@ -155,7 +157,7 @@
//重新登录
const goLogin = () => {
onLockLogin(false);
useLockscreen.setLock(false);
useScreenLock.setLock(false);
router.replace({
path: '/login',
query: {
@@ -176,6 +178,7 @@
battery,
batteryStatus,
calcDischargingTime,
calcChargingTime,
onLockLogin,
onLogin,
goLogin,

View File

@@ -13,7 +13,7 @@
剩余可使用时间{{ calcDischargingTime }}
</div>
<span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0">
距离电池充满需要{{ calcDischargingTime }}
距离电池充满需要{{ calcChargingTime }}
</span>
</div>
</div>
@@ -36,6 +36,10 @@
type: String,
default: '',
},
calcChargingTime: {
type: String,
default: '',
},
batteryStatus: {
// 电池状态
type: String,
@@ -51,12 +55,12 @@
bottom: 20vh;
left: 50vw;
width: 300px;
height: 400px;
height: 500px;
transform: translateX(-50%);
.number {
position: absolute;
top: 27%;
top: 20%;
z-index: 10;
width: 300px;
font-size: 32px;
@@ -131,22 +135,6 @@
}
}
@width: ~`Math.round(Math.random() * 100) ` px;
@left: calc(15px + `Math.round(Math.random(70)) `);
each(range(15), {
.xiaoma-@{value} {
height: (@value * 50px);
}
li:nth-child(@{index}) {
top: 50%;
left: @left;
width: @width;
height: @width;
transform: translate(-50%, -50%);
/*animation: moveToTop (Math.random(6) + 3s) ease-in-out -(Math.random(5000) / 1000s) infinite;*/
}
});
@keyframes rotate {
50% {
border-radius: 45% / 42% 38% 58% 49%;

View File

@@ -1,3 +0,0 @@
import MessageContent from './index.vue';
export { MessageContent };

View File

@@ -1,12 +0,0 @@
<template></template>
<script lang="ts">
import { useMessage } from 'naive-ui';
export default {
name: 'MessageContent',
setup() {
//挂载在 window 方便与在js中使用
window['$message'] = useMessage();
},
};
</script>

View File

@@ -0,0 +1,3 @@
export { default as basicModal } from './src/basicModal.vue';
export { useModal } from './src/hooks/useModal';
export * from './src/type';

View File

@@ -0,0 +1,107 @@
<template>
<n-modal id="basic-modal" v-bind="getBindValue" v-model:show="isModal" @close="onCloseModal">
<template #header>
<div class="w-full cursor-move" id="basic-modal-bar">{{ getBindValue.title }}</div>
</template>
<template #default>
<slot name="default"></slot>
</template>
<template #action v-if="!$slots.action">
<n-space>
<n-button @click="closeModal">取消</n-button>
<n-button type="primary" :loading="subLoading" @click="handleSubmit">{{
subBtuText
}}</n-button>
</n-space>
</template>
<template v-else #action>
<slot name="action"></slot>
</template>
</n-modal>
</template>
<script lang="ts" setup>
import { getCurrentInstance, ref, nextTick, unref, computed, useAttrs } from 'vue';
import { basicProps } from './props';
import startDrag from '@/utils/Drag';
import { deepMerge } from '@/utils';
import { FormProps } from '@/components/Form';
import { ModalProps, ModalMethods } from './type';
const attrs = useAttrs();
const props = defineProps({ ...basicProps });
const emit = defineEmits(['on-close', 'on-ok', 'register']);
const propsRef = ref<Partial<ModalProps> | null>(null);
const isModal = ref(false);
const subLoading = ref(false);
const getProps = computed((): FormProps => {
return { ...props, ...(unref(propsRef) as any) };
});
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;
emit('on-ok');
}
const modalMethods: ModalMethods = {
setProps,
openModal,
closeModal,
setSubLoading,
};
const instance = getCurrentInstance();
if (instance) {
emit('register', modalMethods);
}
</script>
<style lang="less">
.cursor-move {
cursor: move;
}
</style>

View File

@@ -0,0 +1,54 @@
import { ref, unref, getCurrentInstance, watch } from 'vue';
import { isProdMode } from '@/utils/env';
import { ModalMethods, UseModalReturnType } from '../type';
import { getDynamicProps } from '@/utils';
import { tryOnUnmounted } from '@vueuse/core';
export function useModal(props): UseModalReturnType {
const modalRef = ref<Nullable<ModalMethods>>(null);
const currentInstance = getCurrentInstance();
const getInstance = () => {
const instance = unref(modalRef.value);
if (!instance) {
console.error('useModal instance is undefined!');
}
return instance;
};
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 => {
getInstance()?.setProps(props);
},
openModal: () => {
getInstance()?.openModal();
},
closeModal: () => {
getInstance()?.closeModal();
},
setSubLoading: (status) => {
getInstance()?.setSubLoading(status);
},
};
return [register, methods];
}

View File

@@ -0,0 +1,30 @@
import { NModal } from 'naive-ui';
export const basicProps = {
...NModal.props,
// 确认按钮文字
subBtuText: {
type: String,
default: '确认',
},
showIcon: {
type: Boolean,
default: false,
},
width: {
type: Number,
default: 446,
},
title: {
type: String,
default: '',
},
maskClosable: {
type: Boolean,
default: false,
},
preset: {
type: String,
default: 'dialog',
},
};

View File

@@ -0,0 +1,19 @@
import type { DialogOptions } from 'naive-ui/lib/dialog';
/**
* @description: 弹窗对外暴露的方法
*/
export interface ModalMethods {
setProps: (props) => void;
openModal: () => void;
closeModal: () => 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 }}
<n-tooltip trigger="hover" v-if="titleTooltip">
<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 />
</n-icon>
</template>
@@ -22,6 +22,17 @@
<!--顶部右侧区域-->
<slot name="toolbar"></slot>
<!--斑马纹-->
<n-tooltip trigger="hover">
<template #trigger>
<div class="mr-2 table-toolbar-right-icon">
<n-switch v-model:value="isStriped" @update:value="setStriped" />
</div>
</template>
<span>表格斑马纹</span>
</n-tooltip>
<n-divider vertical />
<!--刷新-->
<n-tooltip trigger="hover">
<template #trigger>
@@ -61,6 +72,7 @@
<n-data-table
ref="tableElRef"
v-bind="getBindValues"
:striped="isStriped"
:pagination="pagination"
@update:page="updatePage"
@update:page-size="updatePageSize"
@@ -73,7 +85,6 @@
</template>
<script lang="ts">
import { NDataTable } from 'naive-ui';
import {
ref,
defineComponent,
@@ -129,7 +140,6 @@
QuestionCircleOutlined,
},
props: {
...NDataTable.props, // 这里继承原 UI 组件的 props
...basicProps,
},
emits: [
@@ -146,7 +156,7 @@
const tableElRef = ref<ComponentRef>(null);
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
let paginationEl: HTMLElement | null;
const isStriped = ref(false);
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
@@ -158,7 +168,7 @@
const { getPaginationInfo, setPagination } = usePagination(getProps);
const { getDataSourceRef, getRowKey, reload } = useDataSource(
const { getDataSourceRef, getDataSource, getRowKey, reload } = useDataSource(
getProps,
{
getPaginationInfo,
@@ -173,7 +183,7 @@
useColumns(getProps);
const state = reactive({
tableSize: 'medium',
tableSize: unref(getProps as any).size || 'medium',
isColumnSetting: false,
});
@@ -225,6 +235,8 @@
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const setStriped = (value: boolean) => (isStriped.value = value);
const tableAction = {
reload,
setColumns,
@@ -235,9 +247,6 @@
getCacheColumns,
setCacheColumnsField,
emit,
getSize: () => {
return unref(getBindValues).size;
},
};
const getCanResize = computed(() => {
@@ -255,7 +264,7 @@
const headerH = 64;
let paginationH = 2;
let marginH = 24;
if (!isBoolean(pagination)) {
if (!isBoolean(unref(pagination))) {
paginationEl = tableEl.querySelector('.n-data-table__pagination') as HTMLElement;
if (paginationEl) {
const offsetHeight = paginationEl.offsetHeight;
@@ -285,14 +294,16 @@
...toRefs(state),
tableElRef,
getBindValues,
getDataSource,
densityOptions,
reload,
densitySelect,
updatePage,
updatePageSize,
updateCheckedRowKeys,
pagination,
tableAction,
setStriped,
isStriped,
};
},
});

View File

@@ -2,7 +2,12 @@
<div class="tableAction">
<div class="flex items-center justify-center">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<n-button v-bind="action" class="mx-2">{{ action.label }}</n-button>
<n-button v-bind="action" class="mx-1">
{{ action.label }}
<template #icon v-if="action.hasOwnProperty('icon')">
<n-icon :component="action.icon" />
</template>
</n-button>
</template>
<n-dropdown
v-if="dropDownActions && getDropdownList.length"
@@ -11,7 +16,7 @@
@select="select"
>
<slot name="more"></slot>
<n-button v-bind="getMoreProps" class="mx-2" v-if="!$slots.more" icon-placement="right">
<n-button v-bind="getMoreProps" class="mx-1" v-if="!$slots.more" icon-placement="right">
<div class="flex items-center">
<span>更多</span>
<n-icon size="14" class="ml-1">
@@ -75,7 +80,7 @@
const getDropdownList = computed(() => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
return hasPermission(action.auth as string[]) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
@@ -108,7 +113,7 @@
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
return hasPermission(action.auth as string[]) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;

View File

@@ -7,23 +7,24 @@
</n-icon>
</div>
<div class="flex editable-cell-content" v-show="isEdit" v-click-outside="onClickOutside">
<CellComponent
v-bind="getComponentProps"
:component="getComponent"
:style="getWrapperStyle"
:popoverVisible="getRuleVisible"
:ruleMessage="ruleMessage"
:rule="getRule"
:class="getWrapperClass"
ref="elRef"
@options-change="handleOptionsChange"
@pressEnter="handleEnter"
/>
<div class="editable-cell-content-comp">
<CellComponent
v-bind="getComponentProps"
:component="getComponent"
:popoverVisible="getRuleVisible"
:ruleMessage="ruleMessage"
:rule="getRule"
:class="getWrapperClass"
ref="elRef"
@options-change="handleOptionsChange"
@press-enter="handleEnter"
/>
</div>
<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" />
</n-icon>
<n-icon class="cursor-pointer mx-2">
<n-icon class="mx-2 cursor-pointer">
<CloseOutlined @click="handleCancel" />
</n-icon>
</div>
@@ -31,7 +32,7 @@
</div>
</template>
<script lang="ts">
import type { CSSProperties, PropType } from 'vue';
import type { PropType } from 'vue';
import type { BasicColumn } from '../../types/table';
import type { EditRecordRow } from './index';
@@ -49,7 +50,7 @@
import { set, omit } from 'lodash-es';
import { EventEnum } from '@/components/Table/src/componentMap';
import dayjs from 'dayjs';
import { parseISO, format } from 'date-fns';
export default defineComponent({
name: 'EditableCell',
@@ -92,7 +93,7 @@
const getIsCheckComp = computed(() => {
const component = unref(getComponent);
return ['NCheckbox', 'NSwitch'].includes(component);
return ['NCheckbox', 'NRadio'].includes(component);
});
const getComponentProps = computed(() => {
@@ -103,13 +104,26 @@
const isCheckValue = unref(getIsCheckComp);
const valueField = isCheckValue ? 'checked' : 'value';
let valueField = isCheckValue ? 'checked' : 'value';
const val = unref(currentValueRef);
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
if (component === 'NDatePicker') {
value = dayjs(value).valueOf();
if (isString(value)) {
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;
@@ -143,15 +157,6 @@
return option?.label ?? value;
});
const getWrapperStyle = computed((): CSSProperties => {
// if (unref(getIsCheckComp) || unref(getRowEditable)) {
// return {};
// }
return {
width: 'calc(100% - 48px)',
};
});
const getWrapperClass = computed(() => {
const { align = 'center' } = props.column;
return `edit-cell-align-${align}`;
@@ -185,6 +190,7 @@
async function handleChange(e: any) {
const component = unref(getComponent);
const compProps = props.column?.editComponentProps ?? {};
if (!e) {
currentValueRef.value = e;
} else if (e?.target && Reflect.has(e.target, 'value')) {
@@ -195,12 +201,19 @@
currentValueRef.value = e;
}
//TODO 这里组件参数格式和dayjs格式不一致
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
if (component === 'NDatePicker') {
let format = (props.column.editComponentProps?.format)
.replace(/yyyy/g, 'YYYY')
.replace(/dd/g, 'DD');
currentValueRef.value = dayjs(currentValueRef.value).format(format);
if (isNumber(currentValueRef.value)) {
if (compProps.valueFormat) {
currentValueRef.value = format(currentValueRef.value, compProps.valueFormat);
}
} else if (isArray(currentValueRef.value)) {
if (compProps.valueFormat) {
currentValueRef.value = currentValueRef.value.map((item) => {
format(item, compProps.valueFormat);
});
}
}
}
const onChange = props.column?.editComponentProps?.onChange;
@@ -303,100 +316,103 @@
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
if (props.record) {
/* eslint-disable */
isArray(props.record[cbs])
? props.record[cbs]?.push(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;
isArray(props.record[cbs])
? props.record[cbs]?.push(handle)
: (props.record[cbs] = [handle]);
}
}
};
}
return {
isEdit,
handleEdit,
currentValueRef,
handleSubmit,
handleChange,
handleCancel,
elRef,
getComponent,
getRule,
onClickOutside,
ruleMessage,
getRuleVisible,
getComponentProps,
handleOptionsChange,
getWrapperStyle,
getWrapperClass,
getRowEditable,
getValues,
handleEnter,
// getSize,
};
},
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 {
isEdit,
handleEdit,
currentValueRef,
handleSubmit,
handleChange,
handleCancel,
elRef,
getComponent,
getRule,
onClickOutside,
ruleMessage,
getRuleVisible,
getComponentProps,
handleOptionsChange,
getWrapperClass,
getRowEditable,
getValues,
handleEnter,
// getSize,
};
},
});
</script>
<style lang="less">
.editable-cell {
&-content {
position: relative;
overflow-wrap: break-word;
word-break: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.editable-cell {
&-content {
position: relative;
overflow-wrap: break-word;
word-break: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.edit-icon {
font-size: 14px;
//position: absolute;
//top: 4px;
//right: 0;
display: none;
width: 20px;
cursor: pointer;
&-comp {
flex: 1;
}
.edit-icon {
font-size: 14px;
//position: absolute;
//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>

View File

@@ -25,13 +25,26 @@
</template>
<div class="table-toolbar-inner">
<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 }">
<div
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">
<DragOutlined />
</n-icon>
@@ -81,13 +94,14 @@
<script lang="ts">
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from 'vue';
import { useTableContext } from '../../hooks/useTableContext';
import { cloneDeep } from 'lodash-es';
import {
SettingOutlined,
DragOutlined,
VerticalRightOutlined,
VerticalLeftOutlined,
} from '@vicons/antd';
import Draggable from 'vuedraggable/src/vuedraggable';
import Draggable from 'vuedraggable';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
interface Options {
@@ -107,7 +121,7 @@
},
setup() {
const { getDarkTheme } = useDesignSetting();
const table = useTableContext();
const table: any = useTableContext();
const columnsList = ref<Options[]>([]);
const cacheColumnsList = ref<Options[]>([]);
@@ -135,8 +149,11 @@
const checkList: any = columns.map((item) => item.key);
state.checkList = checkList;
state.defaultCheckList = checkList;
columnsList.value = columns;
cacheColumnsList.value = columns;
const newColumns = columns.filter((item) => item.key != 'action' && item.title != '操作');
if (!columnsList.value.length) {
columnsList.value = cloneDeep(newColumns);
cacheColumnsList.value = cloneDeep(newColumns);
}
}
//切换
@@ -154,11 +171,11 @@
//获取
function getColumns() {
let newRet = [];
let newRet: any[] = [];
table.getColumns().forEach((item) => {
newRet.push({ ...item });
});
return newRet.filter((item) => item.key != 'action' && item.title != '操作');
return newRet;
}
//重置
@@ -207,6 +224,11 @@
}
}
function onMove(e) {
if (e.draggedContext.element.draggable === false) return false;
return true;
}
//固定
function fixedColumn(item, fixed) {
if (!state.checkList.includes(item.key)) return;
@@ -228,6 +250,7 @@
onChange,
onCheckAll,
onSelection,
onMove,
resetColumns,
fixedColumn,
draggableEnd,
@@ -271,6 +294,10 @@
display: inline-flex;
margin-right: 8px;
cursor: move;
&-hidden {
visibility: hidden;
cursor: default;
}
}
.fixed-item {

View File

@@ -48,11 +48,12 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columns = cloneDeep(pageColumns);
return columns
.filter((column) => {
// @ts-ignore
return hasPermission(column.auth) && isIfShow(column);
return hasPermission(column.auth as string[]) && isIfShow(column);
})
.map((column) => {
const { edit, editRow } = column;
//默认 ellipsis 为true
column.ellipsis = typeof column.ellipsis === 'undefined' ? { tooltip: true } : false;
const { edit } = column;
if (edit) {
column.render = renderEditCell(column);
if (edit) {
@@ -91,10 +92,10 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
const { actionColumn } = unref(propsRef);
if (!actionColumn) return;
// @ts-ignore
columns.push({
...actionColumn,
});
!columns.find((col) => col.key === 'action') &&
columns.push({
...(actionColumn as any),
});
}
//设置
@@ -127,7 +128,7 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
}
//获取
function getColumns() {
function getColumns(): BasicColumn[] {
const columns = toRaw(unref(getColumnsRef));
return columns.map((item) => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined };
@@ -140,12 +141,12 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
}
//更新原始数据单个字段
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) {
function setCacheColumnsField(key: string | undefined, value: Partial<BasicColumn>) {
if (!key || !value) {
return;
}
cacheColumns.forEach((item) => {
if (item.key === dataIndex) {
if (item.key === key) {
Object.assign(item, value);
return;
}

View File

@@ -1,7 +1,7 @@
import { ref, ComputedRef, unref, computed, onMounted, watchEffect, watch } from 'vue';
import type { BasicTableProps } from '../types/table';
import type { PaginationProps } from '../types/pagination';
import { isBoolean } from '@/utils/is';
import { isBoolean, isFunction, isArray } from '@/utils/is';
import { APISETTING } from '../const';
export function useDataSource(
@@ -9,7 +9,7 @@ export function useDataSource(
{ getPaginationInfo, setPagination, setLoading, tableData },
emit
) {
const dataSourceRef = ref([]);
const dataSourceRef = ref<Recordable[]>([]);
watchEffect(() => {
tableData.value = unref(dataSourceRef);
@@ -46,14 +46,14 @@ export function useDataSource(
async function fetch(opt?) {
try {
setLoading(true);
const { request, pagination }: any = unref(propsRef);
const { request, pagination, beforeRequest, afterRequest }: any = unref(propsRef);
if (!request) return;
//组装分页信息
const pageField = APISETTING.pageField;
const sizeField = APISETTING.sizeField;
const totalField = APISETTING.totalField;
const listField = APISETTING.listField;
const itemCount = APISETTING.countField;
let pageParams = {};
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
@@ -64,33 +64,45 @@ export function useDataSource(
pageParams[sizeField] = pageSize;
}
const params = {
let params = {
...pageParams,
...opt,
};
if (beforeRequest && isFunction(beforeRequest)) {
// The params parameter can be modified by outsiders
params = (await beforeRequest(params)) || params;
}
const res = await request(params);
const resultTotal = res[totalField] || 0;
const resultTotal = res[totalField];
const currentPage = res[pageField];
const total = res[itemCount];
const results = res[listField] ? res[listField] : [];
// 如果数据异常,需获取正确的页码再次执行
if (resultTotal) {
const currentTotalPage = Math.ceil(resultTotal / pageSize);
const currentTotalPage = Math.ceil(total / pageSize);
if (page > currentTotalPage) {
setPagination({
[pageField]: currentTotalPage,
page: currentTotalPage,
itemCount: total,
});
fetch(opt);
return await fetch(opt);
}
}
const resultInfo = res[listField] ? res[listField] : [];
let resultInfo = res[listField] ? res[listField] : [];
if (afterRequest && isFunction(afterRequest)) {
// can modify the data returned by the interface for processing
resultInfo = (await afterRequest(resultInfo)) || resultInfo;
}
dataSourceRef.value = resultInfo;
setPagination({
[pageField]: currentPage,
[totalField]: resultTotal,
page: currentPage,
pageCount: resultTotal,
itemCount: total,
});
if (opt && opt[pageField]) {
setPagination({
[pageField]: opt[pageField] || 1,
page: opt[pageField] || 1,
});
}
emit('fetch-success', {
@@ -101,9 +113,9 @@ export function useDataSource(
console.error(error);
emit('fetch-error', error);
dataSourceRef.value = [];
// setPagination({
// pageCount: 0,
// });
setPagination({
pageCount: 0,
});
} finally {
setLoading(false);
}

View File

@@ -1,6 +1,6 @@
import type { PaginationProps } from '../types/pagination';
import type { BasicTableProps } from '../types/table';
import { computed, unref, ref, ComputedRef } from 'vue';
import { computed, unref, ref, ComputedRef, watch } from 'vue';
import { isBoolean } from '@/utils/is';
import { DEFAULTPAGESIZE, PAGESIZES } from '../const';
@@ -9,16 +9,30 @@ export function usePagination(refProps: ComputedRef<BasicTableProps>) {
const configRef = ref<PaginationProps>({});
const show = ref(true);
watch(
() => unref(refProps).pagination,
(pagination) => {
if (!isBoolean(pagination) && pagination) {
configRef.value = {
...unref(configRef),
...(pagination ?? {}),
};
}
}
);
const getPaginationInfo = computed((): PaginationProps | boolean => {
const { pagination } = unref(refProps);
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false;
}
return {
pageSize: DEFAULTPAGESIZE,
pageSizes: PAGESIZES,
page: 1, //当前页
pageSize: DEFAULTPAGESIZE, //分页大小
pageSizes: PAGESIZES, // 每页条数
showSizePicker: true,
showQuickJumper: true,
prefix: (pagingInfo) => `${pagingInfo.itemCount}`, // 不需要可以通过 pagination 重置或者删除
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
};

View File

@@ -1,8 +1,9 @@
import type { PropType } from 'vue';
import { propTypes } from '@/utils/propTypes';
import { BasicColumn } from './types/table';
import { NDataTable } from 'naive-ui';
export const basicProps = {
...NDataTable.props, // 这里继承原 UI 组件的 props
title: {
type: String,
default: null,
@@ -15,7 +16,7 @@ export const basicProps = {
type: String,
default: 'medium',
},
tableData: {
dataSource: {
type: [Object],
default: () => [],
},
@@ -24,10 +25,17 @@ export const basicProps = {
default: () => [],
required: true,
},
beforeRequest: {
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
default: null,
},
request: {
type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null,
required: true,
},
afterRequest: {
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
default: null,
},
rowKey: {
type: [String, Function] as PropType<string | ((record) => string)>,

View File

@@ -5,4 +5,5 @@ export type ComponentType =
| 'NCheckbox'
| 'NSwitch'
| 'NDatePicker'
| 'NTimePicker';
| 'NTimePicker'
| 'NCascader';

View File

@@ -1,8 +1,10 @@
export interface PaginationProps {
page?: number;
pageCount?: number;
pageSize?: number;
pageSizes?: number[];
showSizePicker?: boolean;
showQuickJumper?: boolean;
page?: number; //受控模式下的当前页
itemCount?: number; //总条数
pageCount?: number; //总页数
pageSize?: number; //受控模式下的分页大小
pageSizes?: number[]; //每页条数, 可自定义
showSizePicker?: boolean; //是否显示每页条数的选择器
showQuickJumper?: boolean; //是否显示快速跳转
prefix?: any; //分页前缀
}

View File

@@ -1,6 +1,6 @@
import type { TableBaseColumn } from 'naive-ui/lib/data-table/src/interface';
import type { InternalRowData, TableBaseColumn } from 'naive-ui/lib/data-table/src/interface';
import { ComponentType } from './componentType';
export interface BasicColumn extends TableBaseColumn {
export interface BasicColumn<T = InternalRowData> extends TableBaseColumn<T> {
//编辑表格
edit?: boolean;
editRow?: boolean;
@@ -11,9 +11,11 @@ export interface BasicColumn extends TableBaseColumn {
editValueMap?: (value: any) => string;
onEditRow?: () => void;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
auth?: string[];
// 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean);
// 控制是否支持拖拽,默认支持
draggable?: boolean;
}
export interface TableActionType {
@@ -32,4 +34,5 @@ export interface BasicTableProps {
actionColumn: any[];
canResize: boolean;
resizeHeightOffset: number;
loading: boolean;
}

View File

@@ -1,17 +1,18 @@
// @ts-ignore
import { NButton } from 'naive-ui';
import { RoleEnum } from '@/enums/roleEnum';
// @ts-ignore
export interface ActionItem extends NButton.props {
import type { Component } from 'vue';
import { PermissionsEnum } from '@/enums/permissionsEnum';
export interface ActionItem extends Partial<InstanceType<typeof NButton>> {
onClick?: Fn;
label?: string;
color?: 'success' | 'error' | 'warning';
icon?: string;
type?: 'success' | 'error' | 'warning' | 'info' | 'primary' | 'default';
// 设定 color 后会覆盖 type 的样式
color?: string;
icon?: Component;
popConfirm?: PopConfirm;
disabled?: boolean;
divider?: boolean;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((action: ActionItem) => boolean);
}
@@ -22,5 +23,5 @@ export interface PopConfirm {
cancelText?: string;
confirm: Fn;
cancel?: Fn;
icon?: string;
icon?: Component;
}

View File

@@ -14,10 +14,10 @@
<img :src="item" />
</div>
<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 />
</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 />
</n-icon>
</div>
@@ -31,12 +31,13 @@
v-if="imgList.length < maxNumber"
>
<n-upload
class="w-auto"
v-bind="$props"
:file-list-style="{ display: 'none' }"
@before-upload="beforeUpload"
@finish="finish"
>
<div class="flex justify-center flex-col">
<div class="flex flex-col justify-center">
<n-icon size="18" class="m-auto">
<PlusOutlined />
</n-icon>
@@ -68,9 +69,8 @@
</template>
<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 { NUpload } from 'naive-ui';
import { basicProps } from './props';
import { useMessage, useDialog } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
@@ -85,7 +85,6 @@
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
props: {
...NUpload.props, // 这里继承原 UI 组件的 props
...basicProps,
},
emits: ['uploadChange', 'delete'],
@@ -103,16 +102,20 @@
const state = reactive({
showModal: false,
previewUrl: '',
originalImgList: [],
imgList: [],
originalImgList: [] as string[],
imgList: [] as string[],
});
//赋值默认图片显示
if (props.value.length) {
state.imgList = props.value.map((item) => {
return getImgUrl(item);
});
}
watch(
() => props.value,
() => {
state.imgList = props.value.map((item) => {
return getImgUrl(item);
});
},
{ immediate: true }
);
//预览
function preview(url: string) {
@@ -178,7 +181,7 @@
const result = res[infoField];
//成功
if (code === ResultEnum.SUCCESS) {
let imgUrl = getImgUrl(result.photo);
let imgUrl: string = getImgUrl(result.photo);
state.imgList.push(imgUrl);
state.originalImgList.push(result.photo);
emit('uploadChange', state.originalImgList);
@@ -222,6 +225,7 @@
&:hover {
background: 0 0;
.upload-card-item-info::before {
opacity: 1;
}
@@ -234,6 +238,7 @@
&-info {
position: relative;
height: 100%;
width: 100%;
padding: 0;
overflow: hidden;

View File

@@ -0,0 +1,9 @@
import logoImage from '@/assets/images/logo.png';
import loginImage from '@/assets/images/account-logo.png';
export const websiteConfig = Object.freeze({
title: 'NaiveUiAdmin',
logo: logoImage,
loginImage: loginImage,
loginDesc: 'Naive Ui Admin中后台前端/设计解决方案',
});

34
src/directives/copy.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* v-copy
* 复制某个值至剪贴板
* 接收参数string类型/Ref<string>类型/Reactive<string>类型
*/
import type { Directive, DirectiveBinding } from 'vue';
interface ElType extends HTMLElement {
copyData: string | number;
__handleClick__: any;
}
const copy: Directive = {
mounted(el: ElType, binding: DirectiveBinding) {
el.copyData = binding.value;
el.addEventListener('click', handleClick);
},
updated(el: ElType, binding: DirectiveBinding) {
el.copyData = binding.value;
},
beforeUnmount(el: ElType) {
el.removeEventListener('click', el.__handleClick__);
},
};
function handleClick(this: any) {
const input = document.createElement('input');
input.value = this.copyData.toLocaleString();
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
console.log('复制成功', this.copyData);
}
export default copy;

View File

@@ -0,0 +1,31 @@
/**
* v-debounce
* 按钮防抖指令可自行扩展至input
* 接收参数function类型
*/
import type { Directive, DirectiveBinding } from 'vue';
interface ElType extends HTMLElement {
__handleClick__: () => any;
}
const debounce: Directive = {
mounted(el: ElType, binding: DirectiveBinding) {
if (typeof binding.value !== 'function') {
throw 'callback must be a function';
}
let timer: NodeJS.Timeout | null = null;
el.__handleClick__ = function () {
if (timer) {
clearInterval(timer);
}
timer = setTimeout(() => {
binding.value();
}, 500);
};
el.addEventListener('click', el.__handleClick__);
},
beforeUnmount(el: ElType) {
el.removeEventListener('click', el.__handleClick__);
},
};
export default debounce;

View File

@@ -0,0 +1,49 @@
/*
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
思路:
1、设置需要拖拽的元素为absolute其父元素为relative。
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
4、鼠标松开(onmouseup)时完成一次拖拽
使用:在 Dom 上加上 v-draggable 即可
<div class="dialog-model" v-draggable></div>
*/
import type { Directive } from 'vue';
interface ElType extends HTMLElement {
parentNode: any;
}
const draggable: Directive = {
mounted: function (el: ElType) {
el.style.cursor = 'move';
el.style.position = 'absolute';
el.onmousedown = function (e) {
const disX = e.pageX - el.offsetLeft;
const disY = e.pageY - el.offsetTop;
document.onmousemove = function (e) {
let x = e.pageX - disX;
let y = e.pageY - disY;
const maxX = el.parentNode.offsetWidth - el.offsetWidth;
const maxY = el.parentNode.offsetHeight - el.offsetHeight;
if (x < 0) {
x = 0;
} else if (x > maxX) {
x = maxX;
}
if (y < 0) {
y = 0;
} else if (y > maxY) {
y = maxY;
}
el.style.left = x + 'px';
el.style.top = y + 'px';
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
};
},
};
export default draggable;

View File

@@ -0,0 +1,49 @@
/**
* v-longpress
* 长按指令,长按时触发事件
*/
import type { Directive, DirectiveBinding } from 'vue';
const directive: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
if (typeof binding.value !== 'function') {
throw 'callback must be a function';
}
// 定义变量
let pressTimer: any = null;
// 创建计时器( 2秒后执行函数
const start = (e: any) => {
if (e.button) {
if (e.type === 'click' && e.button !== 0) {
return;
}
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
handler(e);
}, 1000);
}
};
// 取消计时器
const cancel = () => {
if (pressTimer !== null) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
// 运行函数
const handler = (e: MouseEvent | TouchEvent) => {
binding.value(e);
};
// 添加事件监听器
el.addEventListener('mousedown', start);
el.addEventListener('touchstart', start);
// 取消计时器
el.addEventListener('click', cancel);
el.addEventListener('mouseout', cancel);
el.addEventListener('touchend', cancel);
el.addEventListener('touchcancel', cancel);
},
};
export default directive;

View File

@@ -0,0 +1,41 @@
/*
需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。
思路:
1、第一次点击立即调用方法并禁用按钮等延迟结束再次激活按钮
2、将需要触发的方法绑定在指令上
使用:给 Dom 加上 v-throttle 及回调函数即可
<button v-throttle="debounceClick">节流提交</button>
*/
import type { Directive, DirectiveBinding } from 'vue';
interface ElType extends HTMLElement {
__handleClick__: () => any;
disabled: boolean;
}
const throttle: Directive = {
mounted(el: ElType, binding: DirectiveBinding) {
if (typeof binding.value !== 'function') {
throw 'callback must be a function';
}
let timer: NodeJS.Timeout | null = null;
el.__handleClick__ = function () {
if (timer) {
clearTimeout(timer);
}
if (!el.disabled) {
el.disabled = true;
binding.value();
timer = setTimeout(() => {
el.disabled = false;
}, 1000);
}
};
el.addEventListener('click', el.__handleClick__);
},
beforeUnmount(el: ElType) {
el.removeEventListener('click', el.__handleClick__);
},
};
export default throttle;

View File

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

View File

@@ -1,3 +1,3 @@
import { useAsync } from './use-async';
import { useAsync } from './useAsync';
export { useAsync };

View File

@@ -4,30 +4,39 @@ import { useProjectSettingStore } from '@/store/modules/projectSetting';
export function useProjectSetting() {
const projectStore = useProjectSettingStore();
const getNavMode = computed(() => projectStore.navMode);
const navMode = computed(() => projectStore.navMode);
const getNavTheme = computed(() => projectStore.navTheme);
const navTheme = computed(() => projectStore.navTheme);
const getHeaderSetting = computed(() => projectStore.headerSetting);
const isMobile = computed(() => projectStore.isMobile);
const getMultiTabsSetting = computed(() => projectStore.multiTabsSetting);
const headerSetting = computed(() => projectStore.headerSetting);
const getMenuSetting = computed(() => projectStore.menuSetting);
const multiTabsSetting = computed(() => projectStore.multiTabsSetting);
const getCrumbsSetting = computed(() => projectStore.crumbsSetting);
const menuSetting = computed(() => projectStore.menuSetting);
const getPermissionMode = computed(() => projectStore.permissionMode);
const crumbsSetting = computed(() => projectStore.crumbsSetting);
const getShowFooter = computed(() => projectStore.showFooter);
const permissionMode = computed(() => projectStore.permissionMode);
const showFooter = computed(() => projectStore.showFooter);
const isPageAnimate = computed(() => projectStore.isPageAnimate);
const pageAnimateType = computed(() => projectStore.pageAnimateType);
return {
getNavMode,
getNavTheme,
getHeaderSetting,
getMultiTabsSetting,
getMenuSetting,
getCrumbsSetting,
getPermissionMode,
getShowFooter,
navMode,
navTheme,
isMobile,
headerSetting,
multiTabsSetting,
menuSetting,
crumbsSetting,
permissionMode,
showFooter,
isPageAnimate,
pageAnimateType,
};
}

View File

@@ -33,6 +33,14 @@ export const useBattery = () => {
return `${~~hour}小时${~~minute}分钟`;
});
// 计算电池充满剩余时间
const calcChargingTime = computed(() => {
console.log(state.battery);
const hour = state.battery.chargingTime / 3600;
const minute = (state.battery.chargingTime / 60) % 60;
return `${~~hour}小时${~~minute}分钟`;
});
// 电池状态
const batteryStatus = computed(() => {
if (state.battery.charging && state.battery.level >= 100) {
@@ -80,5 +88,6 @@ export const useBattery = () => {
...toRefs(state),
batteryStatus,
calcDischargingTime,
calcChargingTime,
};
};

View File

@@ -1,5 +1,5 @@
import { ref, onMounted, onUnmounted } from 'vue';
import { debounce } from 'lodash';
import { debounce } from 'lodash-es';
/**
* description: 获取页面宽度

View File

@@ -10,23 +10,27 @@ import { useBreakpoint } from '@/hooks/event/useBreakpoint';
import echarts from '@/utils/lib/echarts';
// import { useRootSetting } from '@/hooks/setting/useRootSetting';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
export function useECharts(
elRef: Ref<HTMLDivElement>,
theme: 'light' | 'dark' | 'default' = 'light'
theme: 'light' | 'dark' | 'default' = 'default'
) {
// const { getDarkMode } = useRootSetting();
const getDarkMode = 'light';
const { getDarkTheme: getSysDarkTheme } = useDesignSetting();
const getDarkTheme = computed(() => {
const sysTheme: string = getSysDarkTheme.value ? 'dark' : 'light';
return theme === 'default' ? sysTheme : theme;
});
let chartInstance: echarts.ECharts | null = null;
let resizeFn: Fn = resize;
const cacheOptions = ref<EChartsOption>({});
const cacheOptions = ref({});
let removeResizeFn: Fn = () => {};
resizeFn = useDebounceFn(resize, 200);
const getOptions = computed((): EChartsOption => {
if (getDarkMode !== 'dark') {
if (getDarkTheme.value !== 'dark') {
return cacheOptions.value;
}
return {
@@ -67,7 +71,7 @@ export function useECharts(
nextTick(() => {
useTimeoutFn(() => {
if (!chartInstance) {
initCharts(getDarkMode.value as 'default');
initCharts(getDarkTheme.value as 'default');
if (!chartInstance) return;
}
@@ -83,7 +87,7 @@ export function useECharts(
}
watch(
() => getDarkMode.value,
() => getDarkTheme.value,
(theme) => {
if (chartInstance) {
chartInstance.dispose();
@@ -93,18 +97,20 @@ export function useECharts(
}
);
tryOnUnmounted(() => {
tryOnUnmounted(disposeInstance);
function getInstance(): echarts.ECharts | null {
if (!chartInstance) {
initCharts(getDarkTheme.value as 'default');
}
return chartInstance;
}
function disposeInstance() {
if (!chartInstance) return;
removeResizeFn();
chartInstance.dispose();
chartInstance = null;
});
function getInstance(): echarts.ECharts | null {
if (!chartInstance) {
initCharts(getDarkMode.value as 'default');
}
return chartInstance;
}
return {
@@ -112,5 +118,6 @@ export function useECharts(
resize,
echarts,
getInstance,
disposeInstance,
};
}

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
*/
function _someRoles(accesses: string[]) {
return userStore.getRoles.some((item) => {
function _somePermissions(accesses: string[]) {
return userStore.getPermissions.some((item) => {
const { value }: any = item;
return accesses.includes(value);
});
@@ -20,7 +20,7 @@ export function usePermission() {
* */
function hasPermission(accesses: string[]): boolean {
if (!accesses || !accesses.length) return true;
return _someRoles(accesses);
return _somePermissions(accesses);
}
/**
@@ -28,9 +28,9 @@ export function usePermission() {
* @param accesses
*/
function hasEveryPermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles;
const permissionsList = userStore.getPermissions;
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 !`);
}
@@ -41,9 +41,9 @@ export function usePermission() {
* @param accessMap
*/
function hasSomePermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles;
const permissionsList = userStore.getPermissions;
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 !`);
}

View File

@@ -1,10 +1,10 @@
<template>
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement" :native-scrollbar="false">
<n-drawer-content :title="title">
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement">
<n-drawer-content :title="title" :native-scrollbar="false">
<div class="drawer">
<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">
<template #trigger>
<n-switch v-model:value="designStore.darkTheme" class="dark-theme-switch">
@@ -20,7 +20,7 @@
</template>
</n-switch>
</template>
<span>色主题</span>
<span>{{ designStore.darkTheme ? '深' : '浅' }}色主题</span>
</n-tooltip>
</div>
@@ -46,7 +46,11 @@
<div class="drawer-setting-item-style align-items-top">
<n-tooltip placement="top">
<template #trigger>
<img src="~@/assets/images/nav-theme-dark.svg" @click="togNavMode('vertical')" />
<img
src="~@/assets/images/nav-theme-dark.svg"
@click="togNavMode('vertical')"
alt="左侧菜单模式"
/>
</template>
<span>左侧菜单模式</span>
</n-tooltip>
@@ -56,12 +60,30 @@
<div class="drawer-setting-item-style">
<n-tooltip placement="top">
<template #trigger>
<img src="~@/assets/images/nav-horizontal.svg" @click="togNavMode('horizontal')" />
<img
src="~@/assets/images/nav-horizontal.svg"
alt="顶部菜单模式"
@click="togNavMode('horizontal')"
/>
</template>
<span>顶部菜单模式</span>
</n-tooltip>
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal'" />
</div>
<div class="drawer-setting-item-style">
<n-tooltip placement="top">
<template #trigger>
<img
src="~@/assets/images/nav-horizontal-mix.svg"
@click="togNavMode('horizontal-mix')"
alt="顶部菜单混合模式"
/>
</template>
<span>顶部菜单混合模式</span>
</n-tooltip>
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal-mix'" />
</div>
</div>
<n-divider title-placement="center">导航栏风格</n-divider>
@@ -70,7 +92,11 @@
<div class="drawer-setting-item-style align-items-top">
<n-tooltip placement="top">
<template #trigger>
<img src="~@/assets/images/nav-theme-dark.svg" @click="togNavTheme('dark')" />
<img
src="~@/assets/images/nav-theme-dark.svg"
alt="暗色侧边栏"
@click="togNavTheme('dark')"
/>
</template>
<span>暗色侧边栏</span>
</n-tooltip>
@@ -80,21 +106,24 @@
<div class="drawer-setting-item-style">
<n-tooltip placement="top">
<template #trigger>
<img src="~@/assets/images/nav-theme-light.svg" @click="togNavTheme('light')" />
<img
src="~@/assets/images/nav-theme-light.svg"
alt="白色侧边栏"
@click="togNavTheme('light')"
/>
</template>
<span>白色侧边栏</span>
</n-tooltip>
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'light'" />
</div>
</div>
<div class="drawer-setting-item align-items-top">
<div class="drawer-setting-item-style">
<n-tooltip placement="top">
<template #trigger>
<img
src="~@/assets/images/header-theme-dark.svg"
@click="togNavTheme('header-dark')"
alt="暗色顶栏"
/>
</template>
<span>暗色顶栏</span>
@@ -102,9 +131,18 @@
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'header-dark'" />
</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
:disabled="settingStore.navMode !== 'horizontal-mix'"
v-model:value="settingStore.menuSetting.mixMenu"
/>
</div>
</div>
<div class="drawer-setting-item">
<div class="drawer-setting-item-title"> 固定顶栏 </div>
<div class="drawer-setting-item-action">
@@ -165,6 +203,22 @@
<!-- </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">
<n-alert type="warning" :showIcon="false">
<p>{{ alertText }}</p>
@@ -176,12 +230,13 @@
</template>
<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 { useDesignSettingStore } from '@/store/modules/designSetting';
import { CheckOutlined } from '@vicons/antd';
import { Moon, SunnySharp } from '@vicons/ionicons5';
import { darkTheme } from 'naive-ui';
import { animates as animateOptions } from '@/settings/animateSetting';
export default defineComponent({
name: 'ProjectSetting',
@@ -204,8 +259,7 @@
title: props.title,
isDrawer: false,
placement: 'right',
alertText:
'该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。',
alertText: '该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置',
appThemeList: designStore.appThemeList,
});
@@ -216,6 +270,10 @@
}
);
const directionsOptions = computed(() => {
return animateOptions.find((item) => item.value == unref(settingStore.pageAnimateType));
});
function openDrawer() {
state.isDrawer = true;
}
@@ -226,7 +284,7 @@
function togNavTheme(theme) {
settingStore.navTheme = theme;
if (settingStore.navMode === 'horizontal' && theme === 'light') {
if (settingStore.navMode === 'horizontal' && ['light'].includes(theme)) {
settingStore.navTheme = 'dark';
}
}
@@ -237,11 +295,7 @@
function togNavMode(mode) {
settingStore.navMode = mode;
if (mode === 'horizontal') {
settingStore.setNavTheme('light');
} else {
settingStore.setNavTheme('dark');
}
settingStore.menuSetting.mixMenu = false;
}
return {
@@ -254,6 +308,8 @@
darkTheme,
openDrawer,
closeDrawer,
animateOptions,
directionsOptions,
};
},
});
@@ -288,6 +344,10 @@
flex: 0 0 auto;
}
&-select {
flex: 1;
}
.theme-item {
width: 20px;
min-width: 20px;
@@ -297,6 +357,7 @@
border-radius: 2px;
margin: 0 5px 5px 0;
text-align: center;
line-height: 14px;
.n-icon {
color: #fff;
@@ -312,6 +373,7 @@
.justify-center {
justify-content: center;
}
.dark-switch .n-switch {
::v-deep(.n-switch__rail) {
background-color: #000e1c;

View File

@@ -1,15 +1,27 @@
<template>
<div class="layout-header" :class="{ 'layout-header-light': !(navTheme == 'header-dark') }">
<div class="layout-header">
<!--顶部菜单-->
<div class="layout-header-left" v-if="navMode === 'horizontal'">
<AsideMenu v-model:collapsed="collapsed" mode="horizontal" class="n-menu-horizontal-light" />
<div
class="layout-header-left"
v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
>
<div class="logo" v-if="navMode === 'horizontal'">
<img :src="websiteConfig.logo" alt="" />
<h2 v-show="!collapsed" class="title">{{ websiteConfig.title }}</h2>
</div>
<AsideMenu
:collapsed="collapsed"
v-model:location="getMenuLocation"
:inverted="getInverted"
mode="horizontal"
/>
</div>
<!--左侧菜单-->
<div class="layout-header-left" v-else>
<!-- 菜单收起 -->
<div
class="ml-1 layout-header-trigger layout-header-trigger-min"
@click="() => $emit('update:collapsed', !collapsed)"
@click="handleMenuCollapsed"
>
<n-icon size="18" v-if="collapsed">
<MenuUnfoldOutlined />
@@ -30,8 +42,11 @@
</div>
<!-- 面包屑 -->
<n-breadcrumb v-if="crumbsSetting.show">
<template v-for="routeItem in breadcrumbList" :key="routeItem.name">
<n-breadcrumb-item>
<template
v-for="routeItem in breadcrumbList"
:key="routeItem.name === 'Redirect' ? void 0 : routeItem.name"
>
<n-breadcrumb-item v-if="routeItem.meta.title">
<n-dropdown
v-if="routeItem.children.length"
:options="routeItem.children"
@@ -60,7 +75,7 @@
<div
class="layout-header-trigger layout-header-trigger-min"
v-for="item in iconList"
:key="item.icon.name"
:key="item.icon"
>
<n-tooltip placement="bottom">
<template #trigger>
@@ -86,12 +101,14 @@
<div class="layout-header-trigger layout-header-trigger-min">
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
<div class="avatar">
<n-avatar round>
{{ username }}
<n-avatar round :src="websiteConfig.logo">
<template #icon>
<UserOutlined />
</template>
</n-avatar>
<n-divider vertical />
<span>{{ username }}</span>
</div>
</n-dropdown>
</div>
@@ -119,10 +136,11 @@
import { NDialogProvider, useDialog, useMessage } from 'naive-ui';
import { TABS_ROUTES } from '@/store/mutation-types';
import { useUserStore } from '@/store/modules/user';
import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useScreenLockStore } from '@/store/modules/screenLock';
import ProjectSetting from './ProjectSetting.vue';
import { AsideMenu } from '@/layout/components/Menu';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { websiteConfig } from '@/config/website.config';
export default defineComponent({
name: 'PageHeader',
@@ -131,37 +149,52 @@
collapsed: {
type: Boolean,
},
inverted: {
type: Boolean,
},
},
setup(props) {
emits: ['update:collapsed'],
setup(props, { emit }) {
const userStore = useUserStore();
const useLockscreen = useLockscreenStore();
const useLockscreen = useScreenLockStore();
const message = useMessage();
const dialog = useDialog();
const { getNavMode, getNavTheme, getHeaderSetting, getMenuSetting, getCrumbsSetting } =
useProjectSetting();
const { username } = userStore?.info || {};
const { navMode, navTheme, headerSetting, menuSetting, crumbsSetting } = useProjectSetting();
const drawerSetting = ref();
const state = reactive({
username: username || '',
username: userStore?.info?.username ?? '',
fullscreenIcon: 'FullscreenOutlined',
navMode: getNavMode,
navTheme: getNavTheme,
headerSetting: getHeaderSetting,
crumbsSetting: getCrumbsSetting,
navMode,
navTheme,
headerSetting,
crumbsSetting,
});
const getInverted = computed(() => {
return ['light', 'header-dark'].includes(unref(navTheme))
? props.inverted
: !props.inverted;
});
const mixMenu = computed(() => {
return unref(menuSetting).mixMenu;
});
const getChangeStyle = computed(() => {
const { collapsed } = props;
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
const { minMenuWidth, menuWidth } = unref(menuSetting);
return {
left: collapsed ? `${minMenuWidth}px` : `${menuWidth}px`,
width: `calc(100% - ${collapsed ? `${minMenuWidth}px` : `${menuWidth}px`})`,
};
});
const getMenuLocation = computed(() => {
return 'header';
});
const router = useRouter();
const route = useRoute();
@@ -291,6 +324,10 @@
openDrawer();
}
function handleMenuCollapsed() {
emit('update:collapsed', !props.collapsed);
}
return {
...toRefs(state),
iconList,
@@ -305,6 +342,11 @@
reloadPage,
drawerSetting,
openSetting,
getInverted,
getMenuLocation,
mixMenu,
websiteConfig,
handleMenuCollapsed,
};
},
});
@@ -316,32 +358,48 @@
justify-content: space-between;
align-items: center;
padding: 0;
height: @header-height;
height: 64px;
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
transition: all 0.2s ease-in-out;
width: 100%;
z-index: 11;
//color: #fff;
//.n-icon {
// color: #fff
//}
&-left {
display: flex;
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) {
color: #515a6e;
}
::v-deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
color: #fff;
}
.n-breadcrumb {
display: inline-block;
}
&-menu {
color: var(--text-color);
}
}
&-right {

View File

@@ -1,11 +1,12 @@
<template>
<div class="logo">
<img src="~@/assets/images/logo.png" alt="" />
<h2 v-show="!collapsed" class="title">&nbsp;NaiveUiAdmin</h2>
<img :src="websiteConfig.logo" alt="" :class="{ 'mr-2': !collapsed }" />
<h2 v-show="!collapsed" class="title">{{ websiteConfig.title }}</h2>
</div>
</template>
<script>
<script lang="ts">
import { websiteConfig } from '@/config/website.config';
export default {
name: 'Index',
props: {
@@ -13,6 +14,11 @@
type: Boolean,
},
},
data() {
return {
websiteConfig,
};
},
};
</script>
@@ -32,8 +38,7 @@
}
.title {
color: white;
margin-bottom: 0;
margin: 0;
}
}
</style>

View File

@@ -1,19 +1,28 @@
<template>
<RouterView>
<template #default="{ Component, route }">
<transition name="zoom-fade" mode="out-in" appear>
<keep-alive v-if="keepAliveComponents" :include="keepAliveComponents">
<template v-if="mode === 'production'">
<transition :name="getTransitionName" mode="out-in" appear>
<keep-alive v-if="keepAliveComponents.length" :include="keepAliveComponents">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<component v-else :is="Component" :key="route.fullPath" />
</transition>
</template>
<template v-else>
<keep-alive v-if="keepAliveComponents.length" :include="keepAliveComponents">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<component v-else :is="Component" :key="route.fullPath" />
</transition>
</template>
</template>
</RouterView>
</template>
<script>
import { defineComponent, computed } from 'vue';
import { defineComponent, computed, unref } from 'vue';
import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
export default defineComponent({
name: 'MainView',
@@ -29,11 +38,20 @@
},
},
setup() {
const { isPageAnimate, pageAnimateType } = useProjectSetting();
const asyncRouteStore = useAsyncRouteStore();
// 需要缓存的路由组件
const keepAliveComponents = computed(() => asyncRouteStore.keepAliveComponents);
const getTransitionName = computed(() => {
return unref(isPageAnimate) ? unref(pageAnimateType) : '';
});
const mode = import.meta.env.MODE;
return {
keepAliveComponents,
getTransitionName,
mode,
};
},
});

View File

@@ -6,23 +6,24 @@
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="20"
:indent="28"
:indent="24"
:expanded-keys="openKeys"
v-model:value="selectedKeys"
:value="getSelectedKeys"
@update:value="clickMenuItem"
@update:expanded-keys="menuExpanded"
/>
</template>
<script lang="ts">
import { defineComponent, reactive, computed, watch, toRefs, unref } from 'vue';
import { defineComponent, ref, onMounted, reactive, computed, watch, toRefs, unref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { generatorMenu } from '@/utils/index';
import { generatorMenu, generatorMenuMix } from '@/utils';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
export default defineComponent({
name: 'Menu',
name: 'AppMenu',
components: {},
props: {
mode: {
@@ -34,13 +35,24 @@
// 侧边栏菜单是否收起
type: Boolean,
},
//位置
location: {
type: String,
default: 'left',
},
},
setup(props) {
emits: ['update:collapsed', 'clickMenuItem'],
setup(props, { emit }) {
// 当前路由
const currentRoute = useRoute();
const router = useRouter();
const asyncRouteStore = useAsyncRouteStore();
const settingStore = useProjectSettingStore();
const menus = ref<any[]>([]);
const selectedKeys = ref<string>(currentRoute.name as string);
const headerMenuSelectKey = ref<string>('');
const { navMode } = useProjectSetting();
// 获取当前打开的子菜单
const matched = currentRoute.matched;
@@ -49,36 +61,65 @@
const state = reactive({
openKeys: getOpenKeys,
selectedKeys: currentRoute.name,
});
const inverted = computed(() => {
return ['dark', 'header-dark'].includes(settingStore.navTheme);
});
const menus = computed(() => {
return generatorMenu(asyncRouteStore.getMenus);
const getSelectedKeys = computed(() => {
let location = props.location;
return location === 'left' || (location === 'header' && unref(navMode) === 'horizontal')
? unref(selectedKeys)
: unref(headerMenuSelectKey);
});
// 监听菜单收缩状态
// 监听分割菜单
watch(
() => props.collapsed,
(newVal) => {
state.openKeys = newVal ? [] : getOpenKeys;
state.selectedKeys = currentRoute.name;
() => settingStore.menuSetting.mixMenu,
() => {
updateMenu();
if (props.collapsed) {
emit('update:collapsed', !props.collapsed);
}
}
);
// 监听菜单收缩状态
// watch(
// () => props.collapsed,
// (newVal) => {
// }
// );
// 跟随页面路由变化,切换菜单选中状态
watch(
() => currentRoute.fullPath,
() => {
const matched = currentRoute.matched;
state.openKeys = matched.map((item) => item.name);
state.selectedKeys = currentRoute.name;
updateMenu();
}
);
function updateSelectedKeys() {
const matched = currentRoute.matched;
state.openKeys = matched.map((item) => item.name);
const activeMenu: string = (currentRoute.meta?.activeMenu as string) || '';
selectedKeys.value = activeMenu ? (activeMenu as string) : (currentRoute.name as string);
}
function updateMenu() {
if (!settingStore.menuSetting.mixMenu) {
menus.value = generatorMenu(asyncRouteStore.getMenus);
} else {
//混合菜单
const firstRouteName: string = (currentRoute.matched[0].name as string) || '';
menus.value = generatorMenuMix(asyncRouteStore.getMenus, firstRouteName, props.location);
const activeMenu: string = currentRoute?.matched[0].meta?.activeMenu as string;
headerMenuSelectKey.value = (activeMenu ? activeMenu : firstRouteName) || '';
}
updateSelectedKeys();
}
// 点击菜单
function clickMenuItem(key: string) {
if (/http(s)?:/.test(key)) {
@@ -86,6 +127,7 @@
} else {
router.push({ name: key });
}
emit('clickMenuItem' as any, key);
}
//展开菜单
@@ -101,17 +143,24 @@
if (!key) return false;
const subRouteChildren: string[] = [];
for (const { children, key } of unref(menus)) {
if (children && children.length > 0) {
if (children && children.length) {
subRouteChildren.push(key as string);
}
}
return subRouteChildren.includes(key);
}
onMounted(() => {
updateMenu();
});
return {
...toRefs(state),
inverted,
menus,
selectedKeys,
headerMenuSelectKey,
getSelectedKeys,
clickMenuItem,
menuExpanded,
};

View File

@@ -1,24 +0,0 @@
import {
DownOutlined,
ReloadOutlined,
CloseOutlined,
VerticalRightOutlined,
VerticalLeftOutlined,
ColumnWidthOutlined,
MinusOutlined,
} from '@ant-design/icons-vue';
import { Dropdown, Tabs, Card } from 'ant-design-vue';
export default {
[Tabs.name]: Tabs,
[Tabs.TabPane.name]: Tabs.TabPane,
[Dropdown.name]: Dropdown,
[Card.name]: Card,
MinusOutlined,
DownOutlined,
ReloadOutlined,
CloseOutlined,
VerticalRightOutlined,
VerticalLeftOutlined,
ColumnWidthOutlined,
};

View File

@@ -1,10 +1,11 @@
<template>
<div
class="tabs-view"
class="box-border tabs-view"
:class="{
'tabs-view-fix': multiTabsSetting.fixed,
'tabs-view-fixed-header': isMultiHeaderFixed,
'tabs-view-default-background': getDarkTheme === false,
'tabs-view-dark-background': getDarkTheme === true,
}"
:style="getChangeStyle"
>
@@ -29,27 +30,22 @@
</n-icon>
</span>
<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">
<template #item="{ element }">
<div
class="tabs-card-scroll-item"
:class="{ 'active-item': activeKey === element.path }"
@click.stop="goPage(element)"
@contextmenu="handleContextMenu($event, element)"
>
<span>{{ element.meta.title }}</span>
<n-icon
size="14"
@click.stop="closeTabItem(element)"
v-if="element.path != baseHome"
>
<CloseOutlined />
</n-icon>
</div>
</template>
</Draggable>
</div>
<Draggable :list="tabsList" animation="300" item-key="fullPath" class="flex">
<template #item="{ element }">
<div
:id="`tag${element.fullPath.split('/').join('\/')}`"
class="tabs-card-scroll-item"
:class="{ 'active-item': activeKey === element.fullPath }"
@click.stop="goPage(element)"
@contextmenu="handleContextMenu($event, element)"
>
<span>{{ element.meta.title }}</span>
<n-icon size="14" @click.stop="closeTabItem(element)" v-if="!element.meta.affix">
<CloseOutlined />
</n-icon>
</div>
</template>
</Draggable>
</div>
</div>
<div class="tabs-close">
@@ -59,7 +55,7 @@
placement="bottom-end"
:options="TabsMenuOptions"
>
<div class="tabs-close-btn" @click.prevent>
<div class="tabs-close-btn">
<n-icon size="16" color="#515a6e">
<DownOutlined />
</n-icon>
@@ -86,8 +82,6 @@
computed,
ref,
toRefs,
toRaw,
unref,
provide,
watch,
onMounted,
@@ -101,8 +95,7 @@
import { RouteItem } from '@/store/modules/tabsView';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { useMessage } from 'naive-ui';
// @ts-ignore
import Draggable from 'vuedraggable/src/vuedraggable';
import Draggable from 'vuedraggable';
import { PageEnum } from '@/enums/pageEnum';
import {
DownOutlined,
@@ -113,9 +106,12 @@
LeftOutlined,
RightOutlined,
} from '@vicons/antd';
import { renderIcon } from '@/utils/index';
import { renderIcon } from '@/utils';
import elementResizeDetectorMaker from 'element-resize-detector';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useThemeVars } from 'naive-ui';
import { useGo } from '@/hooks/web/usePage';
export default defineComponent({
name: 'TabsView',
@@ -132,31 +128,39 @@
},
},
setup(props) {
const { getDarkTheme } = useDesignSetting();
const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting } =
const { getDarkTheme, getAppTheme } = useDesignSetting();
const { navMode, headerSetting, menuSetting, multiTabsSetting, isMobile } =
useProjectSetting();
const settingStore = useProjectSettingStore();
const message = useMessage();
const route = useRoute();
const router = useRouter();
const tabsViewStore = useTabsViewStore();
const asyncRouteStore = useAsyncRouteStore();
const navRef: any = ref(null);
const navScroll: any = ref(null);
const navWrap: any = ref(null);
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({
activeKey: route.fullPath,
scrollable: false,
navStyle: {
transform: '',
},
dropdownX: 0,
dropdownY: 0,
showDropdown: false,
isMultiHeaderFixed: false,
multiTabsSetting: getMultiTabsSetting,
multiTabsSetting: multiTabsSetting,
});
// 获取简易的路由对象
@@ -165,14 +169,31 @@
return { fullPath, hash, meta, name, params, path, query };
};
const isMixMenuNoneSub = computed(() => {
const mixMenu = settingStore.menuSetting.mixMenu;
const currentRoute = useRoute();
if (navMode.value != 'horizontal-mix') return true;
return !(navMode.value === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot);
});
//动态组装样式 菜单缩进
const getChangeStyle = computed(() => {
const { collapsed } = props;
const navMode = unref(getNavMode);
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
const { fixed }: any = unref(getMultiTabsSetting);
const { minMenuWidth, menuWidth }: any = menuSetting.value;
const { fixed }: any = multiTabsSetting.value;
let lenNum =
navMode === 'horizontal' ? '0px' : collapsed ? `${minMenuWidth}px` : `${menuWidth}px`;
navMode.value === 'horizontal' || !isMixMenuNoneSub.value
? '0px'
: collapsed
? `${minMenuWidth}px`
: `${menuWidth}px`;
if (isMobile.value) {
return {
left: '0px',
width: '100%',
};
}
return {
left: lenNum,
width: `calc(100% - ${!fixed ? '0px' : lenNum})`,
@@ -181,7 +202,7 @@
//tags 右侧下拉菜单
const TabsMenuOptions = computed(() => {
const isDisabled = unref(tabsList).length <= 1 ? true : false;
const isDisabled = tabsList.value.length <= 1;
return [
{
label: '刷新当前',
@@ -191,7 +212,7 @@
{
label: `关闭当前`,
key: '2',
disabled: unref(isCurrent) || isDisabled,
disabled: isCurrent.value || isDisabled,
icon: renderIcon(CloseOutlined),
},
{
@@ -209,17 +230,27 @@
];
});
let routes: RouteItem[] = [];
let cacheRoutes: RouteItem[] = [];
const simpleRoute = getSimpleRoute(route);
try {
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) {
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) {
@@ -228,11 +259,11 @@
document.documentElement.scrollTop ||
window.pageYOffset ||
document.body.scrollTop; // 滚动条偏移量
if (!getHeaderSetting.fixed && getMultiTabsSetting.fixed && scrollTop >= 64) {
state.isMultiHeaderFixed = true;
} else {
state.isMultiHeaderFixed = false;
}
state.isMultiHeaderFixed = !!(
!headerSetting.value.fixed &&
multiTabsSetting.value.fixed &&
scrollTop >= 64
);
}
window.addEventListener('scroll', onScroll, true);
@@ -263,8 +294,8 @@
(to) => {
if (whiteList.includes(route.name as string)) return;
state.activeKey = to;
tabsViewStore.addTabs(getSimpleRoute(route));
updateNavScroll();
tabsViewStore.addTab(getSimpleRoute(route));
updateNavScroll(true);
},
{ immediate: true }
);
@@ -294,7 +325,7 @@
const reloadPage = () => {
delKeepAliveCompName();
router.push({
path: '/redirect' + unref(route).fullPath,
path: '/redirect' + route.fullPath,
});
};
@@ -327,7 +358,6 @@
// 关闭全部
const closeAll = () => {
localStorage.removeItem('routes');
tabsViewStore.closeAllTabs();
router.replace(PageEnum.BASE_HOME);
updateNavScroll();
@@ -354,66 +384,76 @@
break;
}
updateNavScroll();
state.showDropdown = false;
};
function getCurrentScrollOffset() {
const { navStyle } = state;
const transform: any = toRaw(navStyle.transform);
return transform ? Number(transform.match(/translateX\(-(\d+(\.\d+)*)px\)/)[1]) : 0;
}
function setOffset(value) {
state.navStyle.transform = `translateX(-${value}px)`;
/**
* @param value 要滚动到的位置
* @param amplitude 每次滚动的长度
*/
function scrollTo(value: number, amplitude: number) {
const currentScroll = navScroll.value.scrollLeft;
const scrollWidth =
(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() {
const containerWidth = navScroll.value.offsetWidth;
const currentOffset = getCurrentScrollOffset();
if (!currentOffset) return;
let newOffset = currentOffset > containerWidth ? currentOffset - containerWidth : 0;
setOffset(newOffset);
const currentScroll = navScroll.value.scrollLeft;
if (!currentScroll) return;
const scrollLeft = currentScroll > containerWidth ? currentScroll - containerWidth : 0;
scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20);
}
function scrollNext() {
const navWidth = navRef.value.scrollWidth;
const containerWidth = navScroll.value.offsetWidth;
const currentOffset = getCurrentScrollOffset();
if (navWidth - currentOffset <= containerWidth) return;
const navWidth = navScroll.value.scrollWidth;
const currentScroll = navScroll.value.scrollLeft;
let newOffset =
navWidth - currentOffset > containerWidth * 2
? currentOffset + containerWidth
if (navWidth - currentScroll <= containerWidth) return;
const scrollLeft =
navWidth - currentScroll > containerWidth * 2
? currentScroll + containerWidth
: navWidth - containerWidth;
setOffset(newOffset);
scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20);
}
function updateNavScroll() {
if (!navRef.value) return;
let navWidth = navRef.value.scrollWidth;
let containerWidth = navScroll.value.offsetWidth;
const currentOffset = getCurrentScrollOffset();
/**
* @param autoScroll 是否开启自动滚动功能
*/
async function updateNavScroll(autoScroll?: boolean) {
await nextTick();
if (!navScroll.value) return;
const containerWidth = navScroll.value.offsetWidth;
const navWidth = navScroll.value.scrollWidth;
if (containerWidth < navWidth) {
state.scrollable = true;
if (navWidth - currentOffset < containerWidth) {
setOffset(navWidth - containerWidth);
if (autoScroll) {
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 {
state.scrollable = false;
if (currentOffset > 0) {
setOffset(0);
}
}
}
function handleResize() {
updateNavScroll();
updateNavScroll(true);
}
const getNavStyle = computed(() => {
return state.navStyle;
});
function handleContextMenu(e, item) {
e.preventDefault();
isCurrent.value = PageEnum.BASE_HOME_REDIRECT === item.path;
@@ -434,7 +474,7 @@
const { fullPath } = e;
if (fullPath === route.fullPath) return;
state.activeKey = fullPath;
router.push({ path: fullPath });
go(e, true);
}
//删除tab
@@ -457,11 +497,9 @@
return {
...toRefs(state),
navWrap,
navRef,
navScroll,
route,
tabsList,
baseHome: PageEnum.BASE_HOME_REDIRECT,
goPage,
closeTabItem,
closeLeft,
@@ -474,10 +512,12 @@
closeHandleSelect,
scrollNext,
scrollPrev,
getNavStyle,
handleContextMenu,
onClickOutside,
getDarkTheme,
getAppTheme,
getCardColor,
getBaseColor,
};
},
});
@@ -534,22 +574,12 @@
}
&-scroll {
overflow: hidden;
white-space: nowrap;
.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;
}
overflow: hidden;
&-item {
background: var(--color);
color: var(--text-color);
background: v-bind(getCardColor);
color: v-bind(getBaseColor);
height: 32px;
padding: 6px 16px 4px;
border-radius: 3px;
@@ -557,6 +587,7 @@
cursor: pointer;
display: inline-block;
position: relative;
flex: 0 0 auto;
span {
float: left;
@@ -588,7 +619,7 @@
}
.active-item {
color: #2d8cf0;
color: v-bind(getAppTheme);
}
}
}
@@ -608,7 +639,6 @@
background: var(--color);
border-radius: 2px;
cursor: pointer;
//margin-right: 10px;
&-btn {
color: var(--color);
@@ -624,10 +654,14 @@
background: #f5f7f9;
}
.tabs-view-dark-background {
background: #101014;
}
.tabs-view-fix {
position: fixed;
z-index: 5;
padding: 6px 19px 6px 10px;
padding: 6px 10px 6px 10px;
left: 200px;
}

View File

@@ -1,8 +1,10 @@
<template>
<NLayout class="layout" :position="fixedMenu" has-sider>
<NLayoutSider
v-if="navMode === 'vertical'"
show-trigger
<n-layout class="layout" :position="fixedMenu" has-sider>
<n-layout-sider
v-if="
!isMobile && isMixMenuNoneSub && (navMode === 'vertical' || navMode === 'horizontal-mix')
"
show-trigger="bar"
@collapse="collapsed = true"
:position="fixedMenu"
@expand="collapsed = false"
@@ -15,15 +17,34 @@
class="layout-sider"
>
<Logo :collapsed="collapsed" />
<AsideMenu v-model:collapsed="collapsed" />
</NLayoutSider>
<AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" />
</n-layout-sider>
<NLayout :inverted="inverted">
<NLayoutHeader :inverted="inverted" :position="fixedHeader">
<PageHeader v-model:collapsed="collapsed" />
</NLayoutHeader>
<n-drawer
v-model:show="showSideDrawer"
:width="menuWidth"
:placement="'left'"
class="layout-side-drawer"
>
<n-layout-sider
:position="fixedMenu"
:collapsed="false"
:width="menuWidth"
:native-scrollbar="false"
:inverted="inverted"
class="layout-sider"
>
<Logo :collapsed="collapsed" />
<AsideMenu v-model:location="getMenuLocation" />
</n-layout-sider>
</n-drawer>
<NLayoutContent
<n-layout :inverted="inverted">
<n-layout-header :inverted="getHeaderInverted" :position="fixedHeader">
<PageHeader v-model:collapsed="collapsed" :inverted="inverted" />
</n-layout-header>
<n-layout-content
class="layout-content"
:class="{ 'layout-default-background': getDarkTheme === false }"
>
@@ -50,110 +71,134 @@
<!-- <NLayoutFooter v-if="getShowFooter">-->
<!-- <PageFooter />-->
<!-- </NLayoutFooter>-->
</NLayoutContent>
</NLayout>
</NLayout>
</n-layout-content>
<n-back-top :right="100" />
</n-layout>
</n-layout>
</template>
<script lang="ts">
import { defineComponent, ref, unref, computed, onMounted } from 'vue';
<script lang="ts" setup>
import { ref, unref, computed, onMounted } from 'vue';
import { Logo } from './components/Logo';
import { TabsView } from './components/TagsView';
import { MainView } from './components/Main';
import { AsideMenu } from './components/Menu';
import { PageHeader } from './components/Header';
import { PageFooter } from './components/Footer';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
import { useRoute } from 'vue-router';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
export default defineComponent({
name: 'Layout',
components: {
TabsView,
MainView,
PageHeader,
AsideMenu,
Logo,
PageFooter,
},
setup() {
const { getDarkTheme } = useDesignSetting();
const { getDarkTheme } = useDesignSetting();
const {
// showFooter,
navMode,
navTheme,
headerSetting,
menuSetting,
multiTabsSetting,
} = useProjectSetting();
const {
getShowFooter,
getNavMode,
getNavTheme,
getHeaderSetting,
getMenuSetting,
getMultiTabsSetting,
} = useProjectSetting();
const settingStore = useProjectSettingStore();
const navMode = getNavMode;
const collapsed = ref<boolean>(false);
const collapsed = ref<boolean>(false);
const { mobileWidth, menuWidth } = unref(menuSetting);
const fixedHeader = computed(() => {
const { fixed } = unref(getHeaderSetting);
return fixed ? 'absolute' : 'static';
});
const isMobile = computed<boolean>({
get: () => settingStore.getIsMobile,
set: (val) => settingStore.setIsMobile(val),
});
const fixedMenu = computed(() => {
const { fixed } = unref(getHeaderSetting);
return fixed ? 'absolute' : 'static';
});
const fixedHeader = computed(() => {
const { fixed } = unref(headerSetting);
return fixed ? 'absolute' : 'static';
});
const isMultiTabs = computed(() => {
return unref(getMultiTabsSetting).show;
});
const isMixMenuNoneSub = computed(() => {
const mixMenu = unref(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(() => {
return unref(getMultiTabsSetting).fixed;
});
const fixedMenu = computed(() => {
const { fixed } = unref(headerSetting);
return fixed ? 'absolute' : 'static';
});
const inverted = computed(() => {
return ['dark', 'header-dark'].includes(unref(getNavTheme));
});
const isMultiTabs = computed(() => {
return unref(multiTabsSetting).show;
});
const leftMenuWidth = computed(() => {
const { minMenuWidth, menuWidth } = unref(getMenuSetting);
return collapsed.value ? minMenuWidth : menuWidth;
});
const fixedMulti = computed(() => {
return unref(multiTabsSetting).fixed;
});
const getChangeStyle = computed(() => {
const { minMenuWidth, menuWidth } = unref(getMenuSetting);
return {
'padding-left': collapsed.value ? `${minMenuWidth}px` : `${menuWidth}px`,
};
});
const inverted = computed(() => {
return ['dark', 'header-dark'].includes(unref(navTheme));
});
function watchWidth() {
const Width = document.body.clientWidth;
if (Width <= 950) {
collapsed.value = true;
} else collapsed.value = false;
}
const getHeaderInverted = computed(() => {
return ['light', 'header-dark'].includes(unref(navTheme)) ? unref(inverted) : !unref(inverted);
});
onMounted(() => {
window.addEventListener('resize', watchWidth);
});
const leftMenuWidth = computed(() => {
const { minMenuWidth, menuWidth } = unref(menuSetting);
return collapsed.value ? minMenuWidth : menuWidth;
});
return {
fixedMenu,
fixedMulti,
fixedHeader,
collapsed,
inverted,
isMultiTabs,
leftMenuWidth,
getChangeStyle,
navMode,
getShowFooter,
getDarkTheme,
};
},
const getMenuLocation = computed(() => {
return 'left';
});
// 控制显示或隐藏移动端侧边栏
const showSideDrawer = computed({
get: () => isMobile.value && collapsed.value,
set: (val) => (collapsed.value = val),
});
//判断是否触发移动端模式
const checkMobileMode = () => {
if (document.body.clientWidth <= mobileWidth) {
isMobile.value = true;
} else {
isMobile.value = false;
}
collapsed.value = false;
};
const watchWidth = () => {
const Width = document.body.clientWidth;
if (Width <= 950) {
collapsed.value = true;
} else collapsed.value = false;
checkMobileMode();
};
onMounted(() => {
checkMobileMode();
window.addEventListener('resize', watchWidth);
});
</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>
.layout {
display: flex;
@@ -214,7 +259,7 @@
}
.fluid-header {
padding-top: 0px;
padding-top: 0;
}
.main-view-fix {

View File

@@ -1,23 +1,23 @@
import './styles/tailwind.css';
import './styles/index.less';
import { createApp } from 'vue';
import { setupNaiveDiscreteApi, setupNaive, setupDirectives } from '@/plugins';
import App from './App.vue';
import router, { setupRouter } from './router';
import { setupStore } from '@/store';
import MakeitCaptcha from 'makeit-captcha';
import 'makeit-captcha/dist/captcha.min.css';
import { setupNaive, setupDirectives } from '@/plugins';
import { AppProvider } from '@/components/Application';
async function bootstrap() {
const appProvider = createApp(AppProvider);
const app = createApp(App);
app.use(MakeitCaptcha);
// 挂载状态管理
setupStore(app);
// 注册全局常用的 naive-ui 组件
setupNaive(app);
// 挂载 naive-ui 脱离上下文的 Api
setupNaiveDiscreteApi();
// 注册全局自定义组件
//setupCustomComponents();
@@ -27,18 +27,18 @@ async function bootstrap() {
// 注册全局方法app.config.globalProperties.$message = message
//setupGlobalMethods(app);
// 挂载状态管理
setupStore(app);
//优先挂载一下 Provider 解决路由守卫Axios中可使用DialogMessage 等之类组件
appProvider.mount('#appProvider', true);
// 挂载路由
await setupRouter(app);
setupRouter(app);
// 路由准备就绪后挂载APP实例
// 路由准备就绪后挂载 APP 实例
// https://router.vuejs.org/api/interfaces/router.html#isready
await router.isReady();
// https://www.naiveui.com/en-US/os-theme/docs/style-conflict#About-Tailwind's-Preflight-Style-Override
const meta = document.createElement('meta');
meta.name = 'naive-ui-style';
document.head.appendChild(meta);
app.mount('#app', true);
}

View File

@@ -1,6 +1,10 @@
import { App } from 'vue';
import { permission } from '@/directives/permission';
import copy from '@/directives/copy';
import debounce from '@/directives/debounce';
import throttle from '@/directives/throttle';
import draggable from '@/directives/draggable';
/**
* 注册全局自定义指令
@@ -9,4 +13,12 @@ import { permission } from '@/directives/permission';
export function setupDirectives(app: App) {
// 权限控制指令(演示)
app.directive('permission', permission);
// 复制指令
app.directive('copy', copy);
// 防抖指令
app.directive('debounce', debounce);
// 节流指令
app.directive('throttle', throttle);
// 拖拽指令
app.directive('draggable', draggable);
}

View File

@@ -1,4 +1,5 @@
export { setupNaive } from '@/plugins/naive';
export { setupNaiveDiscreteApi } from '@/plugins/naiveDiscreteApi';
export { setupDirectives } from '@/plugins/directives';
export { setupCustomComponents } from '@/plugins/customComponents';
export { setupGlobalMethods } from '@/plugins/globalMethods';

View File

@@ -1,9 +1,9 @@
import type { App } from 'vue';
import {
create,
NConfigProvider,
NMessageProvider,
NDialogProvider,
NConfigProvider,
NInput,
NButton,
NForm,
@@ -63,8 +63,13 @@ import {
NUpload,
NTree,
NSpin,
NTimePicker,
NBackTop,
NSkeleton,
NCascader,
} from 'naive-ui';
// https://www.naiveui.com/en-US/os-theme/docs/import-on-demand
const naive = create({
components: [
NMessageProvider,
@@ -129,6 +134,10 @@ const naive = create({
NUpload,
NTree,
NSpin,
NTimePicker,
NBackTop,
NSkeleton,
NCascader,
],
});

View File

@@ -0,0 +1,39 @@
import * as NaiveUI from 'naive-ui';
import { computed } from 'vue';
import { useDesignSetting } from '@/store/modules/designSetting';
import { lighten } from '@/utils/index';
/**
* 挂载 Naive-ui 脱离上下文的 API
* 如果你想在 setup 外使用 useDialog、useMessage、useNotification、useLoadingBar可以通过 createDiscreteApi 来构建对应的 API。
* https://www.naiveui.com/zh-CN/dark/components/discrete
*/
export function setupNaiveDiscreteApi() {
const designStore = useDesignSetting();
const configProviderPropsRef = computed(() => ({
theme: designStore.darkTheme ? NaiveUI.darkTheme : undefined,
themeOverrides: {
common: {
primaryColor: designStore.appTheme,
primaryColorHover: lighten(designStore.appTheme, 6),
primaryColorPressed: lighten(designStore.appTheme, 6),
},
LoadingBar: {
colorLoading: designStore.appTheme,
},
},
}));
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar'],
{
configProviderProps: configProviderPropsRef,
}
);
window['$message'] = message;
window['$dialog'] = dialog;
window['$notification'] = notification;
window['$loading'] = loadingBar;
}

View File

@@ -1,8 +1,8 @@
import type { AppRouteRecordRaw } from '@/router/types';
import { ErrorPage, RedirectName, Layout } from '@/router/constant';
import { RouteRecordRaw } from 'vue-router';
// 404 on a page
export const ErrorPageRoute: AppRouteRecordRaw = {
export const ErrorPageRoute: RouteRecordRaw = {
path: '/:path(.*)*',
name: 'ErrorPage',
component: Layout,
@@ -13,7 +13,7 @@ export const ErrorPageRoute: AppRouteRecordRaw = {
children: [
{
path: '/:path(.*)*',
name: 'ErrorPage',
name: 'ErrorPageSon',
component: ErrorPage,
meta: {
title: 'ErrorPage',
@@ -23,7 +23,7 @@ export const ErrorPageRoute: AppRouteRecordRaw = {
],
};
export const RedirectRoute: AppRouteRecordRaw = {
export const RedirectRoute: RouteRecordRaw = {
path: '/redirect',
name: RedirectName,
component: Layout,

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,64 +0,0 @@
import { adminMenus } from '@/api/system/menu';
import { constantRouterComponents, constantRouterIcon } from './constantRouterComponents';
import router from '@/router/index';
import { constantRouter } from '@/router/index';
import { RouteRecordRaw } from 'vue-router';
/**
* 格式化 后端 结构信息并递归生成层级路由表
*
* @param routerMap
* @param parent
* @returns {*}
*/
export const routerGenerator = (routerMap, parent?): any[] => {
return routerMap.map((item) => {
const currentRouter: any = {
// 路由地址 动态拼接生成如 /dashboard/workplace
path: `${(parent && parent.path) || ''}/${item.path}`,
// 路由名称,建议唯一
name: item.name || '',
// 该路由对应页面的 组件
component: constantRouterComponents[item.component],
// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
meta: {
...item.meta,
label: item.meta.title,
icon: constantRouterIcon[item.meta.icon] || null,
permission: item.meta.permission || null,
},
};
// 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
currentRouter.path = currentRouter.path.replace('//', '/');
// 重定向
item.redirect && (currentRouter.redirect = item.redirect);
// 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) {
// Recursion
currentRouter.children = routerGenerator(item.children, currentRouter);
}
return currentRouter;
});
};
/**
* 动态生成菜单
* @param token
* @returns {Promise<Router>}
*/
export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => {
return new Promise((resolve, reject) => {
adminMenus()
.then((result) => {
const routeList = routerGenerator(result);
const asyncRoutesList = [...constantRouter, ...routeList];
asyncRoutesList.forEach((item) => {
router.addRoute(item);
});
resolve(asyncRoutesList);
})
.catch((err) => {
reject(err);
});
});
};

114
src/router/generator.ts Normal file
View File

@@ -0,0 +1,114 @@
import { adminMenus } from '@/api/system/menu';
import { constantRouterIcon } from './icons';
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 parent
* @returns {*}
*/
export const generateRoutes = (routerMap, parent?): any[] => {
return routerMap.map((item) => {
const currentRoute: any = {
// 路由地址 动态拼接生成如 /dashboard/workplace
path: `${(parent && parent.path) ?? ''}/${item.path}`,
// 路由名称,建议唯一
name: item.name ?? '',
// 该路由对应页面的 组件
component: item.component,
// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
meta: {
...item.meta,
label: item.meta.title,
icon: constantRouterIcon[item.meta.icon] || null,
permissions: item.meta.permissions || null,
},
};
// 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
currentRoute.path = currentRoute.path.replace('//', '/');
// 重定向
item.redirect && (currentRoute.redirect = item.redirect);
// 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) {
//如果未定义 redirect 默认第一个子路由为 redirect
!item.redirect && (currentRoute.redirect = `${item.path}/${item.children[0].path}`);
// Recursion
currentRoute.children = generateRoutes(item.children, currentRoute);
}
return currentRoute;
});
};
/**
* 动态生成菜单
* @returns {Promise<Router>}
*/
export const generateDynamicRoutes = async (): Promise<RouteRecordRaw[]> => {
const result = await adminMenus();
const router = generateRoutes(result);
asyncImportRoute(router);
return router;
};
/**
* 查找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;
}
};

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