289 Commits
v1.3 ... v2.0.0

Author SHA1 Message Date
ahjung
7a62de39c2 2.0.0 2024-09-14 22:39:11 +08:00
Ah jung
1ae5372396 Merge pull request #294 from xiaohuanshu/patch-1
fix user logout error
2024-08-15 13:15:07 +08:00
xiaohuanshu
0bbc29023b fix user logout error 2024-08-13 17:44:24 +08:00
ahjung
84f55a8116 donate path update 2024-06-29 17:46:14 +08:00
ahjung
19df8d5f09 1.9.2 2024-06-29 17:35:10 +08:00
Ah jung
489f791caa Update README.md 2024-06-11 21:43:40 +08:00
ahjung
2f72f34bbe 优化已知错误 2024-05-12 11:02:47 +08:00
ahjung
3d4d554733 文案修改 2024-04-10 14:44:27 +08:00
ahjung
38fe255306 basicModal 组件 导出 modalMethods 方法 2024-04-10 14:36:36 +08:00
ahjung
58e99c183b README 文件更新 2024-04-09 16:43:58 +08:00
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
Ah jung
619669ec9e fix Bug or add docs 2021-07-30 10:26:19 +08:00
啊俊
044976b790 Perfect TS type detection 2021-07-25 22:48:32 +08:00
Ah jung
b43ab1ceb4 fix Bug 2021-07-22 13:54:36 +08:00
Ah jung
f773a3ed06 fix Bug or esLink formatting 2021-07-22 13:48:55 +08:00
Ah jung
7f81152793 fix Bug or esLink formatting 2021-07-22 13:47:44 +08:00
Ah jung
f6be8f521e fix Bug or add example 2021-07-21 18:38:56 +08:00
Ah jung
fa8b33acbe fix Bug or add example 2021-07-21 18:33:02 +08:00
啊俊
54e68db0c2 新增-系统设置-菜单权限-角色权限页面 2021-07-20 22:53:08 +08:00
Ah jung
9542345b54 版本号更新 2021-07-19 17:09:44 +08:00
285 changed files with 24130 additions and 14899 deletions

19
.editorconfig Normal file
View File

@@ -0,0 +1,19 @@
root = true
[*]
charset=utf-8
end_of_line=LF
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100
[*.{yml,yaml,json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

3
.env
View File

@@ -6,6 +6,3 @@ VITE_GLOB_APP_TITLE = AdminPro
# spa shortname
VITE_GLOB_APP_SHORT_NAME = AdminPro
# 生产环境 开启mock
VITE_GLOB_PROD_MOCK = true

View File

@@ -4,23 +4,27 @@ VITE_PORT = 8001
# 网站根目录
VITE_PUBLIC_PATH = /
# 是否开启mock
# 是否开启 mock
VITE_USE_MOCK = true
# 网站前缀
VITE_BASE_URL = /
# 是否开启控制台打印 mock 请求信息
VITE_LOGGER_MOCK = true
# 是否删除console
VITE_DROP_CONSOLE = true
# 跨域代理,可以配置多个,请注意不要换行
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
#VITE_PROXY=[["/api","https://naive-ui-admin"]]
# API 接口地址
VITE_APP_API_URL = /
# 图片上传地址
VITE_GLOB_UPLOAD_URL= /
# 图片前缀地址
VITE_GLOB_IMG_URL= /
VITE_GLOB_API_URL =
# 接口前缀
VITE_GLOB_API_URL_PREFIX = /api
# 文件上传地址
VITE_GLOB_UPLOAD_URL=
# 文件前缀地址
VITE_GLOB_FILE_URL=

View File

@@ -4,20 +4,25 @@ VITE_USE_MOCK = true
# 网站根目录
VITE_PUBLIC_PATH = /
# 网站前缀
VITE_BASE_URL = /
# 是否删除console
VITE_DROP_CONSOLE = true
# API
VITE_APP_API_URL = /
# 图片上传地址
VITE_GLOB_UPLOAD_URL= /
# 图片前缀地址
VITE_GLOB_IMG_URL= /
VITE_GLOB_API_URL =
# 接口前缀
VITE_GLOB_API_URL_PREFIX = /api
# 图片上传地址
VITE_GLOB_UPLOAD_URL=
# 图片前缀地址
VITE_GLOB_IMG_URL=
# 是否启用gzip压缩或brotli压缩
# 可选: gzip | brotli | none
# 如果你需要多种形式,你可以用','来分隔
VITE_BUILD_COMPRESS = 'none'
# 使用压缩时是否删除原始文件默认为false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false

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

@@ -1,9 +1,11 @@
module.exports = {
// @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
root: true,
env: {
browser: true,
node: true,
es6: true
es6: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
@@ -12,21 +14,40 @@ module.exports = {
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true
}
jsx: true,
},
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended'
'plugin:prettier/recommended',
],
rules: {
'vue/no-unused-components': 'off',
'vue/no-unused-vars': 'off',
'vue/no-v-for-template-key-on-child': 'off',
'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',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'vue/custom-event-name-casing': 'off',
// 'vue/attributes-order': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@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', { varsIgnorePattern: '.*', args: 'none' }],
'no-unused-vars': [
'error',
// 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',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
@@ -34,34 +55,17 @@ module.exports = {
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'space-before-function-paren': 'off',
'@typescript-eslint/camelcase': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always'
component: 'always',
},
svg: 'always',
math: 'always'
}
]
}
}
math: 'always',
},
],
},
});

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,214 @@
# 1.3 (2021-07-19)
# CHANGELOG
## 2.0.0
- 新增 `alova` 请求库
- 新增 `@faker-js/faker` 可配合 `mock` 数据模拟
- 新增 `VITE_USE_MOCK` 环境变量-开启 `mock`
- 新增 `demo` 实例,新增/编辑角色
- 移除 `axios` 请求封装,使用 `alova` 代替
- 移除 `vite-plugin-mock` 使用 `@alova/mock` 代替
- 移除 `VITE_GLOB_PROD_MOCK` 环境变量
- 变更 `VITE_GLOB_IMG_URL` 环境变量变更成 `VITE_GLOB_FILE_URL`
- 优化 `BasicTable` 组件相关样式
- 优化 `TS` 类型定义
- 依赖升级
## 1.9.2
- 升级 `vite``5.x` 版本
- 优化 `BasicTable` 组件,编辑样式
- 新增 `BasicTable` 组件,支持 `striped` 入参
- 依赖升级
## 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
- 修复表格列配置,拖拽时最后的操作列重复增加
- 多标签页交互优化
- ### ✨ Features
- `项目文档`已上线
- `Application`组件加载机制优化解决路由守卫Axios中可使用DialogMessage 等之类组件
- `BasicTable` 组件新增`高度自适应``单元格编辑``整行编辑` 特性
- `nprogress` 移除,用 `Loading Bar`代替
- 打包支持`gzip``brotli` 压缩
- 新增代理`VITE_PROXY`配置
- 路由菜单,支持多级菜单
- 依赖升级
- 本次更新,有破坏性更新,涉及文件重命名,增删调整
# 1.4.0 (2021-07-21)
### 🐛 Bug Fixes
- vite降至2.3.6
- 多标签页交互优化
- ### ✨ Features
- 新增 `TableAction` 组件
- 新增 `菜单权限管理` 示例
- 新增 `角色权限管理` 示例
- 持续更新更多实用组件及示例感谢Star
# 1.3.0 (2021-07-19)
### 🐛 Bug Fixes
- 修复多标签页左右切换按钮自适应展示
- 修复登录页面出现多标签页
@@ -11,7 +221,7 @@
- 持续更新更多实用组件及示例感谢Star
# 1.2 (2021-07-16)
# 1.2.0 (2021-07-16)
### 🐛 Bug Fixes
- 修复面包屑显示登录页面
- 菜单支持只展开当前父级菜单
@@ -25,7 +235,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.1 (2021-07-15)
# 1.1.0 (2021-07-15)
- ### ✨ Features
- 新增 `基础表单` 示例页面
- 新增 `分步表单` 示例页面
@@ -33,7 +243,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.0 (2021-07-12)
# 1.0.0 (2021-07-12)
### 🐛 Bug Fixes
- 修复页面切换面包屑未及时更新

119
README.md
View File

@@ -1,43 +1,64 @@
## 简介
## 🚀 简介
Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考
`Naive Ui Admin` 是一款 完全免费 且可商用的中后台解决方案,基于 🌟 `Vue3.0` 🌟、🚀 `Vite` 🚀、✨ [Naive UI](https://www.naiveui.com/) ✨ 和 🎉 `TypeScript` 🎉
它融合了最新的前端技术栈,提炼了典型的业务模型和页面,包括二次封装组件、动态菜单、权限校验等功能,助力快速搭建企业级中后台项目。
## 特性
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发
- **TypeScript**: 应用程序级 JavaScript 的语言
- **主题**:可配置的主题
- **Mock 数据** 内置 Mock 数据方案
- **权限** 内置完善的动态路由权限生成方案
- **组件** 二次封装了多个常用的组件
### 页面功能
#### 系统看板
- [x] 主控台
- [ ] 监控页
- [x] 工作台
- [x] 表单页面
- [x] 列表页面
- [x] 异常页面
- [x] 结果页面
- [x] 设置页面
### 页面组件
#### ProTable
- [x] 基础表格
- [x] 上传图片
- [x] 滑块验证码
- 持续开发中...
## 在线预览
## 🌈 特性
📦 二次封装的实用高扩展性组件
🎨 响应式、多主题、多配置,快速集成,开箱即用
🚀 强大的鉴权系统,支持 三种鉴权模式,满足多样业务需求
🌐 持续更新的实用性页面模板和交互设计,简化页面构建
## 🎥 预览
- [naive-ui-admin](https://jekip.github.io)
账号admin密码123456
账号admin密码123456(随意)
## 文档
## 💡 提示
[文档地址](https://github.com/jekip/naive-ui-admin) - 待完善
如果您需要更多功能和组件,不妨尝试全新的 `NaiveAdmin`,它可能正是您寻找的解决方案
## 准备
[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://docs.naiveadmin.com)
## 🛠 准备
- [node](http://nodejs.org/) 和 [git](https://git-scm.com/) -项目开发环境
- [Vite](https://vitejs.dev/) - 熟悉 vite 特性
@@ -45,10 +66,11 @@ Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3
- [TypeScript](https://www.typescriptlang.org/) - 熟悉`TypeScript`基本语法
- [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法
- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router 基本使用
- [Naive-ui-admin](https://www.naiveui.com/) - ui 基本使用
- [NaiveUi](https://www.naiveui.com/) - ui 基本使用
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
## 安装使用
## 🏗️ 使用
- 获取项目代码
@@ -61,33 +83,30 @@ git clone https://github.com/jekip/naive-ui-admin.git
```bash
cd naive-ui-admin
yarn install
pnpm install
```
- 运行
```bash
yarn dev
pnpm run dev
```
- 打包
```bash
yarn build
pnpm build
```
## 更新日志
## 📜 更新日志
[CHANGELOG](./CHANGELOG.md)
## 感谢
[@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,非常感谢作者。
## 🤝 如何贡献
## 如何贡献
非常欢迎你的加入![提一个 Issue](https://github.com/jekip/naive-ui-admin/issues) 或者提交一个 Pull Request。
非常欢迎你的加入![提一个 Issue](https://github.com/jekip/naive-ui-admin/issues) 或者提交一个 `Pull Request`
**Pull Request:**
@@ -97,7 +116,7 @@ yarn build
4. 推送您的分支: `git push origin feat/xxxx`
5. 提交`pull request`
## Git 贡献提交规范
## 📋 Git 贡献提交规范
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
@@ -115,7 +134,7 @@ yarn build
- `types` 类型定义文件更改
- `wip` 开发中
## 浏览器支持
## 🌐 浏览器支持
本地开发推荐使用`Chrome 80+` 浏览器
@@ -125,12 +144,18 @@ yarn build
| :-: | :-: | :-: | :-: | :-: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 维护者
## 👥 维护者
[@Ah jung](https://github.com/jekip)
## 交流
## 💬 交流
`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://assets.naiveadmin.com/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

@@ -31,8 +31,7 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
if (envName === 'VITE_PROXY') {
try {
realName = JSON.parse(realName);
} catch (error) {
}
} catch (error) {}
}
ret[envName] = realName;
process.env[envName] = realName;
@@ -51,12 +50,11 @@ export function getEnvConfig(match = 'VITE_GLOB_', confFiles = ['.env', '.env.pr
try {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
envConfig = { ...envConfig, ...env };
} catch (error) {
}
} catch (error) {}
});
Object.keys(envConfig).forEach((key) => {
const reg = new RegExp(`^(${ match })`);
const reg = new RegExp(`^(${match})`);
if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key);
}

View File

@@ -0,0 +1,35 @@
/**
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
* https://github.com/anncwb/vite-plugin-compression
*/
import type { Plugin } from 'vite';
import compressPlugin from 'vite-plugin-compression';
export function configCompressPlugin(
compress: 'gzip' | 'brotli' | 'none',
deleteOriginFile = false
): Plugin | Plugin[] {
const compressList = compress.split(',');
const plugins: Plugin[] = [];
if (compressList.includes('gzip')) {
plugins.push(
compressPlugin({
ext: '.gz',
deleteOriginFile,
})
);
}
if (compressList.includes('brotli')) {
plugins.push(
compressPlugin({
ext: '.br',
algorithm: 'brotliCompress',
deleteOriginFile,
})
);
}
return plugins;
}

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,26 +1,38 @@
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';
import { configHtmlPlugin } from './html';
import { configMockPlugin } from './mock';
import { configCompressPlugin } from './compress';
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock) {
const { VITE_USE_MOCK } = viteEnv;
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
const { 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
vitePlugins.push(configHtmlPlugin(viteEnv, isBuild));
// vite-plugin-mock
VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild, prodMock));
if (isBuild) {
// rollup-plugin-gzip
vitePlugins.push(
configCompressPlugin(VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE)
);
}
return vitePlugins;
}

View File

@@ -1,19 +0,0 @@
/**
* Mock plugin for development and production.
* https://github.com/anncwb/vite-plugin-mock
*/
import { viteMockServe } from 'vite-plugin-mock';
export function configMockPlugin(isBuild: boolean, prodMock: boolean) {
return viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: !isBuild,
prodEnabled: isBuild && prodMock,
injectCode: `
import { setupProdMockServer } from '../mock/_createProductionServer';
setupProdMockServer();
`,
});
}

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;
}

34
build/vite/proxy.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* Used to parse the .env.development proxy configuration
*/
import type { ProxyOptions } from 'vite';
type ProxyItem = [string, string];
type ProxyList = ProxyItem[];
type ProxyTargetList = Record<string, ProxyOptions & { rewrite: (path: string) => string }>;
const httpsRE = /^https:\/\//;
/**
* Generate proxy
* @param list
*/
export function createProxy(list: ProxyList = []) {
const ret: ProxyTargetList = {};
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target);
// https://github.com/http-party/node-http-proxy#options
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
// https is require secure=false
...(isHttps ? { secure: false } : {}),
};
}
return ret;
}

View File

@@ -1,57 +0,0 @@
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
parserPreset: {
parserOpts: {
headerPattern: /^(\w*|[\u4e00-\u9fa5]*)(?:[\(\](.*)[\)\])?[\:\] (.*)/,
headerCorrespondence: ['type', 'scope', 'subject'],
referenceActions: [
'close',
'closes',
'closed',
'fix',
'fixes',
'fixed',
'resolve',
'resolves',
'resolved'
],
issuePrefixes: ['#'],
noteKeywords: ['BREAKING CHANGE'],
fieldPattern: /^-(.*?)-$/,
revertPattern: /^Revert\s"([\s\S]*)"\s*This reverts commit (\w*)\./,
revertCorrespondence: ['header', 'hash'],
warn() {},
mergePattern: null,
mergeCorrespondence: null
}
},
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release'
]
]
}
}

View File

@@ -1,25 +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,18 +0,0 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
const modules = import.meta.globEager('./**/*.ts');
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return;
}
mockModules.push(...modules[key].default);
});
/**
* Used in a production environment. Need to manually import all modules
*/
export function setupProdMockServer() {
createProdMockServer(mockModules);
}

View File

@@ -1,4 +1,4 @@
import Mock from 'mockjs'
import Mock from 'mockjs';
export function resultSuccess(result, { message = 'ok' } = {}) {
return Mock.mock({
@@ -50,10 +50,10 @@ export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]
* @param {Number} times 回调函数需要执行的次数
* @param {Function} callback 回调函数
*/
export function doCustomTimes (times:number, callback:any) {
let i = -1
export function doCustomTimes(times: number, callback: any) {
let i = -1;
while (++i < times) {
callback(i)
callback(i);
}
}

View File

@@ -1,47 +1,44 @@
import { Random } from 'mockjs'
import { resultSuccess } from '../_util'
import { defineMock } from '@alova/mock';
import { faker } from '@faker-js/faker';
import { resultSuccess } from '../_util';
const consoleInfo = {
//访问量
visits:{
dayVisits:Random.float(10000,99999,2,2),
rise:Random.float(10,99),
decline:Random.float(10,99),
amount:Random.float(99999,999999,3,5),
},
//销售额
saleroom:{
weekSaleroom:Random.float(10000,99999,2,2),
amount:Random.float(99999,999999,2,2),
degree:Random.float(10,99)
},
//订单量
orderLarge:{
weekLarge:Random.float(10000,99999,2,2),
rise:Random.float(10,99),
decline:Random.float(10,99),
amount:Random.float(99999,999999,2,2),
},
//成交额度
volume:{
weekLarge:Random.float(10000,99999,2,2),
rise:Random.float(10,99),
decline:Random.float(10,99),
amount:Random.float(99999,999999,2,2)
},
function getRandom(options) {
return Number(faker.commerce.price(options));
}
const result = {
//访问量
visits: {
dayVisits: getRandom({ min: 10000, max: 99999, dec: 2 }),
rise: getRandom({ min: 10000, max: 99999, dec: 0 }),
decline: getRandom({ min: 10000, max: 99999, dec: 0 }),
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
},
//销售额
saleroom: {
weekSaleroom: getRandom({ min: 10000, max: 99999, dec: 2 }),
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
degree: getRandom({ min: 10000, max: 99999, dec: 0 }),
},
//订单量
orderLarge: {
weekLarge: getRandom({ min: 10000, max: 99999, dec: 2 }),
rise: getRandom({ min: 10000, max: 99999, dec: 0 }),
decline: getRandom({ min: 10000, max: 99999, dec: 0 }),
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
},
//成交额度
volume: {
weekLarge: getRandom({ min: 10000, max: 99999, dec: 2 }),
rise: getRandom({ min: 10000, max: 99999, dec: 0 }),
decline: getRandom({ min: 10000, max: 99999, dec: 0 }),
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
},
};
export default [
//主控台 卡片数据
{
url: '/api/dashboard/console',
timeout: 1000,
method: 'get',
response: () => {
return resultSuccess(consoleInfo);
},
}
]
export default defineMock({
// 主控台数据
'/api/dashboard/console': () => {
return resultSuccess(result);
},
});

96
mock/system/menu.ts Normal file
View File

@@ -0,0 +1,96 @@
import { defineMock } from '@alova/mock';
import { resultSuccess } from '../_util';
export interface ListDate {
label: string;
key: string;
type: number;
subtitle: string;
openType: number;
auth: string;
path: string;
children?: ListDate[];
}
const menuList = () => {
const result: ListDate[] = [
{
label: 'Dashboard',
key: 'dashboard',
type: 1,
subtitle: 'dashboard',
openType: 1,
auth: 'dashboard',
path: '/dashboard',
children: [
{
label: '主控台',
key: 'console',
type: 1,
subtitle: 'console',
openType: 1,
auth: 'console',
path: '/dashboard/console',
},
{
label: '工作台',
key: 'workplace',
type: 1,
subtitle: 'workplace',
openType: 1,
auth: 'workplace',
path: '/dashboard/workplace',
},
],
},
{
label: '表单管理',
key: 'form',
type: 1,
subtitle: 'form',
openType: 1,
auth: 'form',
path: '/form',
children: [
{
label: '基础表单',
key: 'basic-form',
type: 1,
subtitle: 'basic-form',
openType: 1,
auth: 'basic-form',
path: '/form/basic-form',
},
{
label: '分步表单',
key: 'step-form',
type: 1,
subtitle: 'step-form',
openType: 1,
auth: 'step-form',
path: '/form/step-form',
},
{
label: '表单详情',
key: 'detail',
type: 1,
subtitle: 'detail',
openType: 1,
auth: 'detail',
path: '/form/detail',
},
],
},
];
return result;
};
export default defineMock({
'/api/menu/list': () => {
const list = menuList();
return resultSuccess({
list,
});
},
});

45
mock/system/role.ts Normal file
View File

@@ -0,0 +1,45 @@
import { defineMock } from '@alova/mock';
import { faker } from '@faker-js/faker';
import { resultSuccess, doCustomTimes } from '../_util';
import dayjs from 'dayjs';
function getMenuKeys() {
const keys = ['dashboard', 'console', 'workplace', 'basic-form', 'step-form', 'detail'];
const newKeys = [];
doCustomTimes(parseInt(Math.random() * 6), () => {
const key = keys[Math.floor(Math.random() * keys.length)];
newKeys.push(key as never);
});
return Array.from(new Set(newKeys));
}
const roleList = (pageSize) => {
const result: any[] = [];
doCustomTimes(pageSize, () => {
result.push({
id: faker.string.numeric(4),
name: faker.person.firstName(),
explain: faker.lorem.sentence({ min: 2, max: 4 }),
isDefault: faker.helpers.arrayElement([true, false]),
menu_keys: getMenuKeys(),
create_date: dayjs(faker.date.anytime()).format('YYYY-MM-DD HH:mm'),
status: faker.helpers.arrayElement(['normal', 'enable', 'disable']),
});
});
return result;
};
export default defineMock({
'/api/role/list': ({ query }) => {
const { page = 1, pageSize = 10, name } = query;
const list = roleList(Number(pageSize));
// 并非真实,只是为了模拟搜索结果
const count = name ? 30 : 60;
return resultSuccess({
page: Number(page),
pageSize: Number(pageSize),
pageCount: count,
itemCount: count * Number(pageSize),
list,
});
},
});

View File

@@ -1,44 +1,39 @@
import { Random } from 'mockjs'
import { resultSuccess, doCustomTimes, resultPageSuccess } from '../_util'
const tableList = ((pageSize) => {
const result:any[] = []
doCustomTimes(pageSize,()=> {
import { defineMock } from '@alova/mock';
import { faker } from '@faker-js/faker';
import { doCustomTimes, resultSuccess } from '../_util';
import dayjs from 'dayjs';
function tableList(pageSize: number) {
const result: any[] = [];
doCustomTimes(pageSize, () => {
result.push({
id: '@integer(10,100)',
beginTime: '@datetime',
endTime: '@datetime',
address: '@city()',
name: '@cname()',
avatar: Random.image('400x400', Random.color(), Random.color(), Random.first()),
date: `@date('yyyy-MM-dd')`,
time: `@time('HH:mm')`,
'no|100000-10000000': 100000,
'status|1': ['normal', 'enable', 'disable'],
id: faker.string.numeric(4),
name: faker.person.firstName(),
sex: faker.person.sexType(),
avatar: `https://picsum.photos/200/200?v=${faker.string.numeric(4)}`,
email: faker.internet.email({ firstName: 'admin' }),
city: faker.location.city(),
status: faker.helpers.arrayElement(['close', 'refuse', 'pass']),
type: faker.helpers.arrayElement(['person', 'company']),
// createDate: faker.helpers.arrayElement(dateStrs),
createDate: dayjs(faker.date.anytime()).format('YYYY-MM-DD HH:mm'),
});
})
return result
});
return result;
}
export default defineMock({
// 表格数据列表
'/api/table/list': ({ 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: count,
itemCount: count * Number(pageSize),
list,
});
},
});
export default [
//表格数据列表
{
url: '/api/table/list',
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query;
const list = tableList(Number(pageSize))
return resultSuccess({
page:Number(page),
pageSize:Number(pageSize),
pageCount: 60,
list
}
);
},
}
]

44
mock/user/index.ts Normal file
View File

@@ -0,0 +1,44 @@
import Mock from 'mockjs';
import { resultSuccess } from '../_util';
import { defineMock } from '@alova/mock';
const Random = Mock.Random;
const token = Random.string('upper', 32, 32);
const adminInfo = {
userId: '1',
username: 'admin',
realName: 'Admin',
avatar: Random.image(),
desc: 'manager',
password: Random.string('upper', 4, 16),
token,
permissions: [
{
label: '主控台',
value: 'dashboard_console',
},
{
label: '监控页',
value: 'dashboard_monitor',
},
{
label: '工作台',
value: 'dashboard_workplace',
},
{
label: '基础列表',
value: 'basic_list',
},
{
label: '基础列表删除',
value: 'basic_list_delete',
},
],
};
export default defineMock({
'[POST]/api/login': () => resultSuccess({ token }),
'/api/admin_info': () => resultSuccess(adminInfo),
});

View File

@@ -1,53 +1,46 @@
import { MockMethod } from 'vite-plugin-mock'
import { resultSuccess, getRequestToken } from '../_util'
const menusList = [
{
path: '/dashboard',
name: 'Dashboard',
component: 'Layout',
redirect: '/dashboard/console',
meta: {
icon: 'DashboardOutlined',
title: 'Dashboard',
},
children: [
{
path: 'console',
name: 'dashboard_console',
component: 'DashboardConsole',
meta: {
title: '主控台',
}
},
{
path: 'monitor',
name: 'dashboard_monitor',
component: 'DashboardMonitor',
meta: {
title: '监控页',
}
},
{
path: 'workplace',
name: 'dashboard_workplace',
component: 'DashboardWorkplace',
meta: {
hidden: true,
title: '工作台',
}
},
],
}
]
export default [
{
url: '/api/menus',
timeout: 1000,
method: 'get',
response: () => {
return resultSuccess(menusList);
},
}
]
import { defineMock } from '@alova/mock';
import { resultSuccess } from '../_util';
const menusList = [
{
path: '/dashboard',
name: 'Dashboard',
component: 'LAYOUT',
redirect: '/dashboard/console',
meta: {
icon: 'DashboardOutlined',
title: 'Dashboard',
},
children: [
{
path: 'console',
name: 'dashboard_console',
component: '/dashboard/console/console',
meta: {
title: '主控台',
},
},
{
path: 'monitor',
name: 'dashboard_monitor',
component: '/dashboard/monitor/monitor',
meta: {
title: '监控页',
},
},
{
path: 'workplace',
name: 'dashboard_workplace',
component: '/dashboard/workplace/workplace',
meta: {
hidden: true,
title: '工作台',
},
},
],
},
];
export default defineMock({
'/api/menus': () => resultSuccess(menusList),
});

View File

@@ -1,51 +0,0 @@
import Mock from 'mockjs'
import { resultSuccess, getRequestToken } from '../_util'
const Random = Mock.Random
const token = Random.string('upper', 32, 32)
const adminInfo = {
userId: '1',
username: 'admin',
realName: 'Admin',
avatar: Random.image(),
desc: 'manager',
password: Random.string('upper', 4, 16),
token,
roles: [
{
roleName: '主控台',
value: 'dashboard_console',
},
{
roleName: '监控页',
value: 'dashboard_monitor',
},
{
roleName: '工作台',
value: 'dashboard_workplace',
}
],
}
export default [
{
url: '/api/login',
timeout: 1000,
method: 'post',
response: () => {
return resultSuccess({ token });
},
},
{
url: '/api/admin_info',
timeout: 1000,
method: 'get',
response: () => {
// const token = getRequestToken(request);
// if (!token) return resultError('Invalid token');
return resultSuccess(adminInfo);
},
},
]

View File

@@ -1,6 +1,6 @@
{
"name": "naive-ui-admin",
"version": "1.2",
"version": "2.0.0",
"author": {
"name": "Ahjung",
"email": "735878602@qq.com",
@@ -8,83 +8,92 @@
},
"private": true,
"scripts": {
"bootstrap": "pnpm install",
"serve": "pnpm run dev",
"dev": "vite",
"build": "vite build && esno ./build/script/postBuild.ts",
"preview": "vite preview",
"build typecheck": "vuedx-typecheck . && vite build",
"build:no-cache": "pnpm clean:cache && pnpm run build",
"report": "cross-env REPORT=true pnpm run build",
"preview": "pnpm run build && vite preview",
"preview:dist": "vite preview",
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
"clean:lib": "rimraf node_modules",
"deploy": "gh-pages -d dist",
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged",
"test prod gzip": "http-server dist --cors --gzip -c-1"
"lint:pretty": "pretty-quick --staged"
},
"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",
"@alova/mock": "^2.0.6",
"@vicons/antd": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^9.13.0",
"alova": "^3.0.16",
"date-fns": "^2.30.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"element-resize-detector": "^1.2.4",
"lodash-es": "^4.17.21",
"makeit-captcha": "^1.2.5",
"mitt": "^2.1.0",
"mockjs": "^1.1.0",
"naive-ui": "^2.15.5",
"nprogress": "^1.0.0-1",
"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.39.0",
"pinia": "^2.2.2",
"qs": "^6.13.0",
"vue": "^3.5.5",
"vue-router": "^4.4.5",
"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-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.11.1",
"esno": "^0.7.3",
"gh-pages": "^3.2.0",
"husky": "^6.0.0",
"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",
"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.4",
"typescript": "^4.3.2",
"vite": "^2.4.2",
"vite-plugin-html": "^2.0.7",
"vite-plugin-mock": "^2.9.1",
"vite-plugin-style-import": "^1.0.1",
"vue-eslint-parser": "^7.8.0"
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@faker-js/faker": "^9.0.0",
"@types/lodash": "^4.17.7",
"@types/node": "^18.19.50",
"@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.5.5",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.20",
"commitizen": "^4.3.0",
"core-js": "^3.38.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.28.0",
"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.45",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"rimraf": "^3.0.2",
"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.11",
"typescript": "^4.9.5",
"unplugin-vue-components": "^0.22.12",
"vite": "^5.4.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-style-import": "^2.0.0",
"vue-demi": "^0.13.11",
"vue-draggable-next": "^2.2.1",
"vue-eslint-parser": "^9.4.3",
"vuedraggable": "^4.1.0"
},
"lint-staged": {
"*.{vue,js,ts,tsx}": "eslint --fix"
@@ -114,6 +123,6 @@
},
"homepage": "https://github.com/jekip/naive-ui-admin",
"engines": {
"node": "^12 || >=14"
"node": ">=16"
}
}

9042
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

@@ -1,105 +1,83 @@
<template>
<NConfigProvider
v-if="!isLock"
:locale="zhCN"
:theme="getDarkTheme"
:theme-overrides="getThemeOverrides"
:date-locale="dateZhCN"
v-if="!isLock"
:locale="zhCN"
:theme="getDarkTheme"
:theme-overrides="getThemeOverrides"
:date-locale="dateZhCN"
>
<AppProvider>
<RouterView/>
<RouterView />
</AppProvider>
</NConfigProvider>
<transition v-if="isLock && $route.name != 'login'" name="slide-up">
<LockScreen/>
<transition v-if="isLock && $route.name !== 'login'" name="slide-up">
<LockScreen />
</transition>
</template>
<script lang="ts">
import { defineComponent, computed, onMounted, onUnmounted } from 'vue'
import { zhCN, dateZhCN, createTheme, inputDark, datePickerDark, darkTheme } from 'naive-ui'
import { LockScreen } from '@/components/Lockscreen'
import { AppProvider } from '@/components/Application'
import { useLockscreenStore } from '@/store/modules/lockscreen'
import { useRoute } from 'vue-router'
import { useDesignSettingStore } from '@/store/modules/designSetting'
<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 { 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)
/**
* @type import('naive-ui').GlobalThemeOverrides
*/
const getThemeOverrides = computed(() => {
return {
common: {
primaryColor: designStore.appTheme,
primaryColorHover: '#57a3f3'
}
}
})
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined))
let 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)
}
onMounted(() => {
document.addEventListener('mousedown', timekeeping)
})
onUnmounted(() => {
document.removeEventListener('mousedown', timekeeping)
})
const route = useRoute();
const useScreenLock = useScreenLockStore();
const designStore = useDesignSettingStore();
const isLock = computed(() => useScreenLock.isLocked);
const lockTime = computed(() => useScreenLock.lockTime);
/**
* @type import('naive-ui').GlobalThemeOverrides
*/
const getThemeOverrides = computed(() => {
const appTheme = designStore.appTheme;
const lightenStr = lighten(designStore.appTheme, 6);
return {
darkTheme: createTheme([inputDark, datePickerDark]),
getDarkTheme,
zhCN,
dateZhCN,
isLock,
getThemeOverrides
}
}
})
common: {
primaryColor: appTheme,
primaryColorHover: lightenStr,
primaryColorPressed: lightenStr,
primaryColorSuppl: appTheme,
},
LoadingBar: {
colorLoading: appTheme,
},
};
});
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined));
let timer: NodeJS.Timer;
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);
});
onUnmounted(() => {
document.removeEventListener('mousedown', timekeeping);
});
</script>
<style lang="less">
@import 'styles/global.less';
@import 'styles/common.less';
@import 'styles/override.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,11 +1,35 @@
import http from '@/utils/http/axios'
import { Alova } from '@/utils/http/alova/index';
export interface TypeVisits {
dayVisits: number;
rise: number;
decline: number;
amount: number;
}
export interface TypeSaleroom {
weekSaleroom: number;
amount: number;
degree: number;
}
export interface TypeOrderLarge {
weekLarge: number;
rise: number;
decline: number;
amount: number;
}
export interface TypeConsole {
visits: TypeVisits;
//销售额
saleroom: TypeSaleroom;
//订单量
orderLarge: TypeOrderLarge;
//成交额度
volume: TypeOrderLarge;
}
//获取主控台信息
export function getConsoleInfo() {
return http.request(
{
url: '/dashboard/console',
method: 'get'
}
)
return Alova.Get<TypeConsole>('/dashboard/console');
}

View File

@@ -1,33 +1,19 @@
import http from '@/utils/http/axios'
import {
GetByUserIdParams,
GetMenuListByUserIdResult,
GetAuthCodeByUserIdResult
} from './model/menuModel'
enum Api {
adminMenus = '/menus',
GetBtnCodeListByUserId = '/getBtnCodeListByUserId'
}
import { Alova } from '@/utils/http/alova/index';
import { ListDate } from 'mock/system/menu';
/**
* @description: 根据用户id获取用户菜单
*/
export function adminMenus() {
return http.request<GetMenuListByUserIdResult>({
url: Api.adminMenus,
method: 'GET'
})
return Alova.Get('/menus');
}
/**
* 根据用户Id获取权限编码
* 获取tree菜单列表
* @param params
*/
export function getBtnCodeListByUserId(params: GetByUserIdParams) {
return http.request<GetAuthCodeByUserIdResult>({
url: Api.GetBtnCodeListByUserId,
method: 'GET',
params
})
export function getMenuList(params?) {
return Alova.Get<{ list: ListDate[] }>('/menu/list', {
params,
});
}

8
src/api/system/role.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Alova } from '@/utils/http/alova/index';
/**
* @description: 角色列表
*/
export function getRoleList(params) {
return Alova.Get('/role/list', { params });
}

View File

@@ -1,68 +1,45 @@
import http from '@/utils/http/axios'
export interface BasicResponseModel<T = any> {
code: number
message: string
result: T
}
export interface BasicPageParams {
pageNumber: number
pageSize: number
total: number
}
import { Alova } from '@/utils/http/alova/index';
/**
* @description: 获取用户信息
*/
export function getUserInfo() {
return http.request(
{
url: '/admin_info',
method: 'get'
}
)
return Alova.Get<InResult>('/admin_info', {
meta: {
isReturnNativeResponse: true,
},
});
}
/**
* @description: 用户登录
*/
export function login(params) {
return http.request<BasicResponseModel>(
{
url: '/login',
method: 'POST',
params
},
{
isTransformRequestResult: false
}
)
return Alova.Post<InResult>(
'/login',
{
params,
},
{
meta: {
isReturnNativeResponse: true,
},
}
);
}
/**
* @description: 用户修改密码
*/
export function changePassword(params, uid) {
return http.request(
{
url: `/user/u${ uid }/changepw`,
method: 'POST',
params
},
{
isTransformRequestResult: false
}
)
return Alova.Post(`/user/u${uid}/changepw`, { params });
}
/**
* @description: 用户登出
*/
export function logout(params) {
return http.request({
url: '/login/logout',
method: 'POST',
params
})
return Alova.Post('/login/logout', {
params,
});
}

View File

@@ -1,12 +1,6 @@
import http from '@/utils/http/axios'
import { Alova } from '@/utils/http/alova/index';
//获取table
export function getTableList(params) {
return http.request(
{
url: '/table/list',
method: 'get',
params
}
)
return Alova.Get('/table/list', { 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,25 +1,26 @@
<template>
<n-dialog-provider>
<DialogContent/>
<n-notification-provider>
<n-message-provider>
<MessageContent/>
<slot slot="default"></slot>
<slot name="default"></slot>
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { MessageContent } from '@/components/MessageContent'
import { DialogContent } from '@/components/DialogContent'
import { defineComponent } from 'vue';
import { NDialogProvider, NNotificationProvider, NMessageProvider } from 'naive-ui';
export default defineComponent({
name: 'Application',
components: { MessageContent, DialogContent },
setup() {
return {}
}
})
export default defineComponent({
name: 'Application',
components: {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
},
setup() {
return {};
},
});
</script>

View File

@@ -1,3 +1,3 @@
import AppProvider from './Application.vue'
import AppProvider from './Application.vue';
export { AppProvider }
export { AppProvider };

View File

@@ -3,108 +3,107 @@
{{ value }}
</span>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
import { useTransition, TransitionPresets } from '@vueuse/core';
import { isNumber } from '@/utils/is';
<script lang="ts" setup>
import { ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
import { useTransition, TransitionPresets } from '@vueuse/core';
import { isNumber } from '@/utils/is';
const props = {
startVal: { type: Number, default: 0 },
endVal: { type: Number, default: 2021 },
duration: { type: Number, default: 1500 },
autoplay: { type: Boolean, default: true },
decimals: {
type: Number,
default: 0,
validator(value: number) {
return value >= 0;
defineOptions({ name: 'CountTo' });
const props = defineProps({
startVal: { type: Number, default: 0 },
endVal: { type: Number, default: 2021 },
duration: { type: Number, default: 1500 },
autoplay: { type: Boolean, default: true },
decimals: {
type: Number,
default: 0,
validator(value: number) {
return value >= 0;
},
},
},
prefix: { type: String, default: '' },
suffix: { type: String, default: '' },
separator: { type: String, default: ',' },
decimal: { type: String, default: '.' },
/**
* font color
*/
color: { type: String },
/**
* Turn on digital animation
*/
useEasing: { type: Boolean, default: true },
/**
* Digital animation
*/
transition: { type: String, default: 'linear' },
};
prefix: { type: String, default: '' },
suffix: { type: String, default: '' },
separator: { type: String, default: ',' },
decimal: { type: String, default: '.' },
/**
* font color
*/
color: { type: String },
/**
* Turn on digital animation
*/
useEasing: { type: Boolean, default: true },
/**
* Digital animation
*/
transition: { type: String, default: 'linear' },
});
export default defineComponent({
name: 'CountTo',
props,
emits: ['onStarted', 'onFinished'],
setup(props, { emit }) {
const source = ref(props.startVal);
const disabled = ref(false);
let outputValue = useTransition(source);
const emit = defineEmits(['onStarted', 'onFinished']);
const value = computed(() => formatNumber(unref(outputValue)));
const source = ref(props.startVal);
const disabled = ref(false);
let outputValue = useTransition(source);
watchEffect(() => {
source.value = props.startVal;
const value = computed(() => formatNumber(unref(outputValue)));
watchEffect(() => {
source.value = props.startVal;
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
onMounted(() => {
props.autoplay && start();
});
function start() {
run();
source.value = props.endVal;
}
function reset() {
source.value = props.startVal;
run();
}
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => emit('onFinished'),
onStarted: () => emit('onStarted'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
});
}
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
function formatNumber(num: number | string) {
if (!num && num !== 0) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
});
onMounted(() => {
props.autoplay && start();
});
function start() {
run();
source.value = props.endVal;
}
return prefix + x1 + x2 + suffix;
}
function reset() {
source.value = props.startVal;
run();
}
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => emit('onFinished'),
onStarted: () => emit('onStarted'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
});
}
function formatNumber(num: number | string) {
if (!num) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
}
return prefix + x1 + x2 + suffix;
}
return { value, start, reset };
},
});
defineExpose({
reset,
});
</script>

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,29 +1,28 @@
<template>
<div
:class="{ onLockLogin: showLogin }"
class="lockscreen"
@keyup="onLockLogin(true)"
@mousedown.stop
@contextmenu.prevent
:class="{ onLockLogin: showLogin }"
class="lockscreen"
@keyup="onLockLogin(true)"
@mousedown.stop
@contextmenu.prevent
>
<template v-if="!showLogin">
<div class="lock-box">
<div class="lock">
<span class="lock-icon" title="解锁屏幕" @click="onLockLogin(true)">
<n-icon>
<lock-outlined/>
<lock-outlined />
</n-icon>
</span>
</div>
</div>
<!--充电-->
<recharge
:battery="battery"
:battery-status="batteryStatus"
:calc-discharging-time="calcDischargingTime"
></recharge>
:battery="battery"
:battery-status="batteryStatus"
:calc-discharging-time="calcDischargingTime"
:calc-charging-time="calcChargingTime"
/>
<div class="local-time">
<div class="time">{{ hour }}:{{ minute }}</div>
@@ -31,9 +30,9 @@
</div>
<div class="computer-status">
<span :class="{ offline: !online }" class="network">
<wifi-outlined class="network"/>
<wifi-outlined class="network" />
</span>
<api-outlined/>
<api-outlined />
</div>
</template>
@@ -42,263 +41,264 @@
<div class="login-box">
<n-avatar :size="128">
<n-icon>
<user-outlined/>
<user-outlined />
</n-icon>
</n-avatar>
<div class="username">{{ loginParams.username }}</div>
<n-input
type="password"
autofocus
v-model:value="loginParams.password"
placeholder="请输入登录密码">
type="password"
autofocus
v-model:value="loginParams.password"
@keyup.enter="onLogin"
placeholder="请输入登录密码"
>
<template #suffix>
<n-icon @click="onLogin" style="cursor: pointer;">
<LoadingOutlined v-if="loginLoading"/>
<arrow-right-outlined v-else/>
<n-icon @click="onLogin" style="cursor: pointer">
<LoadingOutlined v-if="loginLoading" />
<arrow-right-outlined v-else />
</n-icon>
</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><a @click="showLogin=false">返回</a></div>
<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>
</div>
</div>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, reactive, toRefs, computed } from 'vue'
import { ResultEnum } from '@/enums/httpEnum'
import recharge from './Recharge.vue'
import {
LockOutlined,
LoadingOutlined,
UnlockOutlined,
UserOutlined,
ApiOutlined,
ArrowRightOutlined,
WifiOutlined,
} from '@vicons/antd'
import { useRouter, useRoute } from 'vue-router'
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'
export default defineComponent({
name: 'Lockscreen',
components: {
import { defineComponent, reactive, toRefs } from 'vue';
import { ResultEnum } from '@/enums/httpEnum';
import recharge from './Recharge.vue';
import {
LockOutlined,
LoadingOutlined,
UnlockOutlined,
UserOutlined,
ArrowRightOutlined,
ApiOutlined,
ArrowRightOutlined,
WifiOutlined,
recharge,
},
setup(props, { emit }) {
const useLockscreen = useLockscreenStore()
const userStore = useUserStore();
} from '@vicons/antd';
// 获取时间
const { month, day, hour, minute, second, week } = useTime()
const { online } = useOnline()
import { useRouter, useRoute } from 'vue-router';
import { useOnline } from '@/hooks/useOnline';
import { useTime } from '@/hooks/useTime';
import { useBattery } from '@/hooks/useBattery';
import { useScreenLockStore } from '@/store/modules/screenLock';
import { UserInfoType, useUserStore } from '@/store/modules/user';
const router = useRouter()
const route = useRoute()
export default defineComponent({
name: 'ScreenLock',
components: {
LockOutlined,
LoadingOutlined,
UserOutlined,
ArrowRightOutlined,
ApiOutlined,
WifiOutlined,
recharge,
},
setup() {
const useScreenLock = useScreenLockStore();
const userStore = useUserStore();
const { battery, batteryStatus, calcDischargingTime } = useBattery()
const { username } = userStore.getUserInfo || {}
const state = reactive({
showLogin: false,
loginLoading: false, // 正在登录
isLoginError: false, //密码错误
errorMsg: '密码错误',
loginParams: {
username: username || '',
password: ''
}
})
// 获取时间
const { month, day, hour, minute, second, week } = useTime();
const { online } = useOnline();
// 解锁登录
const onLockLogin = (value: boolean) => (state.showLogin = value)
const router = useRouter();
const route = useRoute();
// 登录
const onLogin = async () => {
if (!state.loginParams.password.trim()) {
return
}
const params = {
isLock: true,
...state.loginParams
}
state.loginLoading = true
const { code, result, message } = await userStore.login(params)
if (code === ResultEnum.SUCCESS) {
onLockLogin(false)
useLockscreen.setLock(false)
} else {
state.errorMsg = message
state.isLoginError = true
}
state.loginLoading = false
}
const { battery, batteryStatus, calcDischargingTime, calcChargingTime } = useBattery();
const userInfo: UserInfoType = userStore.getUserInfo || {};
const username = userInfo['username'] || '';
const state = reactive({
showLogin: false,
loginLoading: false, // 正在登录
isLoginError: false, //密码错误
errorMsg: '密码错误',
loginParams: {
username: username || '',
password: '',
},
});
//重新登录
const goLogin = () => {
onLockLogin(false)
useLockscreen.setLock(false)
router.replace({
path: '/login',
query: {
redirect: route.fullPath
// 解锁登录
const onLockLogin = (value: boolean) => (state.showLogin = value);
// 登录
const onLogin = async () => {
if (!state.loginParams.password.trim()) {
return;
}
})
}
const params = {
isLock: true,
...state.loginParams,
};
state.loginLoading = true;
const { code, message } = await userStore.login(params);
if (code === ResultEnum.SUCCESS) {
onLockLogin(false);
useScreenLock.setLock(false);
} else {
state.errorMsg = message;
state.isLoginError = true;
}
state.loginLoading = false;
};
return {
...toRefs(state),
online,
month,
day,
hour,
minute,
second,
week,
battery,
batteryStatus,
calcDischargingTime,
onLockLogin,
onLogin,
goLogin
}
}
})
//重新登录
const goLogin = () => {
onLockLogin(false);
useScreenLock.setLock(false);
router.replace({
path: '/login',
query: {
redirect: route.fullPath,
},
});
};
return {
...toRefs(state),
online,
month,
day,
hour,
minute,
second,
week,
battery,
batteryStatus,
calcDischargingTime,
calcChargingTime,
onLockLogin,
onLogin,
goLogin,
};
},
});
</script>
<style lang="less" scoped>
.lockscreen {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
background: #000;
color: white;
overflow: hidden;
z-index: 9999;
&.onLockLogin {
background-color: rgba(25, 28, 34, 0.88);
backdrop-filter: blur(7px);
}
.login-box {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
.lockscreen {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #000;
color: white;
overflow: hidden;
z-index: 9999;
> * {
margin-bottom: 14px;
&.onLockLogin {
background-color: rgba(25, 28, 34, 0.88);
backdrop-filter: blur(7px);
}
.username {
font-size: 30px;
}
}
.lock-box {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 34px;
z-index: 100;
.tips {
color: white;
cursor: text;
}
.lock {
.login-box {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.lock-icon {
cursor: pointer;
> * {
margin-bottom: 14px;
}
.anticon-unlock {
display: none;
.username {
font-size: 30px;
}
}
.lock-box {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 34px;
z-index: 100;
.tips {
color: white;
cursor: text;
}
.lock {
display: flex;
justify-content: center;
.lock-icon {
cursor: pointer;
.anticon-unlock {
display: none;
}
&:hover .anticon-unlock {
display: initial;
}
&:hover .anticon-lock {
display: none;
}
}
}
}
&:hover .anticon-unlock {
display: initial;
}
.local-time {
position: absolute;
bottom: 60px;
left: 60px;
font-family: helvetica;
&:hover .anticon-lock {
display: none;
.time {
font-size: 70px;
}
.date {
font-size: 40px;
}
}
.computer-status {
position: absolute;
bottom: 60px;
right: 60px;
font-size: 24px;
> * {
margin-left: 14px;
}
.network {
position: relative;
&.offline::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 2px;
height: 28px;
transform: translate(-50%, -50%) rotate(45deg);
background-color: red;
z-index: 10;
}
}
}
}
.local-time {
position: absolute;
bottom: 60px;
left: 60px;
font-family: helvetica;
.time {
font-size: 70px;
}
.date {
font-size: 40px;
}
}
.computer-status {
position: absolute;
bottom: 60px;
right: 60px;
font-size: 24px;
> * {
margin-left: 14px;
}
.network {
position: relative;
&.offline::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 2px;
height: 28px;
transform: translate(-50%, -50%) rotate(45deg);
background-color: red;
z-index: 10;
}
}
}
}
</style>

View File

@@ -13,165 +13,152 @@
剩余可使用时间{{ calcDischargingTime }}
</div>
<span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0">
距离电池充满需要{{ calcDischargingTime }}
距离电池充满需要{{ calcChargingTime }}
</span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { defineComponent } from 'vue';
export default defineComponent({
name: 'HuaweiCharge',
// props: ['batteryStatus', 'battery', 'calcDischargingTime'],
props: {
battery: {
// 电池对象
type: Object,
default: () => ({})
export default defineComponent({
name: 'HuaweiCharge',
// props: ['batteryStatus', 'battery', 'calcDischargingTime'],
props: {
battery: {
// 电池对象
type: Object,
default: () => ({}),
},
calcDischargingTime: {
// 电池剩余时间可用时间
type: String,
default: '',
},
calcChargingTime: {
type: String,
default: '',
},
batteryStatus: {
// 电池状态
type: String,
validator: (val: string) => ['充电中', '已充满', '已断开电源'].includes(val),
},
},
calcDischargingTime: {
// 电池剩余时间可用时间
type: String,
default: ''
},
batteryStatus: {
// 电池状态
type: String,
validator: (val: string) => ['充电中', '已充满', '已断开电源'].includes(val)
}
}
})
});
</script>
<style lang="less" scoped>
.container {
position: absolute;
bottom: 20vh;
left: 50vw;
width: 300px;
height: 400px;
transform: translateX(-50%);
.number {
.container {
position: absolute;
top: 27%;
z-index: 10;
bottom: 20vh;
left: 50vw;
width: 300px;
font-size: 32px;
color: #fff;
text-align: center;
}
height: 500px;
transform: translateX(-50%);
.contrast {
width: 300px;
height: 400px;
overflow: hidden;
background-color: #000;
filter: contrast(15) hue-rotate(0);
animation: hueRotate 10s infinite linear;
.circle {
position: relative;
width: 300px;
height: 300px;
filter: blur(8px);
box-sizing: border-box;
&::after {
position: absolute;
top: 40%;
left: 50%;
width: 200px;
height: 200px;
background-color: #00ff6f;
border-radius: 42% 38% 62% 49% / 45%;
content: '';
transform: translate(-50%, -50%) rotate(0);
animation: rotate 10s infinite linear;
}
&::before {
position: absolute;
top: 40%;
left: 50%;
z-index: 10;
width: 176px;
height: 176px;
background-color: #000;
border-radius: 50%;
content: '';
transform: translate(-50%, -50%);
}
}
.bubbles {
.number {
position: absolute;
bottom: 0;
left: 50%;
width: 100px;
height: 40px;
background-color: #00ff6f;
border-radius: 100px 100px 0 0;
filter: blur(5px);
transform: translate(-50%, 0);
top: 20%;
z-index: 10;
width: 300px;
font-size: 32px;
color: #fff;
text-align: center;
}
li {
position: absolute;
background: #00ff6f;
border-radius: 50%;
.contrast {
width: 300px;
height: 400px;
overflow: hidden;
background-color: #000;
filter: contrast(15) hue-rotate(0);
animation: hueRotate 10s infinite linear;
.circle {
position: relative;
width: 300px;
height: 300px;
filter: blur(8px);
box-sizing: border-box;
&::after {
position: absolute;
top: 40%;
left: 50%;
width: 200px;
height: 200px;
background-color: #00ff6f;
border-radius: 42% 38% 62% 49% / 45%;
content: '';
transform: translate(-50%, -50%) rotate(0);
animation: rotate 10s infinite linear;
}
&::before {
position: absolute;
top: 40%;
left: 50%;
z-index: 10;
width: 176px;
height: 176px;
background-color: #000;
border-radius: 50%;
content: '';
transform: translate(-50%, -50%);
}
}
.bubbles {
position: absolute;
bottom: 0;
left: 50%;
width: 100px;
height: 40px;
background-color: #00ff6f;
border-radius: 100px 100px 0 0;
filter: blur(5px);
transform: translate(-50%, 0);
li {
position: absolute;
background: #00ff6f;
border-radius: 50%;
}
}
}
.charging {
font-size: 20px;
text-align: center;
}
}
.charging {
font-size: 20px;
text-align: center;
}
}
@keyframes rotate {
50% {
border-radius: 45% / 42% 38% 58% 49%;
}
@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%;
100% {
transform: translate(-50%, -50%) rotate(720deg);
}
}
100% {
transform: translate(-50%, -50%) rotate(720deg);
}
}
@keyframes moveToTop {
90% {
opacity: 1;
}
@keyframes moveToTop {
90% {
opacity: 1;
100% {
opacity: 0.1;
transform: translate(-50%, -180px);
}
}
100% {
opacity: 0.1;
transform: translate(-50%, -180px);
@keyframes hueRotate {
100% {
filter: contrast(15) hue-rotate(360deg);
}
}
}
@keyframes hueRotate {
100% {
filter: contrast(15) hue-rotate(360deg);
}
}
</style>

View File

@@ -1,3 +1,3 @@
import LockScreen from './Lockscreen.vue'
import LockScreen from './Lockscreen.vue';
export { LockScreen }
export { LockScreen };

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,109 @@
<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);
}
defineExpose(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

@@ -1,91 +0,0 @@
BasicTable 重封装组件说明
====
封装说明
----
> 基础的使用方式与 API 与 [官方版(data-table)](https://www.naiveui.com/zh-CN/os-theme/components/data-table#tree) 本一致,在其基础上,封装了加载数据的方法。
>
> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 BasicTable 组件传递绑定 `:api="Promise"` 对象即可
>
> 例子1
----
(基础使用)
```vue
<template>
<BasicTable
title="表格列表"
:columns="columns"
:api="loadDataTable"
:row-key="row => row.id"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary">添加会员</n-button>
</template>
</BasicTable>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { BasicTable } from '@/components/Table'
import { getTableList } from '@/api/table/list'
const columns = [
{
title: 'id',
key: 'id'
},
{
title: '名称',
key: 'name'
},
{
title: '地址',
key: 'address'
},
{
title: '日期',
key: 'date'
},
]
export default defineComponent({
components: { BasicTable },
setup() {
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data
}
return {
columns,
loadDataTable
}
}
})
</script>
```
API
----
BasicTable 在 NaiveUi 的 data-table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 data-table 不同的 api。
> requestPromise 参考上面例子写法
> ref可绑定ref 调用组件内部方法data-table本身的方法和参数
Methods
----
> reloadactionRef.value.reload()
> 其余方法,请打印查看
Slots
----
> 名称tableTitle | 表格顶部左侧区域
> 名称toolbar | 表格顶部右侧区域
更新时间
----
该文档最后更新于: 2021-07-12 PM 10:13

View File

@@ -1 +1,4 @@
export { default as BasicTable } from './src/Table.vue';
export { default as TableAction } from './src/components/TableAction.vue';
export * from './src/types/table';
export * from './src/types/tableAction';

View File

@@ -1,35 +1,44 @@
<template>
<div class="table-toolbar">
<!--顶部左侧区域-->
<div class="flex items-center table-toolbar-left ">
<template v-if="title">
<div class="flex items-center table-toolbar-left">
<template v-if="props.title">
<div class="table-toolbar-left-title">
{{ title }}
<n-tooltip trigger="hover" v-if="titleTooltip">
{{ props.title }}
<n-tooltip trigger="hover" v-if="props.titleTooltip">
<template #trigger>
<n-icon size="18" class="ml-1 cursor-pointer text-gray-400">
<QuestionCircleOutlined/>
<n-icon size="18" class="ml-1 text-gray-400 cursor-pointer">
<QuestionCircleOutlined />
</n-icon>
</template>
{{ titleTooltip }}
{{ props.titleTooltip }}
</n-tooltip>
</div>
</template>
<slot name="tableTitle"></slot>
</div>
<div class="flex items-center table-toolbar-right">
<div class="flex items-center leading-none table-toolbar-right">
<!--顶部右侧区域-->
<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>
<div class="table-toolbar-right-icon" @click="reload">
<n-icon size="18">
<ReloadOutlined/>
<ReloadOutlined />
</n-icon>
</div>
</template>
@@ -40,9 +49,14 @@
<n-tooltip trigger="hover">
<template #trigger>
<div class="table-toolbar-right-icon">
<n-dropdown @select="densitySelect" trigger="click" :options="densityOptions" v-model:value="tableSize">
<n-dropdown
@select="densitySelect"
trigger="click"
:options="densityOptions"
v-model:value="tableSize"
>
<n-icon size="18">
<ColumnHeightOutlined/>
<ColumnHeightOutlined />
</n-icon>
</n-dropdown>
</div>
@@ -51,17 +65,17 @@
</n-tooltip>
<!--表格设置单独抽离成组件-->
<ColumnSetting></ColumnSetting>
<ColumnSetting />
</div>
</div>
<div class="s-table">
<n-data-table
v-bind="getBindValues"
:pagination="pagination"
@update:page="updatePage"
@update:page-size="updatePageSize"
ref="tableElRef"
v-bind="getBindValues"
:striped="isStriped"
:pagination="pagination"
@update:page="updatePage"
@update:page-size="updatePageSize"
>
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data"></slot>
@@ -70,227 +84,229 @@
</div>
</template>
<script lang="ts">
import { NDataTable } from 'naive-ui'
import { ref, defineComponent, reactive, unref, onMounted, toRaw, onBeforeMount, computed, toRefs, watch } from "vue"
import { ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, QuestionCircleOutlined } from '@vicons/antd'
import { createTableContext } from './hooks/useTableContext';
<script lang="ts" setup>
import { ref, unref, toRaw, computed, onMounted, nextTick } from 'vue';
import { ReloadOutlined, ColumnHeightOutlined, QuestionCircleOutlined } from '@vicons/antd';
import { createTableContext } from './hooks/useTableContext';
import ColumnSetting from './components/settings/ColumnSetting.vue'
import ColumnSetting from './components/settings/ColumnSetting.vue';
import { useLoading } from './hooks/useLoading';
import { useColumns } from './hooks/useColumns';
import { useDataSource } from './hooks/useDataSource';
import { usePagination } from './hooks/usePagination';
import { useLoading } from './hooks/useLoading';
import { useColumns } from './hooks/useColumns';
import { useDataSource } from './hooks/useDataSource';
import { usePagination } from './hooks/usePagination';
import { basicProps } from './props'
import { basicProps } from './props';
import { BasicTableProps } from './types/table'
import { BasicTableProps } from './types/table';
import { getViewportOffset } from '@/utils/domUtils';
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
import { isBoolean } from '@/utils/is';
const densityOptions = [
{
type: "menu",
label: '紧凑',
key: 'small',
},
{
type: "menu",
label: '默认',
key: "medium"
},
{
type: "menu",
label: '宽松',
key: 'large'
}
]
const densityOptions = [
{
type: 'menu',
label: '紧凑',
key: 'small',
},
{
type: 'menu',
label: '默认',
key: 'medium',
},
{
type: 'menu',
label: '宽松',
key: 'large',
},
];
export default defineComponent({
components: {
ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, ColumnSetting, QuestionCircleOutlined
},
props: {
...NDataTable.props, // 这里继承原 UI 组件的 props
...basicProps
},
emits: [
const emit = defineEmits([
'fetch-success',
'fetch-error',
'update:checked-row-keys'
],
setup(props, { emit }) {
'update:checked-row-keys',
'edit-end',
'edit-cancel',
'edit-row-end',
'edit-change',
]);
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
const props = defineProps({ ...basicProps });
const deviceHeight = ref(150);
const tableElRef = ref<ComponentRef>(null);
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
let paginationEl: HTMLElement | null;
const isStriped = ref(props.striped || false);
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
});
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
});
const tableSize = ref(unref(getProps as any).size || 'medium');
const { getLoading, setLoading } = useLoading(getProps);
const { getLoading, setLoading } = useLoading(getProps);
const {
const { getPaginationInfo, setPagination } = usePagination(getProps);
const { getDataSourceRef, getDataSource, getRowKey, reload } = useDataSource(
getProps,
{
getPaginationInfo,
getPagination,
setPagination,
setShowPagination,
getShowPagination,
} = usePagination(getProps)
const { getDataSourceRef, getRowKey, getDataSource, setDataSource, reload } = useDataSource(
getProps, {
getPaginationInfo,
setPagination,
tableData,
setLoading
}, emit
)
const {
getPageColumns,
setColumns,
getColumns,
getCacheColumns,
setCacheColumnsField,
getColumnsRef
} = useColumns(getProps)
const state = reactive({
tableSize: 'medium',
isColumnSetting: false
})
//页码切换
function updatePage(page) {
setPagination({ page: page, });
reload()
}
//分页数量切换
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size, });
reload()
}
//密度切换
function densitySelect(e) {
state.tableSize = e
}
//选中行
function updateCheckedRowKeys(rowKeys) {
emit('update:checked-row-keys', rowKeys)
}
//重置 Columns
const resetColumns = () => {
columns.map(item => {
item.isShow = true
})
}
//获取表格大小
const getTableSize = computed(() => state.tableSize)
//组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef);
let propsData = {
...unref(getProps),
loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)),
rowKey: unref(getRowKey),
data: tableData,
size: unref(getTableSize),
remote: true
}
return propsData
})
//获取分页信息
const pagination = computed(() => toRaw(unref(getPaginationInfo)))
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const tableAction: TableActionType = {
reload,
setColumns,
tableData,
setLoading,
setProps,
getColumns,
getPageColumns,
getCacheColumns,
setCacheColumnsField,
emit,
getSize: () => {
return unref(getBindValues).size as SizeType;
},
};
},
emit
);
createTableContext({ ...tableAction, wrapRef, getBindValues });
const { getPageColumns, setColumns, getColumns, getCacheColumns, setCacheColumnsField } =
useColumns(getProps);
return {
...toRefs(state),
getBindValues,
densityOptions,
reload,
densitySelect,
updatePage,
updatePageSize,
updateCheckedRowKeys,
pagination,
resetColumns,
tableAction
}
//页码切换
function updatePage(page) {
setPagination({ page: page });
reload();
}
})
//分页数量切换
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size });
reload();
}
//密度切换
function densitySelect(e) {
tableSize.value = e;
}
//获取表格大小
const getTableSize = computed(() => tableSize.value);
//组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef);
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
return {
...unref(getProps),
loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)),
rowKey: unref(getRowKey),
data: tableData,
size: unref(getTableSize),
remote: true,
'max-height': maxHeight,
title: '', // 重置为空 避免绑定到 table 上面
};
});
//获取分页信息
const pagination = computed(() => toRaw(unref(getPaginationInfo)));
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const setStriped = (value: boolean) => (isStriped.value = value);
const tableAction = {
reload,
setColumns,
setLoading,
setProps,
getColumns,
getDataSource,
getPageColumns,
getCacheColumns,
setCacheColumnsField,
emit,
};
const getCanResize = computed(() => {
const { canResize } = unref(getProps);
return canResize;
});
async function computeTableHeight() {
const table = unref(tableElRef);
if (!table) return;
if (!unref(getCanResize)) return;
const tableEl: any = table?.$el;
const headEl = tableEl.querySelector('.n-data-table-thead ');
const { bottomIncludeBody } = getViewportOffset(headEl);
const headerH = 64;
let paginationH = 2;
let marginH = 24;
if (!isBoolean(unref(pagination))) {
paginationEl = tableEl.querySelector('.n-data-table__pagination') as HTMLElement;
if (paginationEl) {
const offsetHeight = paginationEl.offsetHeight;
paginationH += offsetHeight || 0;
} else {
paginationH += 28;
}
}
let height =
bottomIncludeBody - (headerH + paginationH + marginH + (props.resizeHeightOffset || 0));
const maxHeight = props.maxHeight;
height = maxHeight && maxHeight < height ? maxHeight : height;
deviceHeight.value = height;
}
useWindowSizeFn(computeTableHeight, 280);
onMounted(() => {
nextTick(() => {
computeTableHeight();
});
});
createTableContext({ ...tableAction, wrapRef, getBindValues });
defineExpose(tableAction);
</script>
<style lang='less' scoped>
.table-toolbar {
display: flex;
justify-content: space-between;
padding: 0 0 16px 0;
&-left {
<style lang="less" scoped>
.table-toolbar {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
justify-content: space-between;
padding: 0 0 16px 0;
&-title {
&-left {
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 16px;
font-weight: 600;
flex: 1;
&-title {
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 16px;
font-weight: 600;
}
}
}
&-right {
display: flex;
justify-content: flex-end;
flex: 1;
&-right {
display: flex;
justify-content: flex-end;
flex: 1;
&-icon {
margin-left: 12px;
font-size: 16px;
cursor: pointer;
color: var(--text-color);
&-icon {
margin-left: 12px;
font-size: 16px;
cursor: pointer;
color: var(--text-color);
:hover {
color: #1890ff;
:hover {
color: #1890ff;
}
}
}
}
}
.table-toolbar-inner-popover-title {
padding: 2px 0;
}
.table-toolbar-inner-popover-title {
padding: 2px 0;
}
</style>

View File

@@ -0,0 +1,41 @@
import type { Component } from 'vue';
import {
NInput,
NSelect,
NCheckbox,
NInputNumber,
NSwitch,
NDatePicker,
NTimePicker,
} from 'naive-ui';
import type { ComponentType } from './types/componentType';
export enum EventEnum {
NInput = 'on-input',
NInputNumber = 'on-input',
NSelect = 'on-update:value',
NSwitch = 'on-update:value',
NCheckbox = 'on-update:value',
NDatePicker = 'on-update:value',
NTimePicker = 'on-update:value',
}
const componentMap = new Map<ComponentType, Component>();
componentMap.set('NInput', NInput);
componentMap.set('NInputNumber', NInputNumber);
componentMap.set('NSelect', NSelect);
componentMap.set('NSwitch', NSwitch);
componentMap.set('NCheckbox', NCheckbox);
componentMap.set('NDatePicker', NDatePicker);
componentMap.set('NTimePicker', NTimePicker);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName);
}
export { componentMap };

View File

@@ -0,0 +1,141 @@
<template>
<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-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"
trigger="hover"
:options="getDropdownList"
@select="select"
>
<slot name="more"></slot>
<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">
<DownOutlined />
</n-icon>
</div>
<!-- <template #icon>-->
<!-- -->
<!-- </template>-->
</n-button>
</n-dropdown>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRaw } from 'vue';
import { ActionItem } from '@/components/Table';
import { usePermission } from '@/hooks/web/usePermission';
import { isBoolean, isFunction } from '@/utils/is';
import { DownOutlined } from '@vicons/antd';
export default defineComponent({
name: 'TableAction',
components: { DownOutlined },
props: {
actions: {
type: Array as PropType<ActionItem[]>,
default: null,
required: true,
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
style: {
type: String as PropType<String>,
default: 'button',
},
select: {
type: Function as PropType<Function>,
default: () => {},
},
},
setup(props) {
const { hasPermission } = usePermission();
const actionType =
props.style === 'button' ? 'default' : props.style === 'text' ? 'primary' : 'default';
const actionText =
props.style === 'button' ? undefined : props.style === 'text' ? true : undefined;
const getMoreProps = computed(() => {
return {
text: actionText,
type: actionType,
size: 'small',
};
});
const getDropdownList = computed(() => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return hasPermission(action.auth as string[]) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
return {
size: 'small',
text: actionText,
type: actionType,
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
};
});
});
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return hasPermission(action.auth as string[]) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
//需要展示什么风格,自己修改一下参数
return {
size: 'small',
text: actionText,
type: actionType,
...action,
...(popConfirm || {}),
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
return {
getActions,
getDropdownList,
getMoreProps,
};
},
});
</script>

View File

@@ -0,0 +1,47 @@
import type { FunctionalComponent, defineComponent } from 'vue';
import type { ComponentType } from '../../types/componentType';
import { componentMap } from '@/components/Table/src/componentMap';
import { h } from 'vue';
import { NPopover } from 'naive-ui';
export interface ComponentProps {
component: ComponentType;
rule: boolean;
popoverVisible: boolean;
ruleMessage: string;
}
export const CellComponent: FunctionalComponent = (
{ component = 'NInput', rule = true, ruleMessage, popoverVisible }: ComponentProps,
{ attrs }
) => {
const Comp = componentMap.get(component) as typeof defineComponent;
const DefaultComp = h(Comp, attrs);
if (!rule) {
return DefaultComp;
}
return h(
NPopover,
{ 'display-directive': 'show', show: !!popoverVisible, manual: 'manual' },
{
trigger: () => DefaultComp,
default: () =>
h(
'span',
{
style: {
color: 'red',
width: '90px',
display: 'inline-block',
},
},
{
default: () => ruleMessage,
}
),
}
);
};

View File

@@ -0,0 +1,418 @@
<template>
<div class="editable-cell">
<div class="flex editable-cell-content" v-if="isEdit" v-click-outside="onClickOutside">
<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="mx-2 cursor-pointer" title="保存">
<CheckOutlined @click="handleSubmit" />
</n-icon>
<n-icon class="mx-2 cursor-pointer" title="取消">
<CloseOutlined @click="handleCancel" />
</n-icon>
</div>
</div>
<div v-else class="flex items-center editable-cell-content" @click="handleEdit">
{{ getValues }}
<n-icon class="ml-1 edit-icon" v-if="!column.editRow">
<FormOutlined />
</n-icon>
</div>
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { BasicColumn } from '../../types/table';
import type { EditRecordRow } from './index';
import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
import { FormOutlined, CloseOutlined, CheckOutlined } from '@vicons/antd';
import { CellComponent } from './CellComponent';
import { useTableContext } from '../../hooks/useTableContext';
import clickOutside from '@/directives/clickOutside';
import { propTypes } from '@/utils/propTypes';
import { isString, isBoolean, isFunction, isNumber, isArray } from '@/utils/is';
import { createPlaceholderMessage } from './helper';
import { set, omit } from 'lodash-es';
import { EventEnum } from '@/components/Table/src/componentMap';
import { parseISO, format } from 'date-fns';
export default defineComponent({
name: 'EditableCell',
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
directives: {
clickOutside,
},
props: {
value: {
type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
default: '',
},
record: {
type: Object as PropType<EditRecordRow>,
},
column: {
type: Object as PropType<BasicColumn>,
default: () => ({}),
},
index: propTypes.number,
},
setup(props) {
const table = useTableContext();
const isEdit = ref(false);
const elRef = ref();
const ruleVisible = ref(false);
const ruleMessage = ref('');
const optionsRef = ref<LabelValueOptions>([]);
const currentValueRef = ref<any>(props.value);
const defaultValueRef = ref<any>(props.value);
// const { prefixCls } = useDesign('editable-cell');
const getComponent = computed(() => props.column?.editComponent || 'NInput');
const getRule = computed(() => props.column?.editRule);
const getRuleVisible = computed(() => {
return unref(ruleMessage) && unref(ruleVisible);
});
const getIsCheckComp = computed(() => {
const component = unref(getComponent);
return ['NCheckbox', 'NRadio'].includes(component);
});
const getComponentProps = computed(() => {
const compProps = props.column?.editComponentProps ?? {};
const editComponent = props.column?.editComponent ?? null;
const component = unref(getComponent);
const apiSelectProps: Recordable = {};
const isCheckValue = unref(getIsCheckComp);
let valueField = isCheckValue ? 'checked' : 'value';
const val = unref(currentValueRef);
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
if (component === 'NDatePicker') {
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;
return {
placeholder: createPlaceholderMessage(unref(getComponent)),
...apiSelectProps,
...omit(compProps, 'onChange'),
[onEvent]: handleChange,
[valueField]: value,
};
});
const getValues = computed(() => {
const { editComponentProps, editValueMap } = props.column;
const value = unref(currentValueRef);
if (editValueMap && isFunction(editValueMap)) {
return editValueMap(value);
}
const component = unref(getComponent);
if (!component.includes('NSelect')) {
return value;
}
const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
const option = options.find((item) => `${item.value}` === `${value}`);
return option?.label ?? value;
});
const getWrapperClass = computed(() => {
const { align = 'center' } = props.column;
return `edit-cell-align-${align}`;
});
const getRowEditable = computed(() => {
const { editable } = props.record || {};
return !!editable;
});
watchEffect(() => {
defaultValueRef.value = props.value;
});
watchEffect(() => {
const { editable } = props.column;
if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
isEdit.value = !!editable || unref(getRowEditable);
}
});
function handleEdit() {
if (unref(getRowEditable) || unref(props.column?.editRow)) return;
ruleMessage.value = '';
isEdit.value = true;
nextTick(() => {
const el = unref(elRef);
el?.focus?.();
});
}
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')) {
currentValueRef.value = (e as ChangeEvent).target.value;
} else if (component === 'NCheckbox') {
currentValueRef.value = (e as ChangeEvent).target.checked;
} else if (isString(e) || isBoolean(e) || isNumber(e)) {
currentValueRef.value = e;
}
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
if (component === 'NDatePicker') {
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;
if (onChange && isFunction(onChange)) onChange(...arguments);
table.emit?.('edit-change', {
column: props.column,
value: unref(currentValueRef),
record: toRaw(props.record),
});
await handleSubmiRule();
}
async function handleSubmiRule() {
const { column, record } = props;
const { editRule } = column;
const currentValue = unref(currentValueRef);
if (editRule) {
if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
ruleVisible.value = true;
const component = unref(getComponent);
ruleMessage.value = createPlaceholderMessage(component);
return false;
}
if (isFunction(editRule)) {
const res = await editRule(currentValue, record as Recordable);
if (!!res) {
ruleMessage.value = res;
ruleVisible.value = true;
return false;
} else {
ruleMessage.value = '';
return true;
}
}
}
ruleMessage.value = '';
return true;
}
async function handleSubmit(needEmit = true, valid = true) {
if (valid) {
const isPass = await handleSubmiRule();
if (!isPass) return false;
}
const { column, index, record } = props;
if (!record) return false;
const { key } = column;
const value = unref(currentValueRef);
if (!key) return;
const dataKey = key as string;
set(record, dataKey, value);
//const record = await table.updateTableData(index, dataKey, value);
needEmit && table.emit?.('edit-end', { record, index, key, value });
isEdit.value = false;
}
async function handleEnter() {
if (props.column?.editRow) {
return;
}
await handleSubmit();
}
function handleCancel() {
isEdit.value = false;
currentValueRef.value = defaultValueRef.value;
const { column, index, record } = props;
const { key } = column;
ruleVisible.value = true;
ruleMessage.value = '';
table.emit?.('edit-cancel', {
record,
index,
key: key,
value: unref(currentValueRef),
});
}
function onClickOutside() {
if (props.column?.editable || unref(getRowEditable)) {
return;
}
const component = unref(getComponent);
if (component.includes('NInput')) {
handleCancel();
}
}
// only ApiSelect
function handleOptionsChange(options: LabelValueOptions) {
optionsRef.value = options;
}
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;
}
};
}
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;
&-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;
}
}
</style>

View File

@@ -0,0 +1,15 @@
import { ComponentType } from '../../types/componentType';
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component === 'NInput') return '请输入';
if (
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
component
)
)
return '请选择';
return '';
}

View File

@@ -0,0 +1,49 @@
import type { BasicColumn } from '@/components/Table/src/types/table';
import { h, Ref } from 'vue';
import EditableCell from './EditableCell.vue';
export function renderEditCell(column: BasicColumn) {
return (record, index) => {
const _key = column.key;
const value = record[_key];
record.onEdit = async (edit: boolean, submit = false) => {
if (!submit) {
record.editable = edit;
}
if (!edit && submit) {
const res = await record.onSubmitEdit?.();
if (res) {
record.editable = false;
return true;
}
return false;
}
// cancel
if (!edit && !submit) {
record.onCancelEdit?.();
}
return true;
};
return h(EditableCell, {
value,
record,
column,
index,
});
};
}
export type EditRecordRow<T = Recordable> = Partial<
{
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
editable: boolean;
onCancel: Fn;
onSubmit: Fn;
submitCbs: Fn[];
cancelCbs: Fn[];
validCbs: Fn[];
editValueRefs: Recordable<Ref>;
} & T
>;

View File

@@ -5,46 +5,75 @@
<n-popover trigger="click" :width="230" class="toolbar-popover" placement="bottom-end">
<template #trigger>
<n-icon size="18">
<SettingOutlined/>
<SettingOutlined />
</n-icon>
</template>
<template #header>
<div class="table-toolbar-inner-popover-title">
<n-space>
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll">列展示</n-checkbox>
<n-checkbox v-model:checked="selection" @update:checked="onSelection">勾选列</n-checkbox>
<n-button text type="info" size="small" class="mt-1" @click="resetColumns">重置</n-button>
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll"
>列展示</n-checkbox
>
<n-checkbox v-model:checked="selection" @update:checked="onSelection"
>勾选列</n-checkbox
>
<n-button text type="info" size="small" class="mt-1" @click="resetColumns"
>重置</n-button
>
</n-space>
</div>
</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">
<template #item="{element, index}">
<div class="table-toolbar-inner-checkbox"
:class="{'table-toolbar-inner-checkbox-dark':getDarkTheme === true}">
<span class="drag-icon">
<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,
'no-draggable': element.draggable === false,
}"
>
<span
class="drag-icon"
:class="{ 'drag-icon-hidden': element.draggable === false }"
>
<n-icon size="18">
<DragOutlined/>
<DragOutlined />
</n-icon>
</span>
<n-checkbox :value="element.key" :label="element.title"/>
<n-checkbox :value="element.key" :label="element.title" />
<div class="fixed-item">
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-icon size="18" :color="element.fixed === 'left' ? '#2080f0':undefined"
class="cursor-pointer" @click="fixedColumn(element,'left')">
<VerticalRightOutlined/>
<n-icon
size="18"
:color="element.fixed === 'left' ? '#2080f0' : undefined"
class="cursor-pointer"
@click="fixedColumn(element, 'left')"
>
<VerticalRightOutlined />
</n-icon>
</template>
<span>固定到左侧</span>
</n-tooltip>
<n-divider vertical/>
<n-divider vertical />
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-icon size="18" :color="element.fixed === 'right' ? '#2080f0':undefined"
class="cursor-pointer" @click="fixedColumn(element,'right')">
<VerticalLeftOutlined/>
<n-icon
size="18"
:color="element.fixed === 'right' ? '#2080f0' : undefined"
class="cursor-pointer"
@click="fixedColumn(element, 'right')"
>
<VerticalLeftOutlined />
</n-icon>
</template>
<span>固定到右侧</span>
@@ -63,221 +92,240 @@
</template>
<script lang="ts">
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from "vue"
import { useTableContext } from '../../hooks/useTableContext';
import { ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, VerticalRightOutlined, VerticalLeftOutlined } from '@vicons/antd'
import Draggable from 'vuedraggable/src/vuedraggable'
import { useDesignSetting } from "@/hooks/setting/useDesignSetting";
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';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
interface Options {
title: string;
key: string;
fixed?: boolean | 'left' | 'right';
}
export default defineComponent({
name: 'ColumnSetting',
components: {
ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, Draggable,
VerticalRightOutlined, VerticalLeftOutlined
},
setup(props, { emit }) {
const { getDarkTheme } = useDesignSetting()
const table = useTableContext();
const columnsList = ref<Options[]>([]);
const cacheColumnsList = ref<Options[]>([]);
const state = reactive({
selection: false,
checkAll: true,
checkList: [],
defaultCheckList: []
})
const getSelection = computed(() => {
return state.selection
})
watchEffect(() => {
const columns = table.getColumns();
if (columns.length) {
init();
}
});
//初始化
function init() {
const columns = getColumns();
const checkList = columns.map(item => item.key)
state.checkList = checkList
state.defaultCheckList = checkList
columnsList.value = columns
cacheColumnsList.value = columns
}
//切换
function onChange(checkList) {
if (state.selection) {
checkList.unshift('selection')
}
setColumns(checkList)
}
//设置
function setColumns(columns) {
table.setColumns(columns)
}
//获取
function getColumns() {
let newRet = []
table.getColumns().forEach(item => {
newRet.push({ ...item })
})
return newRet
}
//重置
function resetColumns() {
state.checkList = [...state.defaultCheckList]
state.checkAll = true;
let cacheColumnsKeys: any[] = table.getCacheColumns()
let newColumns = cacheColumnsKeys.map(item => {
return {
...item,
fixed: undefined
}
})
setColumns(newColumns);
columnsList.value = newColumns
}
//全选
function onCheckAll(e) {
let checkList = table.getCacheColumns(true)
if (e) {
setColumns(checkList);
state.checkList = checkList
} else {
setColumns([]);
state.checkList = []
}
}
//拖拽排序
function draggableEnd() {
const newColumns = toRaw(unref(columnsList))
columnsList.value = newColumns
setColumns(newColumns);
}
//勾选列
function onSelection(e) {
let checkList = table.getCacheColumns()
if (e) {
checkList.unshift({ type: 'selection', key: 'selection' })
setColumns(checkList);
} else {
checkList.splice(0, 1)
setColumns(checkList);
}
}
//固定
function fixedColumn(item, fixed) {
console.log('item', item)
if (!state.checkList.includes(item.key)) return;
let columns = getColumns();
const isFixed = item.fixed === fixed ? undefined : fixed
let index = columns.findIndex(res => res.key === item.key)
console.log('index', index)
if (index !== -1) {
columns[index].fixed = isFixed;
}
table.setCacheColumnsField(item.key, { fixed: isFixed })
columnsList.value[index].fixed = isFixed
console.log('columnsList', columnsList.value)
setColumns(columns);
}
return {
...toRefs(state),
columnsList,
getDarkTheme,
onChange,
onCheckAll,
onSelection,
resetColumns,
fixedColumn,
draggableEnd,
getSelection
}
interface Options {
title: string;
key: string;
fixed?: boolean | 'left' | 'right';
}
})
export default defineComponent({
name: 'ColumnSetting',
components: {
SettingOutlined,
DragOutlined,
Draggable,
VerticalRightOutlined,
VerticalLeftOutlined,
},
setup() {
const { getDarkTheme } = useDesignSetting();
const table: any = useTableContext();
const columnsList = ref<Options[]>([]);
const cacheColumnsList = ref<Options[]>([]);
const state = reactive({
selection: false,
checkAll: true,
checkList: [],
defaultCheckList: [],
});
const getSelection = computed(() => {
return state.selection;
});
watchEffect(() => {
const columns = table.getColumns();
if (columns.length) {
init();
}
});
//初始化
function init() {
const columns: any[] = getColumns();
const checkList: any = columns.map((item) => item.key);
state.checkList = checkList;
state.defaultCheckList = checkList;
const newColumns = columns.filter((item) => item.key != 'action' && item.title != '操作');
if (!columnsList.value.length) {
columnsList.value = cloneDeep(newColumns);
cacheColumnsList.value = cloneDeep(newColumns);
}
}
//切换
function onChange(checkList) {
if (state.selection) {
checkList.unshift('selection');
}
setColumns(checkList);
}
//设置
function setColumns(columns) {
table.setColumns(columns);
}
//获取
function getColumns() {
let newRet: any[] = [];
table.getColumns().forEach((item) => {
newRet.push({ ...item });
});
return newRet;
}
//重置
function resetColumns() {
state.checkList = [...state.defaultCheckList];
state.checkAll = true;
let cacheColumnsKeys: any[] = table.getCacheColumns();
let newColumns = cacheColumnsKeys.map((item) => {
return {
...item,
fixed: undefined,
};
});
setColumns(newColumns);
columnsList.value = newColumns;
}
//全选
function onCheckAll(e) {
let checkList = table.getCacheColumns(true);
if (e) {
setColumns(checkList);
state.checkList = checkList;
} else {
setColumns([]);
state.checkList = [];
}
}
//拖拽排序
function draggableEnd() {
const newColumns = toRaw(unref(columnsList));
columnsList.value = newColumns;
setColumns(newColumns);
}
//勾选列
function onSelection(e) {
let checkList = table.getCacheColumns();
if (e) {
checkList.unshift({ type: 'selection', key: 'selection' });
setColumns(checkList);
} else {
checkList.splice(0, 1);
setColumns(checkList);
}
}
function onMove(e) {
if (e.draggedContext.element.draggable === false) return false;
return true;
}
//固定
function fixedColumn(item, fixed) {
if (!state.checkList.includes(item.key)) return;
let columns = getColumns();
const isFixed = item.fixed === fixed ? undefined : fixed;
let index = columns.findIndex((res) => res.key === item.key);
if (index !== -1) {
columns[index].fixed = isFixed;
}
table.setCacheColumnsField(item.key, { fixed: isFixed });
columnsList.value[index].fixed = isFixed;
setColumns(columns);
}
return {
...toRefs(state),
columnsList,
getDarkTheme,
onChange,
onCheckAll,
onSelection,
onMove,
resetColumns,
fixedColumn,
draggableEnd,
getSelection,
};
},
});
</script>
<style lang="less">
.table-toolbar {
&-inner-popover-title {
padding: 3px 0;
}
.table-toolbar {
&-inner-popover-title {
padding: 3px 0;
}
&-right {
&-icon {
margin-left: 12px;
font-size: 16px;
color: var(--text-color);
cursor: pointer;
&-right {
&-icon {
margin-left: 12px;
font-size: 16px;
color: var(--text-color);
cursor: pointer;
:hover {
color: #1890ff;
:hover {
color: #1890ff;
}
}
}
}
}
.table-toolbar-inner {
&-checkbox {
display: flex;
align-items: center;
padding: 10px 14px;
&:hover {
background: #e6f7ff;
}
.drag-icon {
display: inline-flex;
margin-right: 8px;
cursor: move;
}
.fixed-item {
.table-toolbar-inner {
&-checkbox {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
}
.ant-checkbox-wrapper {
flex: 1;
padding: 10px 14px;
&:hover {
color: #1890ff !important;
background: #e6f7ff;
}
.drag-icon {
display: inline-flex;
margin-right: 8px;
cursor: move;
&-hidden {
visibility: hidden;
cursor: default;
}
}
.fixed-item {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
}
.ant-checkbox-wrapper {
flex: 1;
&:hover {
color: #1890ff !important;
}
}
}
&-checkbox-dark {
&:hover {
background: hsla(0, 0%, 100%, 0.08);
}
}
}
&-checkbox-dark {
&:hover {
background: hsla(0, 0%, 100%, .08);
.toolbar-popover {
.n-popover__content {
padding: 0;
}
}
}
.toolbar-popover {
.n-popover__content {
padding: 0;
}
}
</style>

View File

@@ -1,6 +1,6 @@
import componentSetting from '@/settings/componentSetting'
import componentSetting from '@/settings/componentSetting';
const { table } = componentSetting
const { table } = componentSetting;
const { apiSetting, defaultPageSize, pageSizes } = table;
@@ -9,7 +9,3 @@ export const DEFAULTPAGESIZE = defaultPageSize;
export const APISETTING = apiSetting;
export const PAGESIZES = pageSizes;

View File

@@ -1,95 +1,164 @@
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw } from 'vue';
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw, h } from 'vue';
import type { BasicColumn, BasicTableProps } from '../types/table';
import { isEqual, cloneDeep } from 'lodash-es';
import { isArray, isString } from '@/utils/is';
import { isArray, isString, isBoolean, isFunction } from '@/utils/is';
import { usePermission } from '@/hooks/web/usePermission';
import { ActionItem } from '@/components/Table';
import { renderEditCell } from '../components/editable';
import { NTooltip, NIcon } from 'naive-ui';
import { FormOutlined } from '@vicons/antd';
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
const getColumnsRef = computed(() => {
const columns = cloneDeep(unref(columnsRef));
return columns;
})
const getColumnsRef = computed(() => {
const columns = cloneDeep(unref(columnsRef));
const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns);
return columns
})
handleActionColumn(propsRef, columns);
if (!columns) return [];
return columns;
});
watch(
() => unref(propsRef).columns,
(columns) => {
columnsRef.value = columns;
cacheColumns = columns;
}
);
const { hasPermission } = usePermission();
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
//设置
function setColumns(columnList: string[]) {
const columns: any[] = cloneDeep(columnList);
if (!isArray(columns)) return;
let isIfShow = true;
if (!columns.length) {
columnsRef.value = [];
return;
}
const cacheKeys = cacheColumns.map((item) => item.key);
//针对拖拽排序
if (!isString(columns[0])) {
columnsRef.value = columns;
} else {
const newColumns: any[] = []
cacheColumns.forEach(item => {
if (columnList.includes(item.key)) {
newColumns.push({ ...item })
}
})
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return (
cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key)
);
});
}
columnsRef.value = newColumns
}
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
//获取
function getColumns() {
let columns = toRaw(unref(getColumnsRef));
return columns.map(item => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }
})
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
//获取原始
function getCacheColumns(isKey?: boolean): any[] {
return isKey ? cacheColumns.map(item => item.key) : cacheColumns;
}
const renderTooltip = (trigger, content) => {
return h(NTooltip, null, {
trigger: () => trigger,
default: () => content,
});
};
//更新原始数据单个字段
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) {
return;
const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns);
return columns
.filter((column) => {
return hasPermission(column.auth as string[]) && isIfShow(column);
})
.map((column) => {
//默认 ellipsis 为true
column.ellipsis = typeof column.ellipsis === 'undefined' ? { tooltip: true } : false;
const { edit } = column;
if (edit) {
column.render = renderEditCell(column);
if (edit) {
const title: any = column.title;
column.title = () => {
return renderTooltip(
h('div', { class: 'flex items-center' }, [
h('span', { style: { 'margin-right': '5px' } }, title),
h(
NIcon,
{
size: 14,
},
{
default: () => h(FormOutlined),
}
),
]),
'该列可编辑'
);
};
}
}
cacheColumns.forEach((item) => {
if (item.key === dataIndex) {
Object.assign(item, value);
return;
}
return column;
});
});
watch(
() => unref(propsRef).columns,
(columns) => {
columnsRef.value = columns;
cacheColumns = columns;
}
);
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
const { actionColumn } = unref(propsRef);
if (!actionColumn) return;
!columns.find((col) => col.key === 'action') &&
columns.push({
...(actionColumn as any),
});
}
//设置
function setColumns(columnList: string[]) {
const columns: any[] = cloneDeep(columnList);
if (!isArray(columns)) return;
if (!columns.length) {
columnsRef.value = [];
return;
}
const cacheKeys = cacheColumns.map((item) => item.key);
//针对拖拽排序
if (!isString(columns[0])) {
columnsRef.value = columns;
} else {
const newColumns: any[] = [];
cacheColumns.forEach((item) => {
if (columnList.includes(item.key)) {
newColumns.push({ ...item });
}
});
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key);
});
}
columnsRef.value = newColumns;
}
}
return {
getColumnsRef,
getCacheColumns,
setCacheColumnsField,
setColumns,
getColumns,
getPageColumns
};
//获取
function getColumns(): BasicColumn[] {
const columns = toRaw(unref(getColumnsRef));
return columns.map((item) => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined };
});
}
//获取原始
function getCacheColumns(isKey?: boolean): any[] {
return isKey ? cacheColumns.map((item) => item.key) : cacheColumns;
}
//更新原始数据单个字段
function setCacheColumnsField(key: string | undefined, value: Partial<BasicColumn>) {
if (!key || !value) {
return;
}
cacheColumns.forEach((item) => {
if (item.key === key) {
Object.assign(item, value);
return;
}
});
}
return {
getColumnsRef,
getCacheColumns,
setCacheColumnsField,
setColumns,
getColumns,
getPageColumns,
};
}

View File

@@ -1,141 +1,150 @@
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(
propsRef: ComputedRef<BasicTableProps>,
{
getPaginationInfo,
setPagination,
setLoading,
tableData
},
emit
propsRef: ComputedRef<BasicTableProps>,
{ getPaginationInfo, setPagination, setLoading, tableData },
emit
) {
const dataSourceRef = ref([]);
const dataSourceRef = ref<Recordable[]>([]);
watchEffect(() => {
tableData.value = unref(dataSourceRef);
});
watchEffect(() => {
tableData.value = unref(dataSourceRef);
});
watch(
() => unref(propsRef).dataSource,
() => {
const { dataSource }: any = unref(propsRef);
dataSource && (dataSourceRef.value = dataSource);
},
{
immediate: true,
}
);
watch(
() => unref(propsRef).dataSource,
() => {
const { dataSource }: any = unref(propsRef);
dataSource && (dataSourceRef.value = dataSource);
},
{
immediate: true,
}
);
const getRowKey = computed(() => {
const { rowKey }: any = unref(propsRef);
return rowKey ? rowKey : () => {
return 'key'
const getRowKey = computed(() => {
const { rowKey }: any = unref(propsRef);
return rowKey
? rowKey
: () => {
return 'key';
};
});
});
const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef);
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef);
}
return unref(dataSourceRef);
});
async function fetch(opt?) {
try {
setLoading(true);
const { request, pagination }: any = unref(propsRef);
//组装分页信息
const pageField = APISETTING.pageField
const sizeField = APISETTING.sizeField
const totalField = APISETTING.totalField
const listField = APISETTING.listField
let pageParams = {};
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
pageParams = {};
} else {
pageParams[pageField] = (opt && opt[pageField]) || page;
pageParams[sizeField] = pageSize;
}
let params = {
...pageParams,
}
const res = await request(params);
const resultTotal = res[totalField] || 0
const currentPage = res[pageField]
// 如果数据异常,需获取正确的页码再次执行
if (resultTotal) {
const currentTotalPage = Math.ceil(resultTotal / pageSize);
if (page > currentTotalPage) {
setPagination({
[pageField]: currentTotalPage,
});
fetch(opt);
}
}
let resultInfo = res[listField] ? res[listField] : []
dataSourceRef.value = resultInfo;
setPagination({
[pageField]: currentPage,
[totalField]: resultTotal,
});
if (opt && opt[pageField]) {
setPagination({
[pageField]: opt[pageField] || 1,
});
}
emit('fetch-success', {
items: unref(resultInfo),
resultTotal
});
} catch (error) {
console.error(error)
emit('fetch-error', error);
dataSourceRef.value = [];
// setPagination({
// pageCount: 0,
// });
} finally {
setLoading(false);
const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef);
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef);
}
return unref(dataSourceRef);
});
async function fetch(opt?) {
try {
setLoading(true);
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;
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
pageParams = {};
} else {
pageParams[pageField] = (opt && opt[pageField]) || page;
pageParams[sizeField] = pageSize;
}
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];
const currentPage = res[pageField];
const total = res[itemCount];
const results = res[listField] ? res[listField] : [];
// 如果数据异常,需获取正确的页码再次执行
if (resultTotal) {
const currentTotalPage = Math.ceil(total / pageSize);
if (page > currentTotalPage) {
setPagination({
page: currentTotalPage,
itemCount: total,
});
return await fetch(opt);
}
}
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({
page: currentPage,
pageCount: resultTotal,
itemCount: total,
});
if (opt && opt[pageField]) {
setPagination({
page: opt[pageField] || 1,
});
}
emit('fetch-success', {
items: unref(resultInfo),
resultTotal,
});
} catch (error) {
console.error(error);
emit('fetch-error', error);
dataSourceRef.value = [];
setPagination({
pageCount: 0,
});
} finally {
setLoading(false);
}
}
onMounted(() => {
setTimeout(() => {
fetch();
}, 16)
});
onMounted(() => {
setTimeout(() => {
fetch();
}, 16);
});
function setTableData(values) {
dataSourceRef.value = values;
}
function setTableData(values) {
dataSourceRef.value = values;
}
function getDataSource(): any[] {
return getDataSourceRef.value;
}
function getDataSource(): any[] {
return getDataSourceRef.value;
}
async function reload(opt?) {
await fetch(opt);
}
async function reload(opt?) {
await fetch(opt);
}
return {
fetch,
getRowKey,
getDataSourceRef,
getDataSource,
setTableData,
reload
}
return {
fetch,
getRowKey,
getDataSourceRef,
getDataSource,
setTableData,
reload,
};
}

View File

@@ -2,20 +2,20 @@ import { ref, ComputedRef, unref, computed, watch } from 'vue';
import type { BasicTableProps } from '../types/table';
export function useLoading(props: ComputedRef<BasicTableProps>) {
const loadingRef = ref(unref(props).loading);
const loadingRef = ref(unref(props).loading);
watch(
() => unref(props).loading,
(loading) => {
loadingRef.value = loading;
}
);
const getLoading = computed(() => unref(loadingRef));
function setLoading(loading: boolean) {
loadingRef.value = loading;
watch(
() => unref(props).loading,
(loading) => {
loadingRef.value = loading;
}
);
return { getLoading, setLoading };
const getLoading = computed(() => unref(loadingRef));
function setLoading(loading: boolean) {
loadingRef.value = loading;
}
return { getLoading, setLoading };
}

View File

@@ -1,48 +1,62 @@
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';
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
const configRef = ref<PaginationProps>({});
const show = ref(true);
const configRef = ref<PaginationProps>({});
const show = ref(true);
const getPaginationInfo = computed((): PaginationProps | boolean => {
const { pagination } = unref(refProps);
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false;
}
return {
pageSize: DEFAULTPAGESIZE,
pageSizes: PAGESIZES,
showSizePicker: true,
showQuickJumper: true,
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
};
});
function setPagination(info: Partial<PaginationProps>) {
const paginationInfo = unref(getPaginationInfo);
watch(
() => unref(refProps).pagination,
(pagination) => {
if (!isBoolean(pagination) && pagination) {
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info,
...unref(configRef),
...(pagination ?? {}),
};
}
}
);
function getPagination() {
return unref(getPaginationInfo);
const getPaginationInfo = computed((): PaginationProps | boolean => {
const { pagination } = unref(refProps);
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false;
}
return {
page: 1, //当前页
pageSize: DEFAULTPAGESIZE, //分页大小
pageSizes: PAGESIZES, // 每页条数
showSizePicker: true,
showQuickJumper: true,
prefix: (pagingInfo) => `${pagingInfo.itemCount}`, // 不需要可以通过 pagination 重置或者删除
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
};
});
function getShowPagination() {
return unref(show);
}
function setPagination(info: Partial<PaginationProps>) {
const paginationInfo = unref(getPaginationInfo);
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info,
};
}
async function setShowPagination(flag: boolean) {
show.value = flag;
}
function getPagination() {
return unref(getPaginationInfo);
}
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
function getShowPagination() {
return unref(show);
}
async function setShowPagination(flag: boolean) {
show.value = flag;
}
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
}

View File

@@ -5,18 +5,18 @@ import { provide, inject, ComputedRef } from 'vue';
const key = Symbol('s-table');
type Instance = TableActionType & {
wrapRef: Ref<Nullable<HTMLElement>>;
getBindValues: ComputedRef<Recordable>;
wrapRef: Ref<Nullable<HTMLElement>>;
getBindValues: ComputedRef<Recordable>;
};
type RetInstance = Omit<Instance, 'getBindValues'> & {
getBindValues: ComputedRef<BasicTableProps>;
getBindValues: ComputedRef<BasicTableProps>;
};
export function createTableContext(instance: Instance) {
provide(key, instance);
provide(key, instance);
}
export function useTableContext(): RetInstance {
return inject(key) as RetInstance;
return inject(key) as RetInstance;
}

View File

@@ -1,45 +1,60 @@
import type { PropType } from 'vue'
import { BasicColumn } from './types/table'
import type { PropType } from 'vue';
import { propTypes } from '@/utils/propTypes';
import { BasicColumn } from './types/table';
import { NDataTable } from 'naive-ui';
export const basicProps = {
title: {
type: String,
default: null,
},
titleTooltip: {
type: String,
default: null,
},
size: {
type: String,
default: 'medium',
},
tableData: {
type: [Object],
default: () => {
},
},
columns: {
type: [Array] as PropType<BasicColumn[]>,
default: () => [],
required: true,
},
request: {
type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null,
required: true
},
rowKey: {
type: [String, Function] as PropType<string | ((record) => string)>,
default: undefined,
},
pagination: {
type: [Object, Boolean],
default: () => {
}
},
showPagination: {
type: [String, Boolean],
default: 'auto'
}
}
...NDataTable.props, // 这里继承原 UI 组件的 props
title: {
type: String,
default: null,
},
titleTooltip: {
type: String,
default: null,
},
size: {
type: String,
default: 'medium',
},
dataSource: {
type: [Object],
default: () => [],
},
columns: {
type: [Array] as PropType<BasicColumn[]>,
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,
},
afterRequest: {
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
default: null,
},
rowKey: {
type: [String, Function] as PropType<string | ((record) => string)>,
default: undefined,
},
pagination: {
type: [Object, Boolean],
default: () => {},
},
//废弃
showPagination: {
type: [String, Boolean],
default: 'auto',
},
actionColumn: {
type: Object as PropType<BasicColumn>,
default: null,
},
canResize: propTypes.bool.def(true),
resizeHeightOffset: propTypes.number.def(0),
striped: propTypes.bool.def(false),
};

View File

@@ -0,0 +1,9 @@
export type ComponentType =
| 'NInput'
| 'NInputNumber'
| 'NSelect'
| 'NCheckbox'
| 'NSwitch'
| 'NDatePicker'
| 'NTimePicker'
| 'NCascader';

View File

@@ -1,11 +1,10 @@
import Pagination from 'naive-ui/lib/pagination';
import { VNodeChild } from 'vue';
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,22 +1,38 @@
import type {
TableBaseColumn,
} from 'naive-ui/lib/data-table/src/interface';
export interface BasicColumn extends TableBaseColumn {
import type { InternalRowData, TableBaseColumn } from 'naive-ui/lib/data-table/src/interface';
import { ComponentType } from './componentType';
export interface BasicColumn<T = InternalRowData> extends TableBaseColumn<T> {
//编辑表格
edit?: boolean;
editRow?: boolean;
editable?: boolean;
editComponent?: ComponentType;
editComponentProps?: Recordable;
editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
editValueMap?: (value: any) => string;
onEditRow?: () => void;
// 权限编码控制是否显示
auth?: string[];
// 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean);
// 控制是否支持拖拽,默认支持
draggable?: boolean;
}
export interface TableActionType {
reload: (opt) => Promise<void>;
emit?: any;
getColumns: (opt) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
reload: (opt) => Promise<void>;
emit?: any;
getColumns: (opt?) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
}
export interface BasicTableProps<T = any> {
title?: string,
dataSource: Function,
columns: any[],
pagination: object,
showPagination: boolean
export interface BasicTableProps {
title?: string;
dataSource: Function;
columns: any[];
pagination: object;
showPagination: boolean;
actionColumn: any[];
canResize: boolean;
resizeHeightOffset: number;
loading: boolean;
}

View File

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

View File

@@ -2,42 +2,49 @@
<div class="w-full">
<div class="upload">
<div class="upload-card">
<!--图片列表-->
<div class="upload-card-item" :style="getCSSProperties" v-for="(item,index) in imgList">
<div
class="upload-card-item"
:style="getCSSProperties"
v-for="(item, index) in imgList"
:key="`img_${index}`"
>
<div class="upload-card-item-info">
<div class="img-box">
<img :src="item"/>
<img :src="item" />
</div>
<div class="img-box-actions">
<n-icon size="18" class="action-icon mx-2" @click="preview(item)">
<EyeOutlined/>
<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)">
<DeleteOutlined/>
<n-icon size="18" class="mx-2 action-icon" @click="remove(index)">
<DeleteOutlined />
</n-icon>
</div>
</div>
</div>
<!--上传图片-->
<div class="upload-card-item upload-card-item-select-picture" :style="getCSSProperties"
v-if="imgList.length < maxNumber">
<div
class="upload-card-item upload-card-item-select-picture"
:style="getCSSProperties"
v-if="imgList.length < maxNumber"
>
<n-upload
v-bind="$props"
:file-list-style="{ display:'none'}"
@before-upload="beforeUpload"
@finish="finish"
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/>
<PlusOutlined />
</n-icon>
<span class="upload-title">上传图片</span>
</div>
</n-upload>
</div>
</div>
</div>
@@ -47,267 +54,258 @@
{{ helpText }}
</n-alert>
</n-space>
</div>
<!--预览图片-->
<n-modal
v-model:show="showModal"
preset="card"
title="预览"
:bordered="false"
:style="{width: '520px'}"
v-model:show="showModal"
preset="card"
title="预览"
:bordered="false"
:style="{ width: '520px' }"
>
<img :src="previewUrl"/>
<img :src="previewUrl" />
</n-modal>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, computed } 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'
import componentSetting from '@/settings/componentSetting'
import { useGlobSetting } from '@/hooks/setting'
import { isString } from '@/utils/is'
import { defineComponent, toRefs, reactive, computed, watch } from 'vue';
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { basicProps } from './props';
import { useMessage, useDialog } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import componentSetting from '@/settings/componentSetting';
import { useGlobSetting } from '@/hooks/setting';
import { isString } from '@/utils/is';
const globSetting = useGlobSetting()
const globSetting = useGlobSetting();
export default defineComponent({
name: 'BasicUpload',
export default defineComponent({
name: 'BasicUpload',
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
props: {
...NUpload.props, // 这里继承原 UI 组件的 props
...basicProps
},
emits: ['uploadChange', 'delete'],
setup(props, { emit }) {
const { value, width, height } = props
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
props: {
...basicProps,
},
emits: ['uploadChange', 'delete'],
setup(props, { emit }) {
const getCSSProperties = computed(() => {
return {
width: `${props.width}px`,
height: `${props.height}px`,
};
});
const getCSSProperties = computed(() => {
return {
width: `${ width }px`,
height: `${ height }px`,
}
})
const message = useMessage();
const dialog = useDialog();
const message = useMessage()
const dialog = useDialog()
const state = reactive({
showModal: false,
previewUrl: '',
originalImgList: [] as string[],
imgList: [] as string[],
});
const state = reactive({
showModal: false,
previewUrl: '',
originalImgList: [],
imgList: []
})
//赋值默认图片显示
if (value.length) {
state.imgList = value.map(item => {
return getImgUrl(item)
})
}
//预览
function preview(url: string) {
state.showModal = true
state.previewUrl = url
}
//删除
function remove(index: number) {
dialog.info({
title: '提示',
content: '你确定要删除吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
state.imgList.splice(index, 1)
state.originalImgList.splice(index, 1)
emit('uploadChange', state.originalImgList)
emit('delete', state.originalImgList)
//赋值默认图片显示
watch(
() => props.value,
() => {
state.imgList = props.value.map((item) => {
return getImgUrl(item);
});
},
onNegativeClick: () => {
{ immediate: true }
);
//预览
function preview(url: string) {
state.showModal = true;
state.previewUrl = url;
}
//删除
function remove(index: number) {
dialog.info({
title: '提示',
content: '你确定要删除吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
state.imgList.splice(index, 1);
state.originalImgList.splice(index, 1);
emit('uploadChange', state.originalImgList);
emit('delete', state.originalImgList);
},
onNegativeClick: () => {},
});
}
//组装完整图片地址
function getImgUrl(url: string): string {
const { imgUrl } = globSetting;
return /(^http|https:\/\/)/g.test(url) ? url : `${imgUrl}${url}`;
}
function checkFileType(fileType: string) {
return componentSetting.upload.fileType.includes(fileType);
}
//上传之前
function beforeUpload({ file }) {
const fileInfo = file.file;
const { maxSize, accept } = props;
const acceptRef = (isString(accept) && accept.split(',')) || [];
// 设置最大值,则判断
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
message.error(`上传文件最大值不能超过${maxSize}M`);
return false;
}
})
}
//组装完整图片地址
function getImgUrl(url: string) {
const { imgUrl } = globSetting
return (/(^http|https:\/\/)/g).test(url) ? url : `${ imgUrl }${ url }`
}
// 设置类型,则判断
const fileType = componentSetting.upload.fileType;
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
message.error(`只能上传文件类型为${fileType.join(',')}`);
return false;
}
function checkFileType(fileType: string) {
return componentSetting.upload.fileType.includes(fileType)
}
//上传之前
function beforeUpload({ file, fileList }) {
const fileInfo = file.file;
const { maxSize, accept, maxNumber } = props;
const acceptRef = (isString(accept) && accept.split(',')) || [];
// 设置最大值,则判断
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
message.error(`上传文件最大值不能超过${ maxSize }M`);
return false;
return true;
}
// 设置类型,则判断
const fileType = componentSetting.upload.fileType
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
message.error(`只能上传文件类型为${ fileType.join(',') }`);
return false;
//上传结束
function finish({ event: Event }) {
const res = eval('(' + Event.target.response + ')');
const infoField = componentSetting.upload.apiSetting.infoField;
const { code } = res;
const message = res.msg || res.message || '上传失败';
const result = res[infoField];
//成功
if (code === ResultEnum.SUCCESS) {
let imgUrl: string = getImgUrl(result.photo);
state.imgList.push(imgUrl);
state.originalImgList.push(result.photo);
emit('uploadChange', state.originalImgList);
} else message.error(message);
}
return true
}
//上传结束
function finish({ event: Event }) {
const res = eval('(' + Event.target.response + ')');
const infoField = componentSetting.upload.apiSetting.infoField
const { code } = res
const message = (res.msg || res.message) || '上传失败'
const result = res[infoField]
//成功
if (code === ResultEnum.SUCCESS) {
let imgUrl: string = getImgUrl(result.photo)
state.imgList.push(imgUrl)
state.originalImgList.push(result.photo)
emit('uploadChange', state.originalImgList)
} else message.error(message)
}
return {
...toRefs(state),
finish,
preview,
remove,
beforeUpload,
getCSSProperties
}
}
})
return {
...toRefs(state),
finish,
preview,
remove,
beforeUpload,
getCSSProperties,
};
},
});
</script>
<style lang="less">
.upload {
width: 100%;
overflow: hidden;
.upload {
width: 100%;
overflow: hidden;
&-card {
width: auto;
height: auto;
display: flex;
flex-wrap: wrap;
align-items: center;
&-item {
margin: 0 8px 8px 0;
position: relative;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
&-card {
width: auto;
height: auto;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
&:hover {
background: 0 0;
&-info::before {
opacity: 1
}
}
&-info {
&-item {
margin: 0 8px 8px 0;
position: relative;
height: 100%;
padding: 0;
overflow: hidden;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
&:hover {
.img-box-actions {
background: 0 0;
.upload-card-item-info::before {
opacity: 1;
}
&-info::before {
opacity: 1;
}
}
&::before {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
opacity: 0;
transition: all .3s;
content: ' ';
}
.img-box {
&-info {
position: relative;
//padding: 8px;
//border: 1px solid #d9d9d9;
border-radius: 2px;
}
.img-box-actions {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
white-space: nowrap;
transform: translate(-50%, -50%);
opacity: 0;
transition: all .3s;
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
width: 100%;
padding: 0;
overflow: hidden;
&:hover {
background: 0 0;
.img-box-actions {
opacity: 1;
}
}
.action-icon {
color: rgba(255, 255, 255, .85);
&::before {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: all 0.3s;
content: ' ';
}
.img-box {
position: relative;
//padding: 8px;
//border: 1px solid #d9d9d9;
border-radius: 2px;
}
.img-box-actions {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
white-space: nowrap;
transform: translate(-50%, -50%);
opacity: 0;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
cursor: pointer;
color: #fff
background: 0 0;
}
.action-icon {
color: rgba(255, 255, 255, 0.85);
&:hover {
cursor: pointer;
color: #fff;
}
}
}
}
}
}
&-item-select-picture {
border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
background: #fafafa;
color: #666;
.upload-title {
&-item-select-picture {
border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
background: #fafafa;
color: #666;
}
}
&-item:hover {
background: 0 0;
.upload-card-item-info::before {
opacity: 1
.upload-title {
color: #666;
}
}
}
}
}
</style>

View File

@@ -1,34 +1,34 @@
import type { PropType } from 'vue'
import { NUpload } from 'naive-ui'
import type { PropType } from 'vue';
import { NUpload } from 'naive-ui';
export const basicProps = {
...NUpload.props,
accept: {
type: String,
default: '.jpg,.png,.jpeg,.svg,.gif',
},
helpText: {
type: String as PropType<string>,
default: '',
},
maxSize: {
type: Number as PropType<number>,
default: 2
},
maxNumber: {
type: Number as PropType<number>,
default: Infinity
},
value: {
type: Array as PropType<string[]>,
default: () => []
},
width: {
type: Number as PropType<number>,
default: 104
},
height: {
type: Number as PropType<number>,
default: 104 //建议不小于这个尺寸 太小页面可能显示有异常
},
}
...NUpload.props,
accept: {
type: String,
default: '.jpg,.png,.jpeg,.svg,.gif',
},
helpText: {
type: String as PropType<string>,
default: '',
},
maxSize: {
type: Number as PropType<number>,
default: 2,
},
maxNumber: {
type: Number as PropType<number>,
default: Infinity,
},
value: {
type: Array as PropType<string[]>,
default: () => [],
},
width: {
type: Number as PropType<number>,
default: 104,
},
height: {
type: Number as PropType<number>,
default: 104, //建议不小于这个尺寸 太小页面可能显示有异常
},
};

View File

@@ -1,7 +1,7 @@
export interface BasicProps<T = any> {
title?: string,
dataSource: Function,
columns: any[],
pagination: object,
showPagination: boolean
export interface BasicProps {
title?: string;
dataSource: Function;
columns: any[];
pagination: object;
showPagination: boolean;
}

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中后台前端/设计解决方案',
});

View File

@@ -0,0 +1,86 @@
import { on } from '@/utils/domUtils';
import { isServer } from '@/utils/is';
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
type FlushList = Map<
HTMLElement,
{
documentHandler: DocumentHandler;
bindingFn: (...args: unknown[]) => unknown;
}
>;
const nodeList: FlushList = new Map();
let startClick: MouseEvent;
if (!isServer) {
on(document, 'mousedown', (e: MouseEvent) => (startClick = e));
on(document, 'mouseup', (e: MouseEvent) => {
for (const { documentHandler } of nodeList.values()) {
documentHandler(e, startClick);
}
});
}
function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
let excludes: HTMLElement[] = [];
if (Array.isArray(binding.arg)) {
excludes = binding.arg;
} else {
// due to current implementation on binding type is wrong the type casting is necessary here
excludes.push(binding.arg as unknown as HTMLElement);
}
return function (mouseup, mousedown) {
const popperRef = (
binding.instance as ComponentPublicInstance<{
popperRef: Nullable<HTMLElement>;
}>
).popperRef;
const mouseUpTarget = mouseup.target as Node;
const mouseDownTarget = mousedown.target as Node;
const isBound = !binding || !binding.instance;
const isTargetExists = !mouseUpTarget || !mouseDownTarget;
const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
const isSelf = el === mouseUpTarget;
const isTargetExcluded =
(excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
const isContainedByPopper =
popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
if (
isBound ||
isTargetExists ||
isContainedByEl ||
isSelf ||
isTargetExcluded ||
isContainedByPopper
) {
return;
}
binding.value();
};
}
const ClickOutside: ObjectDirective = {
beforeMount(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
updated(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
unmounted(el) {
nodeList.delete(el);
},
};
export default ClickOutside;

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

@@ -1,19 +1,19 @@
import { ObjectDirective } from 'vue'
import { usePermission } from "@/hooks/web/usePermission";
import { ObjectDirective } from 'vue';
import { usePermission } from '@/hooks/web/usePermission';
export const permission: ObjectDirective = {
mounted(el: HTMLButtonElement, binding, vnode) {
if (binding.value == undefined) return
const { action, effect } = binding.value
const { hasPermission } = usePermission()
if (!hasPermission(action)) {
if (effect == 'disabled') {
el.disabled = true
el.style["disabled"] = 'disabled'
el.classList.add("n-button--disabled")
} else {
el.remove()
}
}
mounted(el: HTMLButtonElement, binding) {
if (binding.value == undefined) return;
const { action, effect } = binding.value;
const { hasPermission } = usePermission();
if (!hasPermission(action)) {
if (effect == 'disabled') {
el.disabled = true;
el.style['disabled'] = 'disabled';
el.classList.add('n-button--disabled');
} else {
el.remove();
}
}
}
},
};

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;

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