430 Commits

Author SHA1 Message Date
dap
10b8b81954 docs: version update 2025-05-16 10:06:51 +08:00
哦是吗
1f50c95c66 update apps/web-antd/src/components/upload/src/hook.ts.
fix: 上传组件清空绑定值时,同时清空innerFileList,避免外部使用时还能读取到

Signed-off-by: 哦是吗 <1733179386@qq.com>
2025-05-15 07:38:02 +00:00
zhangl1438
cd4706b717 fix:修复切换默认oss config后,上传文件报错:“文件存储服务类型无法找到” 2025-05-14 11:45:43 +08:00
dap
769aceb55f Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-13 11:32:35 +08:00
Netfan
e89cf400c0 fix: refresh command of tabbar issue, fixed: #6162 (#6169) 2025-05-12 23:34:08 +08:00
anyup
9e67929ee7 feat: support to refresh the tab page by route name (#6153)
Co-authored-by: anyup <anyupxing@163.com>
2025-05-10 22:33:31 +08:00
dap
7926865bf9 docs: version update 2025-05-09 11:12:54 +08:00
dap
51fbfcedd2 fix: 某些带Vxe表格弹窗 关闭后没有正常清理表格数据的问题 2025-05-09 11:12:24 +08:00
dap
8f71d6a5d9 docs: changelog and version update 2025-05-09 10:08:48 +08:00
afe1
90625782c0 fix: delete useless code (#6143) 2025-05-08 16:51:12 +08:00
dap
12d0ba24e5 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-08 09:28:52 +08:00
dap
540f24ed43 Revert "fix: 更新表格增加minWidth属性"
This reverts commit b52f3ba0c5.
2025-05-08 09:26:03 +08:00
Yann
c57d3f32b5 Merge remote-tracking branch 'origin/dev' 2025-05-07 22:53:51 +08:00
Yann
b52f3ba0c5 fix: 更新表格增加minWidth属性 2025-05-07 22:52:13 +08:00
wyc001122
84ef207d9c docs(@vben/docs): update settings doc (#6128)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-07 12:04:48 +08:00
zyf0624
e68fff58e8 fix: tsconfig moduleResolution (#6122)
Co-authored-by: pzzyf <2279948211@qq.com>
2025-05-07 12:04:15 +08:00
dap
63c06e02b2 fix: 修改手机号验证码长度 2025-05-07 10:31:45 +08:00
Netfan
bf70539221 fix: missing argument for getPopupContainer 2025-05-06 22:48:03 +08:00
Leeson
5949c73a30 fix: delete Popconfirm being obscured by fixed columns (#6118)
* fix: delete Popconfirm being obscured by fixed columns

* fix: opened popConfirm will prevent the table from scrolling

---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-05-06 22:33:17 +08:00
vben
cc6c9bf7a0 chore: release v5.5.6 2025-05-06 22:32:58 +08:00
Jin Mao
6b1aab9c67 fix: handle undefined children in generate-menus (#6117)
When children is undefined, use empty array as fallback to prevent potential runtime errors. This matches the behavior when hideChildrenInMenu is true.
2025-05-06 14:29:50 +08:00
LinaBell
8f4d3d418d fix: when keepAlive is enabled, returning directly through browser buttons/gestures will not close pop ups (#6113) 2025-05-06 14:02:23 +08:00
dap
aa086a2800 refactor: replace defaultHomePath 2025-05-06 09:41:11 +08:00
ming4762
3b3f8e4e44 fix: fix IconPicker props warning (#6108)
Invalid prop: type check failed for prop "onUpdate:value". Expected Function, got Array
2025-05-06 09:30:37 +08:00
dap
b0763d6429 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-04 17:23:32 +08:00
vben
f94ca10adf chore: remove prepare script from package.json 2025-05-04 07:33:36 +08:00
vben
4471bc7a5d chore: update prepare script in package.json to remove lefthook installation 2025-05-04 00:05:29 +08:00
Vben
5689ac60ff feat(project): migrate from husky and lint-staged to lefthook (#6104) 2025-05-03 19:43:12 +08:00
Vben
045bc4e5ee feat: support smooth auto-scroll to active menu item (#6102) 2025-05-03 18:05:26 +08:00
Vben
17a18fc9ba chore: close eslint object sorting (#6101) 2025-05-03 16:06:36 +08:00
aonoa
41152d1722 refactor: modify the default homepage path loaded from the preference… (#6099)
* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

---------

Signed-off-by: aonoa <1991849113@qq.com>
2025-05-03 16:03:08 +08:00
Netfan
f1af9f8f6e fix: add triggerClass binding to PopoverTrigger and update icon-picker styles (#6095)
* Popover支持设置trigger的样式
* 修正icon-picker的input值更新
2025-05-01 21:40:45 +08:00
Netfan
0517a7014f fix: add missing translation for preferences drawer (#6094) 2025-05-01 20:08:44 +08:00
Netfan
3e6d608a2f fix: destroyOnClose incorrect default value, fixed #6092 (#6093) 2025-05-01 14:09:37 +08:00
ming4762
5de954baa4 fix: fix LoginExpiredModal in some cases, message may be obscured (#6086) 2025-05-01 10:40:42 +08:00
Netfan
add1e61b6f fix: show validation message as tooltip in compact form (#6087)
* 紧凑模式表单的校验消息将显示为一个tooltip
2025-04-30 23:41:44 +08:00
dap
9f978cc9b0 fix: 用来标识是否为上传 这样在watch内部不需要请求api 2025-04-30 14:13:35 +08:00
dap
89dd4b8131 chore: version update 2025-04-30 11:26:57 +08:00
dap
a10a981fab Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-30 11:13:25 +08:00
Jin Mao
20c15f352f perf: page componet supports custom height offset for flexible content height … (#6081)
* perf: Page supports custom height offset for flexible content height control.

允许通过 height 属性调整页面内容高度计算。修改了 Page 组件以支持自定义高度偏移量,用于更灵活的内容高度控制。

* chore: typo

* perf(page): replace height with heightOffset for flexible content sizing

The `height` prop was replaced with `heightOffset` to better describe its purpose when used with `autoContentHeight`. The new prop allows custom offset values (in pixels) to adjust content area sizing, with clearer documentation.
2025-04-29 18:15:12 +08:00
Netfan
8aa7dabeff fix: calculation for collapsing search form is incorrect while initially hidden (#6068)
* 修复当默认隐藏搜索表单时,折叠位置的计算不正确的问题
2025-04-28 23:20:33 +08:00
vben
78c7c1589a chore: update readme.md 2025-04-28 23:11:34 +08:00
Vben
dd833ca56b chore: update dependencies and documentation, optimize build toolchain (#6060)
* chore: update packageManager version to pnpm@10.9.0 for compatibility improvements

* chore: Update dependent versions and configurations to improve compatibility and stability

- Update Node version to 22.1.0
- Updated pnpm version to 10.10.0
- Fixed syntax error in prettier command in lintstagedrc
- Update dependent versions in pnpm-lock.yaml to ensure consistency
- Update format and content in README documents to improve readability

* fix: lint error
2025-04-28 23:08:05 +08:00
vem
681c1dc267 fix: Update existing route index to prevent 404 on user switch (#6003)
Co-authored-by: tars-macmini <vem@qq.com>
2025-04-28 18:19:47 +08:00
Netfan
4545422ee0 fix: lock state will not change overflow style in drawer and modal (#6067)
* Modal和Drawer的锁定状态不再修改overflow样式
2025-04-28 17:02:54 +08:00
dap
5f26f5662e Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-28 13:19:57 +08:00
Gahotx
ca94ca906f fix: add rounded corners to project and quick nav items (#5296) 2025-04-27 22:50:42 +08:00
Vben
76de450c71 chore: update dependency version for improved stability and compatibility (#6023)
* chore: update dependency version for improved stability and compatibility

* fix: optimize clearPoints function in useCaptchaPoints hook to improve performance

* fix: make several props optional in various components for better flexibility
2025-04-27 22:06:49 +08:00
Trivikram Kamat
dd2b1ed580 fix: install corepack from npm (#5905)
* fix: install corepack from npm

* docs: install corepack from npm
2025-04-27 22:03:35 +08:00
ming4762
baec89f896 perf: resolve duplicate component names (#6039) 2025-04-27 22:02:38 +08:00
vben
7c7051a11e chore: release v5.5.5 2025-04-27 21:45:10 +08:00
Netfan
aa27a2f7a1 feat: encrypt the privacy data when it is persisted (#6056)
* 对私密数据持久化时执行加密
* 将锁屏密码合并到accessStore中进行加密
2025-04-27 20:59:10 +08:00
Jin Mao
9ee6d06d50 docs: add deepWiki doc link (#6057) 2025-04-27 20:54:07 +08:00
ming4762
0cc1cb5a7b perf: improve destroyOnClose for VbenDrawer&VbenModal (#6051)
* fix: fix that the default value of modal destroyOnClose does not take effect

* perf: improve destroyOnClose for VbenDrawer
2025-04-27 11:26:50 +08:00
dap
e662681ce2 fix: 拖拽上传在单文件时的样式 2025-04-27 09:57:16 +08:00
Netfan
0a9fc4e02d fix: title of search button in vxeTable toolbar (#6046)
* 修改vxeTable工具栏里的搜索按钮的提示文案
2025-04-26 01:08:41 +08:00
Netfan
be840460d8 feat: vbenSelect support prop allowClear (#6043) 2025-04-25 23:37:03 +08:00
Netfan
cb45987fe2 docs: update example (#6036)
* 跟进后端菜单逻辑的修改,现已无需传递basicLayout布局
2025-04-25 11:44:47 +08:00
panda7
5ffd7db8e0 fix: the initial value echo for the check-button-group (#6029)
Co-authored-by: sqchen <9110848@qq.com>
2025-04-25 08:35:03 +08:00
Netfan
14377705e7 fix: alert confirm state in beforeClose callback (#6019) 2025-04-23 12:20:52 +08:00
dap
23503778d4 chore: 去除vxe的锁定 2025-04-22 17:56:22 +08:00
dap
f54fab0bae Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-22 17:55:44 +08:00
pangyajun123
b985ff0584 fix: vxe-table theme token follow primary color (#6007) 2025-04-21 19:15:05 +08:00
dap
eff2f2a0b1 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-20 09:14:37 +08:00
dap
664fa800cd chore: 搜定vxe版本 2025-04-20 09:13:08 +08:00
dap
5dc4448c01 docs: changelog 2025-04-20 09:06:32 +08:00
dap
ccfe992779 chore: 暂时锁定vxe版本(样式问题) 2025-04-20 09:05:57 +08:00
dap
583504495d feat: 对模板的说明... 2025-04-20 08:53:50 +08:00
dap
7fb4bf3431 fix: 工作流list展示在开启缩放会有误差导致触底逻辑不会触发 2025-04-20 08:42:19 +08:00
wyc001122
b148b8ec92 fix: fix geader menu activation path (#5997)
Co-authored-by: 王泳超 <wangyongchao@testor.com.cn>
2025-04-19 14:35:33 +08:00
Netfan
79de6bcbf7 fix: alert send wrong confirm state to beforeClose (#5991)
* 修复alert在按下Esc或者点击遮罩关闭时,可能发送错误的isConfirm状态
2025-04-17 22:23:05 +08:00
Netfan
14bd6dd25d fix: destroyOnClose works within connectedComponent (#5989)
* 修复destroyOnClose没能销毁connectedComponent自身的问题
2025-04-17 20:25:49 +08:00
dap
9b577261e2 chore: vite6.3.1已经修复开发/打包问题 解除版本锁定 2025-04-17 14:21:47 +08:00
PIPEDREA_WZJ
7f269e0d69 Update tailwindcss.md (#5602)
tailwindcss最新的版本已经是v4.x,vben中使用的是3.x的tailwindcss。在未进行兼容前,会出现运行失败的问题
2025-04-17 14:01:39 +08:00
yuh
4baec83db5 feat: add examples: form-upload (#5955)
* feat: add examples: form-upload

* fix: upload: accept and label

* fix: upload: 设置表单值、图片预览
2025-04-17 14:00:46 +08:00
dap
7d8416890b docs: changelog 2025-04-16 21:53:29 +08:00
dap
2e2ffcd59e Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-16 21:33:11 +08:00
dap
2046bfa846 chore: 暂时锁定vite版本 会导致i18n插件打包失败 2025-04-16 19:57:07 +08:00
dap
0446adf778 refactor: 菜单图标更新 2025-04-16 17:38:11 +08:00
Netfan
f7a4d13a4c fix: fixed arguments of callbacks in formApi (#5970)
* 修复 `handleValuesChange` 传递的参数不是处理后的表单值的问题

* 修复 `handleReset` 未能传递正确参数的问题
2025-04-16 14:11:04 +08:00
dap
e587256425 update: placeholder update 2025-04-16 13:53:58 +08:00
Netfan
0936861da1 feat: pass fieldsChanged into the handleValuesChange callback function (#5968)
* fieldsChanged(已被改变值的字段名)将传入handleValuesChange回调函数
2025-04-16 11:29:01 +08:00
ming4762
3318d76bab perf: improve destroyOnClose for VbenModal (#5964) 2025-04-16 11:28:36 +08:00
LinaBell
8f3881eabf perf: beforeClose of drawer support promise (#5932)
* perf: the beforeClose function of drawer is consistent with that of modal

* refactor: drawer test update
2025-04-16 11:27:13 +08:00
zhouda1fu
5252480b09 fix: missing await in department form(#5967) 2025-04-16 11:22:59 +08:00
dap
f096dfc6e6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-16 10:09:48 +08:00
Netfan
d18f56177c docs: update alert and apiComponent docs (#5961) 2025-04-15 20:52:23 +08:00
wyc001122
333998b518 fix: determine if scrollbar has been totally scrolled (#5934)
* 修复在系统屏幕缩放比例不为100%的情况下,滚动组件对是否已滚动到边界的判断可能不正确的问题
2025-04-15 20:51:38 +08:00
ming4762
3fb4fba1cb fix: modal closing animation (#5960) 2025-04-15 18:49:57 +08:00
ming4762
c7e6210c8d feat: modal&drawer support center-footer slot (#5956) 2025-04-15 16:04:44 +08:00
lztb
d864085c13 feat: vben-form添加arrayToStringFields属性 (#5957)
* feat: vben-form添加arrayToStringFields属性

* feat: 修改handleArrayToStringFields和handleStringToArrayFields中嵌套数组格式的处理不一致

---------

Co-authored-by: 米山 <17726957223@189.cn>
2025-04-15 16:03:20 +08:00
Netfan
fcdc1a1602 feat: add more expose methods for apiComponent (#5958)
* 为ApiComponent组件添加getOptions和getValue导出方法。
2025-04-15 15:32:30 +08:00
Netfan
bf7496f0d5 feat: add useAlertContext for Alert component (#5947)
* 新增Alert的子组件中获取弹窗上下文的能力
2025-04-15 00:00:05 +08:00
Netfan
9700150653 fix: table actions in fixed column (#5945) 2025-04-14 19:56:52 +08:00
Netfan
f0e9e55af2 feat: alert support customize footer (#5940)
* Alert组件支持自定义footer
2025-04-14 11:48:21 +08:00
Netfan
ff88274554 fix: long navigation menu can be scrolled (#5939)
* 修复超长的导航菜单无法纵向滚动的问题
2025-04-14 11:18:33 +08:00
ming4762
afce9dc5c0 perf: improve destroyOnClose for VbenModal (#5935)
* perf: 优化Vben Modal destroyOnClose,解决destroyOnClose=false,Modal依旧会被销毁的问题

影响范围(重要):destroyOnClose默认为true,这会导致所有的modal都会默认渲染到body
radix-vue Dialog组件默认会销毁挂载的组件,所以即使destroyOnClose=false,Modal依旧会被销毁的问题
对于一些大表单重复渲染导致卡顿,ApiComponent也会频繁的加载数据

* fix: modal closing animation

---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-04-13 23:02:07 +08:00
ming4762
b5700bd0b1 perf: improve autoSelect of ApiComponent (#5936)
* fix: 修复autoSelect不生效的问题,props.valueField已经被omit了

* feat: ApiComponent autoSelect支持使用函数,可以满足灵活性要求更高的场景
2025-04-13 20:03:18 +08:00
dap
e085083e42 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-12 22:28:23 +08:00
dap
a47910f650 refactor: 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况) 2025-04-12 15:01:28 +08:00
Netfan
a8c4786311 feat: api-component support autoSelect prop (#5931)
* feat: api-component support autoSelect prop

* docs: add version requirement
2025-04-12 14:02:35 +08:00
Netfan
2971ccc0b7 docs: docs modal z-index fixed, update alert docs (#5930) 2025-04-12 13:41:40 +08:00
dap
4ead56eaf1 fix: onClosed 2025-04-12 10:44:53 +08:00
dap
4fad8d77de refactor: 角色管理 auto 2025-04-12 10:42:16 +08:00
dap
9db1087d32 update: 岗位 useBeforeCloseDiff 2025-04-12 10:38:14 +08:00
Netfan
4a2c7b313f fix: alert animation (#5927) 2025-04-12 10:37:47 +08:00
dap
0f5fc5f54c fix: onClosed 2025-04-12 10:34:44 +08:00
dap
76108e7b8f refactor: 宽度设置为auto(根据子元素宽度动态变化) 2025-04-12 10:31:43 +08:00
dap
6018817906 chore: version动态获取 2025-04-12 10:20:12 +08:00
dap
7e4bdf7bd6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-12 09:56:51 +08:00
dap
32117574f6 chore: version update 2025-04-12 09:54:50 +08:00
dap
a48dfa1de2 fix: 新增dictType不显示 2025-04-12 09:44:27 +08:00
Netfan
36bf6fc149 fix: builtin color change throttled in preference drawer (#5924)
修复偏好设置中的自定义主题色拖动选择颜色时页面会明显卡顿的问题
2025-04-12 01:44:08 +08:00
Netfan
f46ec30995 fix: theme mode follow the system only auto (#5923)
* 修复主题在未设置为auto时,仍然会跟随系统主题变化的问题。
2025-04-12 01:16:57 +08:00
Netfan
9bd5a190c2 fix: alert action button focus, fixed #5921 (#5922)
* 修复Alert组件的按钮焦点切换问题
2025-04-12 00:59:56 +08:00
dap
4dc7543bb6 docs: changelog 2025-04-11 13:29:27 +08:00
dap
d8e7945f9f docs: changelog 2025-04-11 13:24:34 +08:00
dap
2fd1fdcb32 refactor: 更改header参数ClientID命名 2025-04-11 11:33:23 +08:00
zhang
86da3cedc2 chore: 导出框架自带的组件,方便独立页面使用 (#5876) 2025-04-09 16:16:56 +08:00
dap
44ba945a12 fix: 无法点击遮罩关闭 2025-04-09 15:07:14 +08:00
dap
2680101872 fix: 无法点击遮罩关闭 2025-04-09 15:06:00 +08:00
dap
1c2e27613c refactor: 富文本/上传同步改为异步组件导入 2025-04-09 10:03:47 +08:00
dap
3e7a2336b0 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-09 09:34:17 +08:00
dap
022d5182d7 fix: onCancel -> onClosed 2025-04-09 09:22:13 +08:00
Netfan
329a176a5c perf: optimize bootstrap modules to speed up first-screen loading (#5899)
优化首屏加载速度
2025-04-09 01:05:20 +08:00
dap
41962ef380 docs: changelog 2025-04-08 21:07:09 +08:00
dap
9003df713c fix: vxe新版需要单独设置headerCellConfig 2025-04-08 21:04:01 +08:00
dap
ebb4738be7 refactor: 流程定义 useBeforeCloseDiff 2025-04-08 20:58:09 +08:00
dap
ad7c33a7d6 refactor: 流程分类 useBeforeCloseDiff 2025-04-08 20:55:34 +08:00
dap
a114335a56 refactor: oss配置 useBeforeCloseDiff 2025-04-08 20:53:44 +08:00
Netfan
9379093a4f feat: customizable table separator (#5898)
* 表格的分隔条支持定制背景色或完全移除
2025-04-08 20:28:50 +08:00
ming4762
c9014d0338 perf: 优化关闭页面切换动画的tab切换性能 (#5883) 2025-04-08 20:27:03 +08:00
dap
b8ec8edb38 update: 字典 colorpicker 2025-04-08 19:33:35 +08:00
Netfan
ed26dca64e chore: update pnpm-lock.yaml 2025-04-08 16:31:41 +08:00
Netfan
08c6496e24 chore: update deps 2025-04-08 14:56:40 +08:00
Netfan
a8c5df38e9 fix: possible circular reference issue during build (#5894)
* 修复构建期间出现的循环引用警告
2025-04-08 14:50:05 +08:00
dap
5b9f647cfd update: [vxe table v4.12.5] 参数 "row-config.height" 已废弃,请使用 "cell-config.height" 2025-04-08 13:29:06 +08:00
dap
ae6bf6ee53 refactor: 用户drawer Promise逻辑重构 2025-04-08 12:03:15 +08:00
dap
77894d5df4 update: i18n更新 2025-04-08 11:09:07 +08:00
dap
ba8f36a2c0 update: 移除老版本的不需要组件/代码 2025-04-08 11:04:12 +08:00
dap
133abe9ded refactor: 角色权限 useBeforeCloseDiff 2025-04-08 11:02:36 +08:00
dap
ef390ae636 refactor: 租户套餐useBeforeCloseDiff 2025-04-08 10:57:08 +08:00
dap
6d2f4e8486 refactor: 租户管理 useBeforeCloseDiff 2025-04-08 10:54:28 +08:00
dap
c4962aaf85 refactor: 客户端管理useBeforeCloseDiff 2025-04-08 10:51:16 +08:00
dap
f7128b099e refactor: 通知公告 useBeforeCloseDiff 2025-04-08 10:47:18 +08:00
dap
5510b6dea4 refactor: 字典useBeforeCloseDiff 2025-04-08 10:40:32 +08:00
dap
98f658d46f refactor: 部门管理useBeforeCloseDiff 2025-04-08 10:34:26 +08:00
dap
e307db2f3d refactor: useBeforeCloseDiff 2025-04-08 10:30:56 +08:00
dap
e6dab8300d refactor: 角色管理 useBeforeCloseDiff 2025-04-08 10:22:21 +08:00
dap
eb9f278e7f refactor: useBeforeCloseDiff 2025-04-08 10:10:15 +08:00
dap
34e5812de9 update: vxe active color 2025-04-07 19:37:11 +08:00
dap
07587c0faf update: 用户管理 表单更新(非最终方案) 2025-04-07 19:02:28 +08:00
dap
88316d7498 refactor: useBeforeCloseDiff逻辑更新 2025-04-07 18:48:46 +08:00
dap
53e02d46c2 docs: changelog 2025-04-07 18:44:42 +08:00
dap
5e1de6fc79 fix: 表格固定高度 getVxePopupContainer 2025-04-07 18:41:23 +08:00
dap
7463df053a update: 去除字典动画 2025-04-07 18:25:21 +08:00
dap
1286b52135 fix: getVxePopupContainer 2025-04-07 17:21:49 +08:00
dap
92fe406ae9 update: 字典loading 2025-04-07 17:20:41 +08:00
dap
5b72d9b79d refactor: 移除deepWatch参数 2025-04-07 13:05:30 +08:00
dap
b97fe47afd fix: 直接使用.value无法触发useForm的更新(原生是正常的) 需要修改地址 2025-04-07 12:53:20 +08:00
dap
4f2354b53a update: 兼容以前代码 先返回body 这样会造成无法跟随滚动 2025-04-07 11:10:10 +08:00
dap
8f9006c96d fix: vxe 右上角toolbar按钮色/翻页主题色保持一致 2025-04-07 10:58:51 +08:00
Netfan
71e8d12b70 fix: improve prompt component (#5879)
* fix: prompt component render fixed

* fix: alert buttonAlign default value
2025-04-07 01:21:30 +08:00
dap
ae5d45763f refactor: getVxePopupContainer与新版Vxe不兼容 先返回body(会导致滚动不跟随)后续版本再优化 2025-04-06 17:41:00 +08:00
dap
3f037f146b refactor: getVxePopupContainer 2025-04-06 17:24:18 +08:00
dap
63db3ba143 update: wechat group image 2025-04-06 15:40:56 +08:00
dap
17ca1ef1b2 update: align 2025-04-06 15:36:14 +08:00
dap
203c2edf63 fix: dropdown 2025-04-06 13:16:45 +08:00
dap
09e0721db7 fix: dropdown 2025-04-06 13:15:04 +08:00
dap
3c2a52057e docs: changelog 2025-04-06 12:40:00 +08:00
dap
06dd17eac3 update: getVxePopupContainer选择器更新(适配新版) 2025-04-05 23:07:45 +08:00
dap
0e3eb887da update: catch 2025-04-05 22:57:52 +08:00
dap
1b2ded7421 update: 修改zIndex 2025-04-05 22:54:45 +08:00
dap
7bc36d2b84 fix: 租户选择下拉框会跟随body滚动 2025-04-05 22:52:48 +08:00
dap
21fb9c8c99 docs: 头像裁剪 私有桶会拼接timestamp参数导致sign计算异常无法上传 2025-04-05 17:12:26 +08:00
dap
84d6559f25 fix: 头像异常时无法一直Loading 无法裁剪 2025-04-05 16:51:24 +08:00
dap
78c16c1016 update: TinyMCE上传失败移除图片 2025-04-05 15:09:22 +08:00
dap
7fc284a609 fix: TinyMCE zIndex 2025-04-05 14:48:42 +08:00
dap
29c062a4a8 refactor: [vxe table v4.12.5] 参数 "row-config.height" 已废弃,请使用 "cell-config.height" 2025-04-05 14:41:20 +08:00
dap
e1aa1f7636 refactor: 流程分类 align left 2025-04-05 14:37:02 +08:00
dap
eae24d8d83 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-05 14:29:10 +08:00
dap
88b3208afb update: 使用lock+try/catch 2025-04-05 13:38:09 +08:00
dap
26587ac09a update: 弹窗表单数据更改关闭时的提示框(可能最终不会加入) 测试页面: 参数管理 2025-04-05 13:33:15 +08:00
dap
b4d038e22f update: 还原loading逻辑 2025-04-05 13:07:57 +08:00
Netfan
d216fdca44 feat: support logo text slot (#5872)
* 基础布局中的LOGO的文字区域允许通过插槽logo-text定制
2025-04-05 13:07:52 +08:00
wyc001122
384c5d7dbb fix: 布局为双列菜单或者水平模式下, 一级菜单高亮问题 (#5870)
Co-authored-by: 王泳超 <wangyongchao@testor.com.cn>
2025-04-05 11:04:59 +08:00
dap
63630b17c1 update: 租户切换Select增加loading 2025-04-05 00:49:57 +08:00
dap
a2ed3fa48b refactor: TinyMCE组件重构 移除冗余代码/功能 增加loading 2025-04-04 22:55:07 +08:00
dap
104039cdfb docs: changelog 2025-04-04 21:19:33 +08:00
dap
16f033aa8f docs: changelog 2025-04-04 21:13:35 +08:00
dap
0c0988404e refactor: ContentTypeEnum使用const替代enum 2025-04-04 21:12:54 +08:00
dap
1a16c520a1 refactor: DictEnum使用const替代enum 2025-04-04 21:11:33 +08:00
dap
e52fec291a update: 移除不需要的description schema 2025-04-04 21:06:57 +08:00
dap
aeed0fd48e refactor: 重构操作日志drawer 2025-04-04 21:06:16 +08:00
dap
542407dcd6 refactor: 用户信息重构(Description) 2025-04-04 20:13:43 +08:00
dap
123f234971 refactor: 重置密码使用原生重构(Descriptions) 2025-04-04 19:55:49 +08:00
dap
2ca39dfdbb Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-04 19:42:43 +08:00
Netfan
b0ad08dbbc feat: use the not-found component instead of the invalid route component in the backend mode (#5871)
* 后端菜单模式下,使用not-found组件代替无效的路由组件
2025-04-04 15:21:09 +08:00
Rascal-Coder
3600603016 fix: vxeGrid height fixed #5861 (#5862) 2025-04-04 13:16:31 +08:00
dap
116fe39b8d fix: 兼容最新版本vxe的勾选api 2025-04-04 01:46:53 +08:00
superdl1996
cde1a85394 docs: typo (#5855) 2025-04-03 19:11:40 +08:00
dap
fe53c33821 update: 更新日志的加载效果 2025-04-03 17:59:31 +08:00
dap
bac71a30f0 refactor: 更新记录 菜单 2025-04-03 17:09:39 +08:00
dap
ec82510f49 docs: 更新日志 2025-04-03 15:43:58 +08:00
dap
f1c4ed1412 update: useDescription deprecated 2025-04-03 15:29:30 +08:00
dap
36683dd218 update: 兼容旧版本上传 增加ImageUploadOld/FileUploadOld(下个版本将移除) 2025-04-03 15:26:00 +08:00
dap
2577ba5500 update: commit-lint增加update: 2025-04-03 15:25:05 +08:00
dap
b5150b5863 refactor: 登录信息 使用原生重构 2025-04-03 15:02:26 +08:00
dap
5d47026908 docs: changelog 2025-04-03 14:03:38 +08:00
dap
63e13069ea Merge 2025-04-03 14:02:12 +08:00
dap
56e7e840b3 feat: 验证码loading 2025-04-03 13:57:32 +08:00
dap
2d58a2172d refactor: 附件上传改为更新后 2025-04-03 11:52:17 +08:00
Netfan
c623604ea9 docs: fix alert demo in docs 2025-04-02 20:37:41 +08:00
dap
75d7a607f5 refactor: 使用原生组件重构redis-info 2025-04-02 19:24:57 +08:00
dap
354ff7ecf6 chore: 更新文案 2025-04-02 19:05:54 +08:00
Netfan
7933da8f66 chore: update deps (#5854) 2025-04-02 18:07:06 +08:00
dap
38d39d5e3d refactor: 修改为computed 支持语言切换 2025-04-02 16:46:31 +08:00
Netfan
ecf518bb02 fix: alert beforeClose callback arguments fixed (#5845) 2025-04-01 22:55:29 +08:00
dap
98e3a4a34c refactor: modalLoading/drawerLoading改为调用内部的lock/unlock方法 2025-04-01 20:36:49 +08:00
dap
362bc84cfb refactor: 管理员租户切换不再返回首页 直接刷新当前页(除特殊页面外会回到首页) 2025-04-01 19:59:28 +08:00
dap
b67be83a19 docs: 更新注释 2025-04-01 19:10:45 +08:00
dap
af118cef71 docs: changelog 2025-04-01 18:38:16 +08:00
dap
bc2beefa7e fix: 字典重新登录unknown的情况 2025-04-01 18:36:41 +08:00
dap
44ad2c5f8d Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-01 17:16:13 +08:00
ming4762
1d9f1be004 fix: 解决AccessModeType:backend登录过期,重新登录不会重新生成路由的问题,重现步骤分析: (#5830)
1、长时间未登录登录过期,再次打开页面构开始生成动态路由
2、fetchMenuListAsync后台返回401登录过期:doReAuthenticate函数跳转到登录页面
3、异常被拦截,return []
4、gurad.ts accessStore.setIsAccessChecked(true); 被错误的标识为已生成路由
5、重新登录后,accessStore.isAccessChecked=true未能正确的重新生成路由
2025-04-01 15:50:45 +08:00
Netfan
44138f578f feat: add preset alert, confirm, prompt components that can be simple called (#5843)
* feat: add preset alert, confirm, prompt components that can be simple called

* fix: type define
2025-04-01 15:10:18 +08:00
dap
13951a0caf refactor: 修改默认radius 与antd保持一致 2025-03-31 21:58:22 +08:00
dap
a84a713eaa Revert "chore: 打包最大内存配置为4G"
This reverts commit 308853cce1.
2025-03-31 19:31:43 +08:00
Joeshu
0e3abc2b53 docs: add third-party libraries to check update methods (#5819) 2025-03-31 19:28:28 +08:00
Arthur Darkstone
a96be3db98 docs: add explanation and related script configuration to distinguish build environment (#5826)
* docs: add explanation and related script configuration to distinguish build environment

* docs: fix spell error
2025-03-31 19:28:02 +08:00
dap
308853cce1 chore: 打包最大内存配置为4G 2025-03-31 19:24:25 +08:00
dap
0a19ec3122 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-31 15:20:14 +08:00
dap
ebc571e13f feat: list-type: picture-card动画效果关闭样式 2025-03-31 15:14:48 +08:00
dap
2b7713323e docs: update wechat group 2025-03-31 12:46:14 +08:00
Netfan
d6f239c564 docs: fix api-component demo link (#5828) 2025-03-31 12:08:45 +08:00
Netfan
166e9a0e82 chore: add demo for apiComponent with caching and concurrency (#5827)
* chore: add demo for apiComponent with caching and concurrency

* docs: update api component docs
2025-03-31 11:51:57 +08:00
dap
c0a5942c2a fix: 单文件查询到会走多文件的判断 2025-03-31 10:47:30 +08:00
dap
bc9e3a50e1 chore: list-type的提示 2025-03-31 10:26:06 +08:00
Netfan
06ccad9db0 fix: vbenTree modelValue synchronization (#5825) 2025-03-31 10:18:35 +08:00
Jin Mao
18722ce434 feat: sidebar button config (#5818)
* feat: 新增 PreferenceCheckboxItem 组件

* feat(preferences): 添加侧边栏按钮配置功能

* feat: 新增按钮点击事件触发功能

* feat(SidebarPreferences): 新增侧边栏折叠按钮与固定按钮配置

* feat(ui): 新增侧边栏固定按钮及配置选项

* fix(test): 修正侧边栏配置项缺失问题
2025-03-31 10:17:42 +08:00
Netfan
a0feeb1966 fix: watermark settings in the preferences modified accidentally (#5823) 2025-03-31 09:06:02 +08:00
dap
825c2837ea docs: 更新说明 2025-03-30 23:52:51 +08:00
dap
23ff03d40c feat: 自定义预览图/文件名 2025-03-30 21:57:01 +08:00
dap
755a30583f feat: 上传list-type 2025-03-30 18:23:37 +08:00
dap
a302fdf119 feat: TableSwitch增加切换前确认Modal(默认false) 2025-03-30 16:53:12 +08:00
Jin Mao
df6341f0b8 feat(tabbar): 添加右键菜单过滤功能 (#5820) 2025-03-30 16:23:24 +08:00
dap
062e999f35 refactor: TableSwitch组件重构 2025-03-30 14:43:37 +08:00
dap
6c4d15136f feat: 上传emit 2025-03-30 13:55:02 +08:00
dap
f16afe657e feat: 是否在组件Unmounted时取消上传 2025-03-30 12:59:18 +08:00
dap
b9843c6faf refactor: 图片/文件上传都支持拖拽 2025-03-30 12:32:58 +08:00
dap
621f79e7d8 chore: singleImageId 2025-03-29 23:02:40 +08:00
dap
467d337515 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-29 22:53:47 +08:00
anyup
dbc0b7e4a9 fix: route root.children duplicate problem (#5755)
Co-authored-by: anyup <anyupxing@163.com>
2025-03-29 22:38:30 +08:00
zhang
aa2907323f style: 更正引用格式 (#5784) 2025-03-29 22:29:16 +08:00
dap
3655cae900 feat: 自定义accept显示 2025-03-29 22:13:58 +08:00
dap
60c398df39 refactor: 图片上传自定义预览逻辑 2025-03-29 21:59:17 +08:00
dap
69222807a4 feat: 文件上传查询不到ossId的丢弃策略 2025-03-29 21:48:46 +08:00
dap
b3e2d758f6 feat: deepWatch参数 2025-03-29 21:24:11 +08:00
Netfan
96d2bc52e9 feat: pre-set serialization methods for request parameters (#5814)
添加快捷设置请求参数序列化方法的配置
2025-03-29 19:21:21 +08:00
dap
f4a88efb0f refactor: 修改oss对应的上传代码 2025-03-29 16:22:08 +08:00
dap
b78b599a06 feat: helpMessage插槽 2025-03-29 16:13:48 +08:00
dap
ffcc21975e refactor: 文件/图片上传重构 2025-03-29 15:52:11 +08:00
dap
dd57e3c9ae feat: 支持拖拽上传 disabled样式优化 2025-03-28 18:02:54 +08:00
dap
8c1cd617ad refactor: 文件上传/图片上传重构(破坏性更新 不兼容之前的api) 2025-03-28 17:24:46 +08:00
Netfan
e91e4e0eea docs: fix form compact docs (#5811)
* docs: fix form `compact` docs

* docs: remove `compact` from FormCommonConfig
2025-03-28 16:15:35 +08:00
Netfan
c2b5f6497d fix: vben tree component warning (#5809) 2025-03-28 16:01:30 +08:00
dap
456f0e1112 style: 字典管理 字典类型 表格选中行增加bold效果 2025-03-27 21:45:35 +08:00
dap
e5fa32bbae style: 字典选中 高亮行 2025-03-27 21:41:32 +08:00
dap
024087b9b2 fix: 测试菜单 请假申请 选中删除 需要根据状态判断 2025-03-27 21:03:55 +08:00
dap
c0476613d7 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-27 19:27:26 +08:00
Netfan
3c2d325d8c perf: improve api-component for using in form (#5796) 2025-03-27 15:51:11 +08:00
Netfan
a77bb8e68d perf: improve component packaging to enable instance method retrieval (#5795)
改进组件适配器里的包装函数,使得组件暴露的方法可以透传
2025-03-27 15:13:13 +08:00
Netfan
870cd86393 fix: auto check parent after node selected (#5794) 2025-03-27 14:22:05 +08:00
dap
f5fada20e6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-27 13:25:23 +08:00
Netfan
0b650367f3 fix: popover background color in dark mode (#5783)
* 修复dark主题下的弹出层背景色在某些浏览器上表现为完全透明的问题
2025-03-25 21:17:55 +08:00
zhang
1616a06bfd perf: 优化多文件上传入参是数组的情况 (#5757)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-03-25 10:26:19 +08:00
dap
b97c83fdf3 refactor: 修改qq - icon 2025-03-24 13:23:56 +08:00
dap
bb5ad57d9c Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-23 16:51:50 +08:00
dap
a8f20a2baa Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-23 16:51:34 +08:00
dap
0e7b76d5ed feat: 角色管理 勾选权限组件添加对错误用法的校验提示 2025-03-23 14:31:55 +08:00
dap
877cc06eff docs: changelog 2025-03-23 13:37:06 +08:00
dap
ba6785931d refactor: 修改为setup形式 2025-03-23 13:30:40 +08:00
dap
731c9be4f1 docs: 建议通过菜单配置成外链/内嵌 达到相同的效果且灵活性更高 2025-03-23 12:25:19 +08:00
dap
e546c21ad6 refactor: 修改为ts写法 2025-03-23 12:23:00 +08:00
dap
22ff5bddae refactor: 修改为ts写法 2025-03-23 12:19:29 +08:00
dap
b2c66c07b4 style: 不需要的样式 2025-03-23 12:13:25 +08:00
dap
cd110433c1 chore: 改为setup写法 优化样式 2025-03-23 12:11:46 +08:00
Netfan
3f0d30897f fix: menu drawer can not be closed after a successful submitting (#5770) 2025-03-23 11:34:21 +08:00
Jin Mao
66c1d390b9 feat(ui): logo icon support click events (#5725)
* feat(ui): 扩展auth页面添加点击 Logo 的事件处理

在 `authentication.vue` 中新增 `clickLogo` 属性,允许在点击 Logo 时执行自定义操作。在 `auth.vue` 中实现了一个示例的点击事件处理函数,用于测试该功能。

* feat(layout): 添加点击 logo 的事件处理函数

在 BasicLayout 组件中添加了 clickLogo 事件处理函数,并通过 emit 方法触发 clickLogo 事件。同时,在 basic.vue 中实现了 handleClickLogo 函数,用于处理 logo 点击事件。

* fix(ui): 移除logo点击事件的控制台日志
2025-03-23 10:02:22 +08:00
dap
6c942418b4 refactor: 重构oauth相关代码 添加新的默认oauth登录方式 2025-03-23 01:23:30 +08:00
dap
db955071d7 refactor: 默认租户id提取为常量 2025-03-23 00:59:07 +08:00
dap
17e82fb766 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-22 18:31:19 +08:00
Netfan
03ceb2aac5 fix: default value for nested fields (#5763) 2025-03-21 16:06:14 +08:00
dap
f3e455c8d3 fix: 修改accept类型 解决无法拖拽上传 2025-03-21 10:31:22 +08:00
Netfan
39888cebaa fix: base component focus color in form (#5760) 2025-03-21 09:41:42 +08:00
Netfan
efb69fc75f feat: add form-is-required class for required items. fixed: #5739 (#5759) 2025-03-21 09:25:38 +08:00
Jin Mao
711a179c69 chore: update codeowners (#5750) 2025-03-21 08:37:21 +08:00
Netfan
3133f8f8b9 fix: table form reset will trigger reload twice while submitOnChange set (#5756)
* 修复表格的搜索表单在重置时,可能会触发2次表格刷新的问题
2025-03-20 19:42:22 +08:00
anyup
f0a43912d1 fix: sort the menu so that it doesn't get replaced with 999 when order=0 (#5753)
Co-authored-by: anyup <anyupxing@163.com>
2025-03-20 19:41:46 +08:00
dap
949004c67f docs: readme update 2025-03-19 19:57:15 +08:00
dap
feb6229383 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-19 19:56:52 +08:00
dap
614d998daf fix: 草稿不允许查看 2025-03-19 16:19:03 +08:00
dap
9e4f886197 fix: 草稿不允许查看 2025-03-19 15:40:17 +08:00
dap
18f2b84093 fix: 自定义数据权限 宽度不正常 2025-03-19 11:04:17 +08:00
dap
25616baa1d fix: 备注宽度不正常 2025-03-19 11:00:04 +08:00
vben
b92ac5c36d chore: release 5.5.4 2025-03-18 21:43:27 +08:00
chen-d-yu
504070f3eb fix: vsh stylelint script fixed (#5729) 2025-03-17 09:23:27 +08:00
dap
7032f79069 style: modal fullscreen 2025-03-16 15:48:58 +08:00
dap
a0b5aaa4dd style: 移除select样式 2025-03-15 17:19:26 +08:00
dap
7230b94b16 style: 登录页input/select样式 2025-03-15 17:05:45 +08:00
dap
cca456aa82 refactor: 验证码样式优化 2025-03-15 16:56:33 +08:00
dap
5310bddc1c chore: 增加环境变量打包配置demo -> build:antd:test 2025-03-15 16:25:13 +08:00
dap
2e064604c1 fix: withDefaultPlaceholder中将placeholder修改为computed, 解决后续使用updateSchema无法正常更新显示placeholder(响应式问题) 2025-03-14 16:09:37 +08:00
dap
f415664acf docs: changelog 2025-03-13 14:13:17 +08:00
dap
786b617179 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-11 13:38:22 +08:00
dap
e6ee1f57b4 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-11 13:38:13 +08:00
Netfan
feab6b3b30 fix: form item style adjustment (#5694) 2025-03-11 02:47:06 +08:00
dap
7f1548b343 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-10 19:17:57 +08:00
Netfan
2d4ac33046 fix: miss default value in vbenLayout 2025-03-10 18:59:56 +08:00
Netfan
17e2a02281 feat: auto set component name for keep-alive (#5690)
* fix: auto set component name for keep-alive

* fix: type define
2025-03-10 16:25:30 +08:00
Netfan
096545c5a1 docs: update table slots docs 2025-03-10 10:53:17 +08:00
Netfan
04dff33ac5 feat: improved formApi for component instance support
* 改进表单API以支持组件实例的获取,以及焦点字段的获取
2025-03-10 02:56:44 +08:00
Netfan
cfa18c2b8e fix: improve component repackaging 2025-03-10 02:56:44 +08:00
Netfan
13354955db chore: update depts 2025-03-10 02:56:44 +08:00
dap
056dee009f Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-09 15:29:45 +08:00
dap
1309c8425e docs: 关于表单 2025-03-09 14:24:55 +08:00
dap
f0ded13df1 refactor: 重构 2025-03-09 14:07:25 +08:00
dap
e78d367cea feat: 请假申请-排他并行网关 2025-03-09 13:50:58 +08:00
ijackwu
bb683804f4 fix: live-server set port use [--port=PORT] (#5687) 2025-03-09 08:49:06 +08:00
dap
209214f6a3 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-08 20:25:01 +08:00
dap
ab003826a3 Merge branch 'native_form' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev 2025-03-08 20:05:47 +08:00
dap
817c4a265d Merge branch 'generator' of https://gitee.com/dapppp/ruoyi-plus-vben5 into native_form 2025-03-08 20:01:26 +08:00
dap
8ed893fb21 chore: 注释说明 2025-03-08 19:57:56 +08:00
dap
55f5e6bd0c feat: 代码生成 支持选择表单生成类型(需要模板支持) 2025-03-08 16:00:32 +08:00
dap
62d03605a3 feat: 流程发起时的按钮权限 2025-03-08 12:18:16 +08:00
dap
6170da0870 feat: 选择下一步审批人权限 2025-03-08 12:08:01 +08:00
Netfan
e2a577de24 feat: add size prop to avatar component and update logo component for size handling (#5684) 2025-03-08 11:37:02 +08:00
Netfan
89d963c81a fix: vxeTable search button not working with slot (#5678) 2025-03-07 22:35:09 +08:00
dap
a9b7bf6442 refactor: 更新注释 2025-03-07 20:48:12 +08:00
dap
a66e13eca6 refactor: 通知公告 原生表单(非最终确定版) 2025-03-07 20:29:27 +08:00
Netfan
b37ed48b9d feat: role management page with component tree (#5675)
* feat: add shadcn tree

* fix: update vbenTree component

* feat: role management demo page

* feat: add cellSwitch renderer for vxeTable

* chore: remove tree examples
2025-03-07 16:03:08 +08:00
dap
e78f4e984d Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-07 15:57:38 +08:00
dap
9f70a61c24 feat: 按钮权限 2025-03-07 15:50:39 +08:00
Netfan
4b9cfcb867 fix: demo nested menu path (#5667)
* 修复演示的嵌套菜单path配置导致的面包屑跳转问题
2025-03-06 22:48:54 +08:00
Netfan
f86c9f90ad fix: keepAlive not working for popup appendToMain (#5666)
* 修复弹窗和抽屉 `appendToMain` 时且启用`keepAlive` 时未能正确缓存的问题
2025-03-06 22:22:45 +08:00
dap
3229899c40 fix: 错误的国际化文案 2025-03-06 17:54:00 +08:00
Netfan
31a6ab59fb feat: vben checkbox support indeterminate state and transition animation (#5662) 2025-03-06 16:11:02 +08:00
dap
11083d5b7e refactor: remove 'less' (迁移v2代码漏掉了) 2025-03-05 19:29:34 +08:00
dap
7b5fb4f164 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-05 10:10:09 +08:00
Netfan
34789645f7 fix: nitro server cookie maxAge fixed 2025-03-04 22:29:27 +08:00
Netfan
f380452ef0 feat: modal and drawer locking improve (#5648)
* feat: add `unlock` for modalApi

* fix: modal's close button style in locking

* fix: fix modal's close button disabled on locking

* feat: add `lock` and `unlock` for drawerApi
2025-03-04 22:00:32 +08:00
jasonz18
decd9c55e5 docs: typo 2025-03-04 21:40:34 +08:00
dap
d8cac9bb00 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-03 13:16:50 +08:00
dap
1dacd96e3c fix: 重复的tooltip help 2025-03-03 11:48:17 +08:00
Netfan
e815f0ff89 docs: vbenVxeTable slots docs update 2025-03-01 22:16:36 +08:00
dap
43534b6142 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-03-01 15:22:35 +08:00
Netfan
5ea6b4a8d8 fix: logo style in login page is affected by the globally-imported antd styles
* 修复登录页左上角LOGO部分的文字在全局导入antd样式的时候位置不正确的问题
2025-02-28 22:39:42 +08:00
Netfan
a53ca3faf1 chore: update depts 2025-02-28 20:30:59 +08:00
Netfan
86fdd6c93b fix: drawer close icon placement default value 2025-02-28 14:54:11 +08:00
Netfan
0e0661fe02 fix: breadcrumb style is affected by the globally-imported antd styles (#5627)
* 修复全局引入Antd时,面包屑的样式会受到影响的问题
2025-02-27 22:28:59 +08:00
Netfan
86ce65e0ea fix: hideChildrenInMenu demo code (#5626) 2025-02-27 20:21:48 +08:00
Netfan
c3eb4fab13 docs: fix zod rules docs 2025-02-27 17:27:00 +08:00
jinmao88
7a476372e1 fix: useDrawer中closeIconPlacement设置无效 (#5624) 2025-02-27 14:34:42 +08:00
Netfan
5e421ce607 chore: demo page menu management (#5619)
* 添加菜单管理演示页面
2025-02-27 01:22:25 +08:00
dap
279fc98d76 refactor: items-baseline -> items-start 2025-02-26 13:16:07 +08:00
dap
36a78dda90 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-02-26 09:17:03 +08:00
Netfan
1d8676f456 chore: remove sleep in department list api 2025-02-25 22:15:27 +08:00
Netfan
0c3dd92592 fix: getPopupContainer will return closet form first (#5612) 2025-02-25 22:07:56 +08:00
Netfan
d33261d0c2 chore: demo page for system/department (#5611)
* feat: department management demo

* perf: department page improve

* feat: demo api middleware

* fix: add losing import
2025-02-25 19:47:45 +08:00
dap
4da1bb9896 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-02-25 09:22:24 +08:00
Netfan
7041c6a106 chore: output console error for invalid route component (#5593) 2025-02-24 16:03:52 +08:00
littlesparklet
12ffb310bf fix: Fix inconsistent spacing around search form (issue #5429) (#5495) 2025-02-24 15:57:50 +08:00
Netfan
d9799fec70 fix: search take no effect in icon-picker with antd (#5592) 2025-02-24 14:13:53 +08:00
Netfan
4570d5b54b feat: add VbenButtonGroup and VbenCheckButtonGroup with demo (#5591)
* 添加按钮组、选择按钮组以及相应的Demo
2025-02-24 13:50:50 +08:00
Netfan
d49e3e81a4 fix: loading and spinner style fixed and improved (#5588) 2025-02-23 15:30:17 +08:00
Netfan
579b1b486c feat: loading and spinner component with directive (#5587)
* 添加loading和spinner组件,以及对应的vue指令
2025-02-23 12:41:54 +08:00
Netfan
eba372062e feat: improve form demo (#5582) 2025-02-21 12:07:32 +08:00
Netfan
c9ccd2bbab fix: form label and control style (#5580)
* fix: form label and control style

* fix: empty label mark with required rules
2025-02-21 11:14:59 +08:00
handsomeFu
5aff8bac10 fix: CountTo component resolve separator prop not taking effect (#5578) 2025-02-21 11:06:18 +08:00
Netfan
1a12687027 fix: vben count to animator event name fixed (#5573) 2025-02-20 23:53:47 +08:00
Netfan
a221d2b491 fix: form item overflow fixed and layout improved (#5572)
* fix: form item overflow fixed and layout improved

* fix: basic form demo update

* feat: form label support render

* fix: form docs update
2025-02-20 23:05:08 +08:00
dap
98d5d607b6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-02-20 18:26:39 +08:00
dap
9ee5369f35 fix: replace ?? to || for fix avatar 2025-02-20 18:25:08 +08:00
anyup
ccd99eb24d fix: solve the problem of inconsistent returns of formSchema custom field names when code login (#5563) 2025-02-20 09:09:32 +08:00
Netfan
c5c6760b5d chore: eslint rules update 2025-02-18 15:41:16 +08:00
dap
fbb0d641db Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-02-18 14:08:05 +08:00
Netfan
c07281bf41 fix: form item slot context fixed (#5552)
* 修复表单插槽
2025-02-17 21:37:05 +08:00
Netfan
24bad09c74 refactor: new CountTo component with demo (#5551) 2025-02-17 21:16:10 +08:00
Netfan
cddf71e600 fix: playground route missing 2025-02-17 17:57:15 +08:00
Netfan
9f82052c71 feat: demo of motion plugin (#5550)
添加Motion的用法例子
2025-02-17 15:25:45 +08:00
Netfan
e0eb57d38d fix: nitro server cors support with cookie (#5549)
* 修复nitro server在使用cookie时的跨域配置
2025-02-17 15:17:31 +08:00
Netfan
b6b97accb1 feat: add more event for jsonViewer (#5546)
* 为JsonViewer添加事件支持
2025-02-17 10:41:09 +08:00
Netfan
799934171a style: code style fixed 2025-02-16 23:32:06 +08:00
Netfan
10ebf03698 fix: auth api definition 2025-02-16 23:06:20 +08:00
Netfan
cd258fbb52 chore: update deps 2025-02-16 23:03:41 +08:00
Netfan
6cba181fad feat: new component jsonViewer (#5544)
* 添加新组件JsonViewer用于展示JSON结构数据
2025-02-16 22:57:00 +08:00
jinmao88
f9504cece3 chore: add qq group 5 (#5530) 2025-02-13 14:45:51 +08:00
Netfan
182f1c9da8 fix: userDropdown triggered unnecessary while overlay shown (#5520)
* 修复顶部的用户资料下拉在弹窗被打开时,仍然可以被触发的问题
2025-02-12 17:59:59 +08:00
dap
6e0c79411b docs: readme 2025-02-12 17:59:55 +08:00
dap
de27e8691d Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-02-12 17:51:25 +08:00
Netfan
e7b009786b fix: width for ellipsisText tooltip in popover content (#5517)
* 修复省略文本用在气泡中时,提示弹出层的宽度计算有误的问题
2025-02-12 14:25:12 +08:00
dap
5e7aeaf12e feat: 代码生成支持路径方式生成 2025-02-08 19:53:12 +08:00
dap
bc6818f531 docs: version update 2025-02-07 14:43:46 +08:00
dap
f78bc4e4f7 chore: 修改路径 2025-02-07 14:42:47 +08:00
dap
cd77063f68 fix: 加密后修改请求头会造成报错HttpMediaTypeNotSupportedException 2025-02-07 11:45:57 +08:00
dap
0be1a0825d perf: 去除顶部进度条样式 2025-02-06 22:44:05 +08:00
dap
551841bdd7 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-02-06 22:38:05 +08:00
Netfan
5262233312 feat: tabbar support max count limit (#5490)
* 标签栏支持限制打开的最大数量
2025-02-06 19:33:10 +08:00
dap
52dc3e1788 perf: 加密后修改content-type 2025-02-06 15:35:33 +08:00
Netfan
a9f9031f49 docs: update form docs (#5485) 2025-02-06 09:45:28 +08:00
dap
f7c00cd8f7 refactor: 去除不需要的css样式 2025-02-05 19:48:56 +08:00
dap
bdc1cb6d3b Merge branch 'dev' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev 2025-02-05 19:42:59 +08:00
dap
b8ed931eb6 chore: 1 2025-02-05 19:41:33 +08:00
dap
8edc7c8ea4 chore: 存在即合理 2025-02-05 19:27:47 +08:00
dap
ba3fc0fe10 refactor: 点击遮罩不关闭 2025-02-05 15:06:01 +08:00
dap
a37dccdec0 fix: modal/drawer升级后zIndex(2000)会遮挡Tinymce的下拉框zIndex(1300) 2025-02-05 15:03:28 +08:00
dap
1df9236a53 fix: 客户端管理 错误的status disabled 2025-02-05 13:20:30 +08:00
dap
049ccca3e0 chore: 锁定cspell版本 2025-02-04 22:41:33 +08:00
dap
00c4501d13 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-02-04 18:40:47 +08:00
Netfan
061fcf926d chore: update deps 2025-02-04 17:14:14 +08:00
Netfan
7e7a5f3fd4 chore: remove testing code 2025-02-04 15:07:10 +08:00
Netfan
f8bb396dc4 fix: ant tag icon default style (#5473) 2025-02-04 10:19:39 +08:00
jsxz
a832edce0d docs: update request and access docs (#5468)
* fix: Update server.md

* docs: update request and access docs
2025-02-03 16:29:34 +08:00
dap
af3fdeb1da fix: remove getPopupContainer 2025-01-26 22:58:46 +08:00
Netfan
67d1f299b3 fix: renderComponentContent lose slot props data (#5466)
* 修复FormItem传递插槽时丢失插槽props的问题
2025-01-26 22:33:16 +08:00
Netfan
cb7c0ecaa2 fix: menu data for backend mode fixed (#5465) 2025-01-26 20:37:37 +08:00
dap
64d3a21153 refactor: update url 2025-01-26 17:35:09 +08:00
dap
a8019ed88a refactor: 移除后缀图标插槽 2025-01-26 13:40:28 +08:00
dap
4959574f21 feat: getPopupContainer 2025-01-26 13:37:25 +08:00
dap
e89d1a0520 perf: 优化timeline丢失的样式 2025-01-24 20:42:56 +08:00
518 changed files with 15278 additions and 4129 deletions

18
.github/CODEOWNERS vendored
View File

@@ -1,14 +1,14 @@
# default onwer
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
# vben core onwer
/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
# vben team onwer
apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5
apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com

View File

@@ -2,5 +2,5 @@ ports:
- port: 5555
onOpen: open-preview
tasks:
- init: corepack enable && pnpm install
- init: npm i -g corepack && pnpm install
command: pnpm run dev:play

View File

@@ -1,6 +0,0 @@
echo Start running commit-msg hook...
# Check whether the git commit information is standardized
pnpm exec commitlint --edit "$1"
echo Run commit-msg hook done.

View File

@@ -1,3 +0,0 @@
# 每次 git pull 之后, 安装依赖
pnpm install

View File

@@ -1,7 +0,0 @@
# update `.vscode/vben-admin.code-workspace` file
pnpm vsh code-workspace --auto-commit
# Format and submit code according to lintstagedrc.js configuration
pnpm exec lint-staged
echo Run pre-commit hook done.

View File

@@ -1,20 +0,0 @@
export default {
'*.md': ['prettier --cache --ignore-unknown --write'],
'*.vue': [
'prettier --write',
'eslint --cache --fix',
'stylelint --fix --allow-empty-input',
],
'*.{js,jsx,ts,tsx}': [
'prettier --cache --ignore-unknown --write',
'eslint --cache --fix',
],
'*.{scss,less,styl,html,vue,css}': [
'prettier --cache --ignore-unknown --write',
'stylelint --fix --allow-empty-input',
],
'package.json': ['prettier --cache --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write--parser json',
],
};

View File

@@ -1 +1 @@
20.14.0
22.1.0

2
.npmrc
View File

@@ -1,5 +1,5 @@
registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=husky
public-hoist-pattern[]=lefthook
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss

21
.vscode/settings.json vendored
View File

@@ -14,7 +14,7 @@
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.cursorBlinking": "expand",
"editor.largeFileOptimizations": false,
"editor.largeFileOptimizations": true,
"editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on",
"editor.guides.bracketPairs": "active",
@@ -91,6 +91,7 @@
"**/bower_components": true,
"**/.turbo": true,
"**/.idea": true,
"**/.vitepress": true,
"**/tmp": true,
"**/.git": true,
"**/.svn": true,
@@ -113,6 +114,8 @@
"**/yarn.lock": true
},
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
@@ -217,17 +220,27 @@
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
"tailwind.config.mjs": "postcss.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"vitest.disableWorkspaceWarning": true,
"cSpell.words": ["tinymce", "vditor"],
"typescript.tsdk": "node_modules/typescript/lib",
"editor.linkedEditing": true, // 自动同步更改html标签,
"vscodeCustomCodeColor.highlightValue": "v-access", // v-access显示的颜色
"vscodeCustomCodeColor.highlightValueColor": "#CCFFFF",
"oxc.enable": false
"oxc.enable": false,
"cSpell.words": [
"archiver",
"axios",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"vitest"
]
}

View File

@@ -1,3 +1,155 @@
# 1.3.6
**BUG FIX**
- oss配置switch切换 导致报错`存储类型找不到`
- 文件上传无法正确清除(innerList)
# 1.3.5
**BUG FIX**
- 某些带Vxe表格弹窗 关闭后没有正常清理表格数据的问题
# 1.3.4
**BUG FIX**
- 文件上传多次触发导致数据不一致 https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC3BK6
**PREFORMANCE**
- 浏览器返回按钮/手势操作时 弹窗不会被关闭(keepAlive导致)
# 1.3.3
**BUG FIX**
- 工作流list展示在开启缩放会有误差导致触底逻辑不会触发
**OTHER**
- 代码生成预览对模板的提示...(下载都懒得点一下吗)
# 1.3.2
**REFACTOR**
- 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况)
- 菜单图标更新了一部分 sql同步更新
**OTHER**
- 暂时锁死vite依赖 i18n会报错
# 1.3.1
**REFACTOR**
- 所有Modal/Drawer表单关闭前会进行表单数据对比来弹出提示框
- 字典项颜色选择从`原生input type=color`改为`vue3-colorpicker`组件
- 全局Header: ClientID 更改大小写 [spring的问题导致](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS)
**BUG FIX**
- getVxePopupContainer逻辑调整 解决表格固定高度展开不全的问题
**FEATURES**
- 字典渲染支持loading(length为0情况)
**OTHERS**
- useForm的组件改为异步导入(官方更新) bootstrap.js体积从2M降到600K 首屏加载速度提升
# 1.3.0
注意: 如果你使用老版本的`文件上传`/`图片上传` 可暂时使用
- `component: 'ImageUploadOld'`
- `component: 'FileUploadOld'`
代替 **建议替换为新版本**
大致变动:
- `accept string[] -> string`
- `resultField 已经移除 统一使用ossId`
- `maxNumber -> maxCount`
具体参数查看: `apps/web-antd/src/components/upload/src/props.d.ts`
不再推荐使用useDescription, 这个版本会标记为@deprecated, 下个次版本将会移除
框架所有使用useDescription组件的会替换为原生(TODO)
**REFACTOR**
- **文件上传/图片上传重构(破坏性更新 不兼容之前的api)**
- **文件上传/图片上传不再支持url用法 强制使用ossId**
- TableSwitch组件重构
- 管理员租户切换不再返回首页 直接刷新当前页(除特殊页面外会回到首页)
- 租户切换Select增加loading
- ~~modalLoading/drawerLoading改为调用内部的lock/unlock方法~~ 有待商榷暂时按老版本逻辑不变
- 登录验证码 增加loading
- DictEnum使用const代替enum
- TinyMCE组件重构 移除冗余代码/功能 增加loading
**ALPHA功能**
- 弹窗表单数据更改关闭时的提示框(可能最终不会加入) 测试页面: 参数管理
**BUG FIX**
- 重新登录 字典会unknown的情况[详细分析](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IBY27D)
- 测试菜单 请假申请 选中删除 需要根据状态判断
- 修复文件/图片在Safari中无法上传 file-type库与Safari不兼容导致
- 头像裁剪 图片加载失败一直处于loading无法上传
- 头像裁剪 私有桶会拼接timestamp参数导致sign计算异常无法上传 感谢cropperjs作者 https://github.com/fengyuanchen/cropperjs/issues/1230
- 租户选择下拉框会跟随body滚动(将下拉框样式的默认absolute改为fixed)
**OTHER**
- 字典管理 字典类型 表格选中行增加bold效果
- 全局圆角修改 与antd保持一致
- vditor(Markdown)升级3.10.9
- 老版本的文件/图片上传将于下个版本移除
- useDescription将于下个版本移除
- getVxePopupContainer与新版Vxe不兼容 先返回body(会导致滚动不跟随)后续版本再优化
# 1.2.3
**BUG FIX**
- `withDefaultPlaceholder`中将`placeholder`修改为computed, 解决后续使用`updateSchema`无法正常更新显示placeholder(响应式问题)
- 流程定义 修改accept类型 解决无法拖拽上传
**FEATURES**
- 增加`环境变量`打包配置demo -> build:antd:test
- 角色管理 勾选权限组件添加对错误用法的校验提示
**REFACTOR**
- OAuth内部逻辑重构 增加新的默认OAuth登录方式
- 重构部分setup组件为setup语法糖形式
# 1.2.2
**FEATURES**
- 代码生成支持路径方式生成
- 代码生成 支持选择表单生成类型(需要模板支持)
- 工作流 支持按钮权限
# 1.2.1
# BUG FIXES
- 客户端管理 错误的status disabled
- modal/drawer升级后zIndex(2000)会遮挡Tinymce的下拉框zIndex(1300)
# 1.2.0
**REFACTOR**

View File

@@ -1,8 +1,13 @@
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"> </a> <br> <br>
<div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
@@ -15,27 +20,27 @@ Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術
## アップグレード通知
これは最新バージョン5.0であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
これは最新バージョン `5.0` であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
## 特徴
- **最新技術スタック**: Vue 3やViteなどの最先端フロントエンド技術で開発
- **TypeScript**: アプリケーション規模のJavaScriptのための言語
- **テーマ**: 複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
- **国際化**: 完全な内蔵国際化サポート
- **権限管理**: 動的ルートベースの権限生成ソリューションを内蔵
- **最新技術スタック**Vue 3やViteなどの最先端フロントエンド技術で開発
- **TypeScript**アプリケーション規模のJavaScriptのための言語
- **テーマ**複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
- **国際化**完全な内蔵国際化サポート
- **権限管理**動的ルートベースの権限生成ソリューションを内蔵
## プレビュー
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
テストアカウント: vben/123456
テストアカウントvben/123456
<p align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</p>
<div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</div>
### Gitpodを使用
@@ -49,30 +54,27 @@ GitpodGitHub用の無料オンライン開発環境でプロジェクト
## インストールと使用
- プロジェクトコードを取得
1. プロジェクトコードを取得
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
- 依存関係のインストール
2. 依存関係のインストール
```bash
cd vue-vben-admin
corepack enable
npm i -g corepack
pnpm install
```
- 実行
3. 実行
```bash
pnpm dev
```
- ビルド
4. ビルド
```bash
pnpm build
@@ -86,40 +88,39 @@ pnpm build
ご参加をお待ちしております![Issueを提出](https://github.com/anncwb/vue-vben-admin/issues/new/choose)するか、Pull Requestを送信してください。
**Pull Request:**
**Pull Request プロセス:**
1. コードをフォーク
2. 自分のブランチを作成: `git checkout -b feat/xxxx`
3. 変更をコミット: `git commit -am 'feat(function): add xxxxx'`
4. ブランチをプッシュ: `git push origin feat/xxxx`
1. コードをフォーク
2. 自分のブランチを作成`git checkout -b feat/xxxx`
3. 変更をコミット`git commit -am 'feat(function): add xxxxx'`
4. ブランチをプッシュ`git push origin feat/xxxx`
5. `pull request`を送信
## 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))
参考 [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))
- `feat` 新機能の追加
- `fix` 問題/バグの修正
- `style` コードスタイルに関連し、実行結果に影響しない
- `perf` 最適化/パフォーマンス向上
- `refactor` リファクタリング
- `revert` 変更の取り消し
- `test` テスト関連
- `docs` ドキュメント/注釈
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
- `ci` 継続的インテグレーション
- `types` 型定義ファイルの変更
- `wip` 開発中
- `feat` 新機能の追加
- `fix` 問題/バグの修正
- `style` コードスタイルに関連し、実行結果に影響しない
- `perf` 最適化/パフォーマンス向上
- `refactor` リファクタリング
- `revert` 変更の取り消し
- `test` テスト関連
- `docs` ドキュメント/注釈
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
- `ci` 継続的インテグレーション
- `types` 型定義ファイルの変更
## ブラウザサポート
ローカル開発には`Chrome 80+`ブラウザを推奨します
ローカル開発には `Chrome 80+` ブラウザを推奨します
モダンブラウザをサポートし、IEはサポートしません
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: |
| サポートしない | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: |
| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
## メンテナー
@@ -140,8 +141,7 @@ pnpm build
## 貢献者
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors"
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a>
## Discord

View File

@@ -1,8 +1,13 @@
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"> </a> <br> <br>
<div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
@@ -17,7 +22,7 @@ Vue Vben Admin is a free and open source middle and back-end template. Using the
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
## Feature
## Features
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
- **TypeScript**: A language for application-scale JavaScript
@@ -31,11 +36,11 @@ This is the latest version, 5.0, and it is not compatible with previous versions
Test Account: vben/123456
<p align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</p>
<div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</div>
### Use Gitpod
@@ -47,31 +52,29 @@ Open the project in Gitpod (free online dev environment for GitHub) and start co
[Document](https://doc.vben.pro/)
## Install and use
## Install and Use
- Get the project code
1. Get the project code
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
- Installation dependencies
2. Install dependencies
```bash
cd vue-vben-admin
corepack enable
npm i -g corepack
pnpm install
```
- run
3. Run
```bash
pnpm dev
```
- build
4. Build
```bash
pnpm build
@@ -81,44 +84,43 @@ pnpm build
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## How to contribute
## How to Contribute
You are very welcome to join[Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) Or submit a Pull Request
You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
**Pull Request:**
**Pull Request Process:**
1. Fork code!
2. Create your own branch: `git checkout -b feat/xxxx`
1. Fork the code
2. Create your branch: `git checkout -b feat/xxxx`
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
4. Push your branch: `git push origin feat/xxxx`
5. submit`pull request`
5. Submit `pull request`
## Git Contribution submission specification
## Git Contribution Submission Specification
- reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` Add new features
- `fix` Fix the problem/BUG
- `style` The code style is related and does not affect the running result
- `perf` Optimization/performance improvement
- `refactor` Refactor
- `revert` Undo edit
- `test` Test related
- `docs` Documentation/notes
- `chore` Dependency update/scaffolding configuration modification etc.
- `ci` Continuous integration
- `types` Type definition file changes
- `wip` In development
- `feat` Add new features
- `fix` Fix the problem/BUG
- `style` The code style is related and does not affect the running result
- `perf` Optimization/performance improvement
- `refactor` Refactor
- `revert` Undo edit
- `test` Test related
- `docs` Documentation/notes
- `chore` Dependency update/scaffolding configuration modification etc.
- `ci` Continuous integration
- `types` Type definition file changes
## Browser support
## Browser Support
The `Chrome 80+` browser is recommended for local development
Support modern browsers, not IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Maintainer
@@ -136,11 +138,10 @@ If you think this project is helpful to you, you can help the author buy a cup o
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## Contributor
## Contributors
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors"
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a>
## Discord

View File

@@ -6,7 +6,7 @@
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
目前对应后端版本: **分布式5.3.0/微服务2.2.3**
目前对应后端版本: **分布式5.3.1/微服务2.3.0**
V1.1.0版本已支持离线图标
@@ -18,7 +18,7 @@ V1.2.0版本对接warmflow工作流
| 组件/框架 | 版本 |
| :------------- | :----- |
| vben | 5.5.3 |
| vben | 5.5.4 |
| ant-design-vue | 4.2.6 |
| vue | 3.5.13 |
@@ -46,6 +46,14 @@ admin 账号: admin admin123
[RuoYi-Plus 文档地址](https://plus-doc.dromara.org/#/)
## 关于表单
如果你觉得`useVbenForm`难度很大, 完全可以**使用原生antd表单**进行开发, 不一定非得用`useVbenForm`进行开发
`apps/web-antd/src/views/system/notice/notice-modal.vue``通知公告modal`使用**原生antd form**进行(反向🤔)重构, 不想用`useVbenForm`可参考该页面进行表单开发
复杂表单(如各种联动, 需要自定义样式布局, 需要自定义组件)**优先使用原生表单**(反正说了也没人听听😅)
## 预览图
![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/1.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/2.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/3.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/4.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/5.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/6.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/7.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/8.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/9.png)
@@ -68,7 +76,7 @@ admin 账号: admin admin123
git clone https://gitee.com/dapppp/ruoyi-plus-vben5.git
```
- 安装依赖
2. 安装依赖
```bash
cd ruoyi-plus-vben5
@@ -142,7 +150,7 @@ VITE_GLOB_WEBSOCKET_ENABLE=false
pnpm dev:antd
```
- 打包
4. 打包
```bash
pnpm build:antd
@@ -156,21 +164,21 @@ pnpm build:antd
## 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))
参考 [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))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
## 浏览器支持
@@ -178,7 +186,7 @@ pnpm build:antd
本地开发推荐使用`Chrome` 最新版本浏览器
支持现代浏览器, 不支持 IE
支持现代浏览器不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: |

View File

@@ -0,0 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(1000);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(2000);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,61 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
pid: 0,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
),
remark: faker.lorem.sentence(),
};
if (faker.datatype.boolean()) {
dataItem.children = Array.from(
{ length: faker.number.int({ min: 1, max: 5 }) },
() => ({
id: faker.string.uuid(),
pid: dataItem.id,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
),
remark: faker.lorem.sentence(),
}),
);
}
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(10);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const listData = structuredClone(mockData);
return useResponseSuccess(listData);
});

View File

@@ -0,0 +1,12 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(MOCK_MENU_LIST);
});

View File

@@ -0,0 +1,28 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
const namesMap: Record<string, any> = {};
function getNames(menus: any[]) {
menus.forEach((menu) => {
namesMap[menu.name] = String(menu.id);
if (menu.children) {
getNames(menu.children);
}
});
}
getNames(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, name } = getQuery(event);
return (name as string) in namesMap &&
(!id || namesMap[name as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});

View File

@@ -0,0 +1,28 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };
function getPaths(menus: any[]) {
menus.forEach((menu) => {
pathMap[menu.path] = String(menu.id);
if (menu.children) {
getPaths(menu.children);
}
});
}
getPaths(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, path } = getQuery(event);
return (path as string) in pathMap &&
(!id || pathMap[path as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});

View File

@@ -0,0 +1,83 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const menuIds = getMenuIds(MOCK_MENU_LIST);
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
permissions: faker.helpers.arrayElements(menuIds),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
pageSize = 20,
name,
id,
remark,
startTime,
endTime,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});

View File

@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];

View File

@@ -0,0 +1,13 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess({
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
});
// return useResponseError("test")
});

View File

@@ -1,7 +1,19 @@
export default defineEventHandler((event) => {
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {
event.node.res.setHeader(
'Access-Control-Allow-Origin',
event.headers.get('Origin') ?? '*',
);
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';
return 'OK';
} else if (
['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
event.path.startsWith('/api/system/')
) {
await sleep(Math.floor(Math.random() * 2000));
return forbiddenResponse(event, '演示环境,禁止修改');
}
});

View File

@@ -9,7 +9,8 @@ export default defineNitroConfig({
cors: true,
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Headers':
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',

View File

@@ -7,6 +7,7 @@ export default defineEventHandler(() => {
<li><a href="/api/menu">/api/menu/all</a></li>
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
<li><a href="/api/auth/login">/api/auth/login</a></li>
<li><a href="/api/upload">/api/upload</a></li>
</ul>
`;
});

View File

@@ -14,7 +14,7 @@ export function setRefreshTokenCookie(
) {
setCookie(event, 'jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
maxAge: 24 * 60 * 60, // unit: seconds
sameSite: 'none',
secure: true,
});

View File

@@ -58,7 +58,7 @@ const dashboardMenus = [
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/',
path: '/dashboard',
redirect: '/analytics',
children: [
{
@@ -185,3 +185,206 @@ export const MOCK_MENUS = [
username: 'jack',
},
];
export const MOCK_MENU_LIST = [
{
id: 1,
name: 'Workspace',
status: 1,
type: 'menu',
icon: 'mdi:dashboard',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
icon: 'carbon:workspace',
title: 'page.dashboard.workspace',
affixTab: true,
order: 0,
},
},
{
id: 2,
meta: {
icon: 'carbon:settings',
order: 9997,
title: 'system.title',
badge: 'new',
badgeType: 'normal',
badgeVariants: 'primary',
},
status: 1,
type: 'catalog',
name: 'System',
path: '/system',
children: [
{
id: 201,
pid: 2,
path: '/system/menu',
name: 'SystemMenu',
authCode: 'System:Menu:List',
status: 1,
type: 'menu',
meta: {
icon: 'carbon:menu',
title: 'system.menu.title',
},
component: '/system/menu/list',
children: [
{
id: 20_101,
pid: 201,
name: 'SystemMenuCreate',
status: 1,
type: 'button',
authCode: 'System:Menu:Create',
meta: { title: 'common.create' },
},
{
id: 20_102,
pid: 201,
name: 'SystemMenuEdit',
status: 1,
type: 'button',
authCode: 'System:Menu:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_103,
pid: 201,
name: 'SystemMenuDelete',
status: 1,
type: 'button',
authCode: 'System:Menu:Delete',
meta: { title: 'common.delete' },
},
],
},
{
id: 202,
pid: 2,
path: '/system/dept',
name: 'SystemDept',
status: 1,
type: 'menu',
authCode: 'System:Dept:List',
meta: {
icon: 'carbon:container-services',
title: 'system.dept.title',
},
component: '/system/dept/list',
children: [
{
id: 20_401,
pid: 201,
name: 'SystemDeptCreate',
status: 1,
type: 'button',
authCode: 'System:Dept:Create',
meta: { title: 'common.create' },
},
{
id: 20_402,
pid: 201,
name: 'SystemDeptEdit',
status: 1,
type: 'button',
authCode: 'System:Dept:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_403,
pid: 201,
name: 'SystemDeptDelete',
status: 1,
type: 'button',
authCode: 'System:Dept:Delete',
meta: { title: 'common.delete' },
},
],
},
],
},
{
id: 9,
meta: {
badgeType: 'dot',
order: 9998,
title: 'demos.vben.title',
icon: 'carbon:data-center',
},
name: 'Project',
path: '/vben-admin',
type: 'catalog',
status: 1,
children: [
{
id: 901,
pid: 9,
name: 'VbenDocument',
path: '/vben-admin/document',
component: 'IFrameView',
type: 'embedded',
status: 1,
meta: {
icon: 'carbon:book',
iframeSrc: 'https://doc.vben.pro',
title: 'demos.vben.document',
},
},
{
id: 902,
pid: 9,
name: 'VbenGithub',
path: '/vben-admin/github',
component: 'IFrameView',
type: 'link',
status: 1,
meta: {
icon: 'carbon:logo-github',
link: 'https://github.com/vbenjs/vue-vben-admin',
title: 'Github',
},
},
{
id: 903,
pid: 9,
name: 'VbenAntdv',
path: '/vben-admin/antdv',
component: 'IFrameView',
type: 'link',
status: 0,
meta: {
icon: 'carbon:hexagon-vertical-solid',
badgeType: 'dot',
link: 'https://ant.vben.pro',
title: 'demos.vben.antdv',
},
},
],
},
{
id: 10,
component: '_core/about/index',
type: 'menu',
status: 1,
meta: {
icon: 'lucide:copyright',
order: 9999,
title: 'demos.vben.about',
},
name: 'About',
path: '/about',
},
];
export function getMenuIds(menus: any[]) {
const ids: number[] = [];
menus.forEach((item) => {
ids.push(item.id);
if (item.children && item.children.length > 0) {
ids.push(...getMenuIds(item.children));
}
});
return ids;
}

View File

@@ -3,3 +3,6 @@ VITE_APP_TITLE=Plus Admin
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-antd
# 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key

View File

@@ -15,7 +15,7 @@ VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
# 后台请求路径 具体在vite.config.mts配置代理
# 后端接口地址
VITE_GLOB_API_URL=/prod-api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)

35
apps/web-antd/.env.test Normal file
View File

@@ -0,0 +1,35 @@
# 该文件是为了给一个增加环境变量打包的例子
# 对应在根目录package.json -> build:antd:test 命令
VITE_BASE=/
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=gzip
# 是否开启 PWA
VITE_PWA=false
# vue-router 的模式
VITE_ROUTER_HISTORY=history
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
# 后端接口地址
VITE_GLOB_API_URL=/test-api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=true
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启SSE
VITE_GLOB_SSE_ENABLE=true

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "1.2.0",
"version": "1.3.6",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -16,7 +16,7 @@
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build": "pnpm vite build",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
@@ -54,7 +54,8 @@
"tinymce": "^7.3.0",
"unplugin-vue-components": "^0.27.3",
"vue": "catalog:",
"vue-router": "catalog:"
"vue-router": "catalog:",
"vue3-colorpicker": "^2.3.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",

View File

@@ -3,51 +3,124 @@
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component, SetupContext } from 'vue';
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { h } from 'vue';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
import { notification } from 'ant-design-vue';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
import { FileUploadOld, ImageUploadOld } from '#/components/upload-old';
const RichTextarea = defineAsyncComponent(() =>
import('#/components/tinymce/index').then((res) => res.Tinymce),
);
const FileUpload = defineAsyncComponent(() =>
import('#/components/upload').then((res) => res.FileUpload),
);
const ImageUpload = defineAsyncComponent(() =>
import('#/components/upload').then((res) => res.ImageUpload),
);
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
return () =>
h(
component,
{
...componentProps,
placeholder,
...props,
...attrs,
ref: innerRef,
},
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
@@ -61,8 +134,10 @@ export type ComponentType =
| 'DefaultButton'
| 'Divider'
| 'FileUpload'
| 'FileUploadOld'
| 'IconPicker'
| 'ImageUpload'
| 'ImageUploadOld'
| 'Input'
| 'InputNumber'
| 'InputPassword'
@@ -87,38 +162,34 @@ async function initComponentAdapter() {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
slots,
);
},
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete,
Checkbox,
CheckboxGroup,
@@ -128,13 +199,11 @@ async function initComponentAdapter() {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: (props, { attrs, slots }) => {
return h(
IconPicker,
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
slots,
);
},
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
}),
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
@@ -157,6 +226,8 @@ async function initComponentAdapter() {
ImageUpload,
FileUpload,
RichTextarea,
ImageUploadOld,
FileUploadOld,
};
// 将组件注册到全局共享状态中

View File

@@ -7,22 +7,6 @@ import { requestClient } from '#/api/request';
*/
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
/**
* 通过单文件上传接口
* @param file 上传的文件
* @param onUploadProgress 上传进度事件 非必传
* @returns 上传结果
*/
export function uploadApi(
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) {
return requestClient.upload(
'/resource/oss/upload',
{ file },
{ onUploadProgress, timeout: 60_000 },
);
}
/**
* 默认上传结果
*/
@@ -31,3 +15,33 @@ export interface UploadResult {
fileName: string;
ossId: string;
}
/**
* 通过单文件上传接口
* @param file 上传的文件
* @param options 一些配置项
* @param options.onUploadProgress 上传进度事件
* @param options.signal 上传取消信号
* @param options.otherData 其他请求参数 后端拓展可能会用到
* @returns 上传结果
*/
export function uploadApi(
file: Blob | File,
options?: {
onUploadProgress?: AxiosProgressEvent;
otherData?: Record<string, any>;
signal?: AbortSignal;
},
) {
const { onUploadProgress, signal, otherData = {} } = options ?? {};
return requestClient.upload<UploadResult>(
'/resource/oss/upload',
{ file, ...otherData },
{ onUploadProgress, signal, timeout: 60_000 },
);
}
/**
* 上传api type
*/
export type UploadApi = typeof uploadApi;

View File

@@ -3,14 +3,14 @@ import { requestClient } from './request';
/**
* @description: contentType
*/
export enum ContentTypeEnum {
export const ContentTypeEnum = {
// form-data upload
FORM_DATA = 'multipart/form-data;charset=UTF-8',
FORM_DATA: 'multipart/form-data;charset=UTF-8',
// form-data qs
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8',
// json
JSON = 'application/json;charset=UTF-8',
}
JSON: 'application/json;charset=UTF-8',
} as const;
/**
* 通用下载接口 封装一层

View File

@@ -9,4 +9,5 @@ export interface LoginLog {
os: string;
msg: string;
loginTime: string;
clientKey: string;
}

View File

@@ -2,7 +2,7 @@ export interface OperationLog {
operId: string;
tenantId: string;
title: string;
businessType: number;
businessType: string;
businessTypes?: any;
method: string;
requestMethod: string;
@@ -14,7 +14,7 @@ export interface OperationLog {
operLocation: string;
operParam: string;
jsonResult: string;
status: number;
status: string;
errorMsg: string;
operTime: string;
costTime: number;

View File

@@ -93,9 +93,12 @@ function createRequestClient(baseURL: string) {
const language = preferences.app.locale.replace('-', '_');
config.headers['Accept-Language'] = language;
config.headers['Content-Language'] = language;
// 添加全局clientId
config.headers.clientId = clientId;
/**
* 添加全局clientId
* 关于header的clientId被错误绑定到实体类
* https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS
*/
config.headers.ClientID = clientId;
/**
* 格式化get/delete参数
* 如果包含自定义的paramsSerializer则不走此逻辑
@@ -225,7 +228,7 @@ function createRequestClient(baseURL: string) {
case 401: {
// 已经在登出过程中 不再执行
if (isLogoutProcessing) {
return;
throw new Error(timeoutMsg);
}
isLogoutProcessing = true;
const _msg = $t('http.loginTimeout');
@@ -235,7 +238,7 @@ function createRequestClient(baseURL: string) {
isLogoutProcessing = false;
});
// 不再执行下面逻辑
return;
throw new Error(_msg);
}
default: {
if (msg) {

View File

@@ -59,7 +59,11 @@ export function clientUpdate(data: Partial<Client>) {
* @param data 状态
*/
export function clientChangeStatus(data: any) {
return requestClient.putWithMsg<void>(Api.clientChangeStatus, data);
const requestData = {
clientId: data.clientId,
status: data.status,
};
return requestClient.putWithMsg<void>(Api.clientChangeStatus, requestData);
}
/**

View File

@@ -37,5 +37,10 @@ export function ossConfigRemove(ossConfigIds: IDS) {
// 更改OSS配置的状态
export function ossConfigChangeStatus(data: any) {
return requestClient.putWithMsg(Api.ossConfigChangeStatus, data);
const requestData = {
ossConfigId: data.ossConfigId,
status: data.status,
configKey: data.configKey,
};
return requestClient.putWithMsg(Api.ossConfigChangeStatus, requestData);
}

View File

@@ -29,7 +29,7 @@ export function ossList(params?: PageQuery) {
* @param ossIds id数组
* @returns 信息数组
*/
export function ossInfo(ossIds: IDS) {
export function ossInfo(ossIds: ID | IDS) {
return requestClient.get<OssFile[]>(`${Api.ossInfo}/${ossIds}`);
}

View File

@@ -72,7 +72,11 @@ export function roleUpdate(data: Partial<Role>) {
* @returns void
*/
export function roleChangeStatus(data: Partial<Role>) {
return requestClient.putWithMsg<void>(Api.roleChangeStatus, data);
const requestData = {
roleId: data.roleId,
status: data.status,
};
return requestClient.putWithMsg<void>(Api.roleChangeStatus, requestData);
}
/**

View File

@@ -74,7 +74,11 @@ export function packageUpdate(data: Partial<TenantPackage>) {
* @returns void
*/
export function packageChangeStatus(data: Partial<TenantPackage>) {
return requestClient.putWithMsg<void>(Api.packageChangeStatus, data);
const packageId = {
packageId: data.packageId,
status: data.status,
};
return requestClient.putWithMsg<void>(Api.packageChangeStatus, packageId);
}
/**

View File

@@ -67,7 +67,12 @@ export function tenantUpdate(data: Partial<Tenant>) {
* @returns void
*/
export function tenantStatusChange(data: Partial<Tenant>) {
return requestClient.putWithMsg(Api.tenantStatus, data);
const requestData = {
id: data.id,
tenantId: data.tenantId,
status: data.status,
};
return requestClient.putWithMsg(Api.tenantStatus, requestData);
}
/**

View File

@@ -110,7 +110,11 @@ export function userUpdate(data: Partial<User>) {
* @returns void
*/
export function userStatusChange(data: Partial<User>) {
return requestClient.putWithMsg<void>(Api.userStatusChange, data);
const requestData = {
userId: data.userId,
status: data.status,
};
return requestClient.putWithMsg<void>(Api.userStatusChange, requestData);
}
/**

View File

@@ -66,6 +66,7 @@ export interface User {
roleIds?: string[];
postIds?: number[];
roleId: string;
deptName: string;
}
export interface Post {

View File

@@ -77,8 +77,8 @@ export function genDownload(tableId: ID) {
}
// 生成代码(自定义路径)
export function genDownloadWithPath(tableId: ID) {
return requestClient.get(`${Api.download}/${tableId}`);
export function genWithPath(tableId: ID) {
return requestClient.get<void>(`${Api.genCode}/${tableId}`);
}
// 同步数据库

View File

@@ -177,6 +177,7 @@ export interface Info {
// 树表需要添加此属性
params?: any;
popupComponent?: string;
formComponent?: string;
}
export interface GenInfo {

View File

@@ -1,5 +1,6 @@
import type {
CompleteTaskReqData,
NextNodeInfo,
StartWorkFlowReqData,
TaskInfo,
TaskOperationData,
@@ -156,3 +157,16 @@ export function getBackTaskNode(definitionId: string, nodeCode: string) {
export function currentTaskAllUser(taskId: ID) {
return requestClient.get<any>(`/workflow/task/currentTaskAllUser/${taskId}`);
}
/**
* 获取下一节点
* @param data data
* @param data.taskId taskId
* @returns NextNodeInfo
*/
export function getNextNodeList(data: { taskId: string }) {
return requestClient.post<NextNodeInfo[]>(
'/workflow/task/getNextNodeList',
data,
);
}

View File

@@ -1,3 +1,9 @@
export interface ButtonWithPermission {
code: string;
value: null | string;
show: boolean;
}
export interface TaskInfo {
id: string;
categoryName: string;
@@ -28,6 +34,7 @@ export interface TaskInfo {
createBy: string;
createByName: string;
targetNodeName?: string;
buttonList: ButtonWithPermission[];
}
export interface CompleteTaskReqData {
@@ -38,6 +45,8 @@ export interface CompleteTaskReqData {
variables: any;
// 附件ID 1,2,3,4形式
fileId?: string;
// 选人 key为节点code value为用户ID join(,)
assigneeMap: { [key: string]: string };
}
export interface StartWorkFlowReqData {
@@ -72,3 +81,28 @@ export type TaskOperationType =
| 'delegateTask'
| 'reductionSignature'
| 'transferTask';
export interface NextNodeInfo {
skipList: string[];
id: string;
createTime: string;
updateTime: string;
tenantId: string;
delFlag: string;
nodeType: number;
definitionId: string;
nodeCode: string;
nodeName: string;
permissionFlag: string;
nodeRatio: string;
coordinate: string;
version: string;
anyNodeSkip: any;
listenerType: any;
listenerPath: any;
handlerType: any;
handlerPath: any;
formCustom: string;
formPath: any;
ext: string;
}

View File

@@ -1,7 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy } from '@vben/common-ui';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@@ -33,6 +33,11 @@ async function bootstrap(namespace: string) {
// 全局组件
setupGlobalComponent(app);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
@@ -44,11 +49,16 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {

View File

@@ -84,6 +84,10 @@ function handleReady(cropperInstance: Cropper) {
modalLoading(false);
}
function handleReadyError() {
modalLoading(false);
}
function handlerToolbar(event: string, arg?: number) {
if (event === 'scaleX') {
scaleX = arg = scaleX === -1 ? 1 : -1;
@@ -128,9 +132,11 @@ async function handleOk() {
v-if="src"
:circled="circled"
:src="src"
crossorigin="anonymous"
height="300px"
@cropend="handleCropend"
@ready="handleReady"
@ready-error="handleReadyError"
/>
</div>
@@ -322,7 +328,8 @@ async function handleOk() {
&-cropper {
height: 300px;
background: #eee;
background-image: linear-gradient(
background-image:
linear-gradient(
45deg,
rgb(0 0 0 / 25%) 25%,
transparent 0,

View File

@@ -26,14 +26,16 @@ const props = defineProps({
src: { required: true, type: String },
});
const emit = defineEmits(['cropend', 'ready', 'cropendError']);
const emit = defineEmits(['cropend', 'ready', 'cropendError', 'readyError']);
const defaultOptions: Options = {
aspectRatio: 1,
autoCrop: true,
background: true,
center: true,
checkCrossOrigin: true,
// 需要设置为false 否则会自动拼接timestamp 导致私有桶sign错误
// 需要配合img crossorigin='anonymous'使用(默认已经做了处理)
checkCrossOrigin: false,
checkOrientation: true,
cropBoxMovable: true,
cropBoxResizable: true,
@@ -94,6 +96,15 @@ async function init() {
if (!imgEl) {
return;
}
// 判断是否为正常访问的图片
try {
const resp = await fetch(props.src);
if (resp.status !== 200) {
emit('readyError');
}
} catch {
emit('readyError');
}
cropper.value = new Cropper(imgEl, {
...defaultOptions,
crop() {

View File

@@ -34,6 +34,9 @@ const props = {
useCollapse: { default: true, type: Boolean },
};
/**
* @deprecated 使用antd原生组件替代 下个版本将会移除
*/
export default defineComponent({
emits: ['register'],
// eslint-disable-next-line vue/order-in-components

View File

@@ -6,6 +6,9 @@ import type {
import { getCurrentInstance, ref, unref } from 'vue';
/**
* @deprecated 使用antd原生组件替代 下个版本将会移除
*/
export function useDescription(
props?: Partial<DescriptionProps>,
): UseDescReturnType {

View File

@@ -4,7 +4,7 @@ import type { DictData } from '#/api/system/dict/dict-data-model';
import { computed } from 'vue';
import { Tag } from 'ant-design-vue';
import { Spin, Tag } from 'ant-design-vue';
import { tagTypes } from './data';
@@ -41,12 +41,22 @@ const label = computed<number | string>(() => {
});
const tagComponent = computed(() => (color.value ? Tag : 'div'));
const loading = computed(() => {
return props.dicts?.length === 0;
});
</script>
<template>
<div>
<component :is="tagComponent" :class="cssClass" :color="color">
<component
v-if="!loading"
:is="tagComponent"
:class="cssClass"
:color="color"
>
{{ label }}
</component>
<Spin v-else :spinning="true" size="small" />
</div>
</template>

View File

@@ -1,84 +1,134 @@
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Switch } from 'ant-design-vue';
import { $t } from '@vben/locales';
import { Modal, Switch } from 'ant-design-vue';
import { isFunction } from 'lodash-es';
export default defineComponent({
name: 'TableSwitch',
components: {
Switch,
},
inheritAttrs: false,
props: {
modelValue: {
type: [Boolean, String, Number],
default: false,
},
checkedText: {
type: String,
default: '启用',
},
unCheckedText: {
type: String,
default: '禁用',
},
// 使用严格相等判断 类型要正确
checkedValue: {
type: [Boolean, String, Number],
default: '0',
},
unCheckedValue: {
type: [Boolean, String, Number],
default: '1',
},
api: {
type: Function,
required: false,
default: null,
},
reload: {
type: Function,
required: false,
default: null,
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
type CheckedType = boolean | number | string;
async function onChange(checked: CheckedType, e: Event) {
// 阻止事件冒泡 否则会跟行选中冲突
e.stopPropagation();
const { checkedValue, unCheckedValue } = props;
// 原本的状态
const lastStatus =
checked === checkedValue ? unCheckedValue : checkedValue;
// 切换状态
emit('update:modelValue', checked);
const { api, reload } = props;
type CheckedType = boolean | number | string;
interface Props {
/**
* 选中的文本
* @default i18n 启用
*/
checkedText?: string;
/**
* 未选中的文本
* @default i18n 禁用
*/
unCheckedText?: string;
checkedValue?: CheckedType;
unCheckedValue?: CheckedType;
disabled?: boolean;
/**
* 需要自己在内部处理更新的逻辑 因为status已经双向绑定了 可以直接获取
*/
api: () => PromiseLike<void>;
/**
* 更新前是否弹窗确认
* @default false
*/
confirm?: boolean;
/**
* 对应的提示内容
* @param checked 选中的值(更新后的值)
* @default string '确认要更新状态吗?'
*/
confirmText?: (checked: CheckedType) => string;
}
const props = withDefaults(defineProps<Props>(), {
checkedText: undefined,
unCheckedText: undefined,
checkedValue: '0',
unCheckedValue: '1',
confirm: false,
confirmText: undefined,
});
const emit = defineEmits<{ reload: [] }>();
// 修改为computed 支持语言切换
const checkedTextComputed = computed(() => {
return props.checkedText ?? $t('pages.common.enable');
});
const unCheckedTextComputed = computed(() => {
return props.unCheckedText ?? $t('pages.common.disable');
});
const currentChecked = defineModel<CheckedType>('value', {
default: false,
});
const loading = ref(false);
function confirmUpdate(checked: CheckedType, lastStatus: CheckedType) {
const content = isFunction(props.confirmText)
? props.confirmText(checked)
: `确认要更新状态吗?`;
Modal.confirm({
title: '提示',
content,
centered: true,
onOk: async () => {
try {
loading.value = true;
const { api } = props;
isFunction(api) && (await api());
isFunction(reload) && (await reload());
emit('reload');
} catch {
emit('update:modelValue', lastStatus);
currentChecked.value = lastStatus;
} finally {
loading.value = false;
}
},
onCancel: () => {
currentChecked.value = lastStatus;
},
});
}
async function handleChange(checked: CheckedType, e: Event) {
// 阻止事件冒泡 否则会跟行选中冲突
e.stopPropagation();
const { checkedValue, unCheckedValue } = props;
// 原本的状态
const lastStatus = checked === checkedValue ? unCheckedValue : checkedValue;
// 切换状态
currentChecked.value = checked;
const { api } = props;
try {
loading.value = true;
if (props.confirm) {
confirmUpdate(checked, lastStatus);
return;
}
return {
onChange,
};
},
});
isFunction(api) && (await api());
emit('reload');
} catch {
currentChecked.value = lastStatus;
} finally {
loading.value = false;
}
}
</script>
<template>
<Switch
v-bind="$attrs"
:checked="modelValue"
:checked-children="checkedText"
:loading="loading"
:disabled="disabled"
:checked="currentChecked"
:checked-children="checkedTextComputed"
:checked-value="checkedValue"
:un-checked-children="unCheckedText"
:un-checked-children="unCheckedTextComputed"
:un-checked-value="unCheckedValue"
@change="onChange"
@change="handleChange"
/>
</template>

View File

@@ -1,17 +1,17 @@
<script setup lang="ts">
import type { MessageType } from 'ant-design-vue/es/message';
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select';
import type { TenantOption } from '#/api';
import { computed, onMounted, ref, unref } from 'vue';
import { useRouter } from 'vue-router';
import { computed, onMounted, ref, shallowRef, unref } from 'vue';
import { useRoute } from 'vue-router';
import { useAccess } from '@vben/access';
import { DEFAULT_HOME_PATH } from '@vben/constants';
import { useTabs } from '@vben/hooks';
import { $t } from '@vben/locales';
import { message, Select } from 'ant-design-vue';
import { message, Select, Spin } from 'ant-design-vue';
import { storeToRefs } from 'pinia';
import { tenantDynamicClear, tenantDynamicToggle } from '#/api/system/tenant';
@@ -35,24 +35,39 @@ const showToggle = computed<boolean>(() => {
});
onMounted(async () => {
// 没有超级管理员权限 不会调用接口
if (!hasAccessByRoles(['superadmin'])) {
return;
}
await initTenant();
});
const { closeAllTabs } = useTabs();
const router = useRouter();
function close(checked: boolean) {
const route = useRoute();
const { closeOtherTabs, refreshTab, closeAllTabs } = useTabs();
async function close(checked: boolean) {
// store设置状态
setChecked(checked);
// 需要关闭全部标签页
closeAllTabs();
// 切换完加载首页
router.push(DEFAULT_HOME_PATH);
/**
* 切换租户需要回到首页的页面 一般为带id的页面
* 其他则直接刷新页面
*/
if (route.meta.requireHomeRedirect) {
await closeAllTabs();
} else {
// 先关闭再刷新 这里不用Promise.all()
await closeOtherTabs();
await refreshTab();
}
}
const dictStore = useDictStore();
// 用于清理上一条message
const messageInstance = shallowRef<MessageType | null>();
// loading加载中效果
const loading = ref(false);
/**
* 选中租户的处理
* @param tenantId tenantId
@@ -63,23 +78,46 @@ const onSelected: SelectHandler = async (tenantId: string, option: any) => {
// createMessage.info('选择一致');
return;
}
await tenantDynamicToggle(tenantId);
lastSelected.value = tenantId;
message.success(
`${$t('component.tenantToggle.switch')} ${option.companyName}`,
);
close(true);
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
setTimeout(() => dictStore.resetCache());
try {
loading.value = true;
await tenantDynamicToggle(tenantId);
lastSelected.value = tenantId;
// 关闭之前的message 只保留一条
messageInstance.value?.();
messageInstance.value = message.success(
`${$t('component.tenantToggle.switch')} ${option.companyName}`,
);
close(true);
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
setTimeout(() => dictStore.resetCache());
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
};
async function onDeselect() {
await tenantDynamicClear();
message.success($t('component.tenantToggle.reset'));
lastSelected.value = '';
close(false);
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
setTimeout(() => dictStore.resetCache());
try {
loading.value = true;
await tenantDynamicClear();
// 关闭之前的message 只保留一条
messageInstance.value?.();
messageInstance.value = message.success($t('component.tenantToggle.reset'));
lastSelected.value = '';
close(false);
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
setTimeout(() => dictStore.resetCache());
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
}
/**
@@ -96,18 +134,20 @@ function filterOption(input: string, option: TenantOption) {
<div v-if="showToggle" class="mr-[8px] hidden md:block">
<Select
v-model:value="selected"
:disabled="loading"
:field-names="{ label: 'companyName', value: 'tenantId' }"
:filter-option="filterOption"
:options="tenantList"
:placeholder="$t('component.tenantToggle.placeholder')"
:dropdown-style="{ position: 'fixed', zIndex: 1024 }"
allow-clear
class="w-60"
show-search
@deselect="onDeselect"
@select="onSelected"
>
<template #suffixIcon>
<span class="icon-mdi--company"></span>
<template v-if="loading" #suffixIcon>
<Spin size="small" spinning />
</template>
</Select>
</div>

View File

@@ -1,80 +1,50 @@
<script lang="ts" setup>
<script setup lang="ts">
import type { IPropTypes } from '@tinymce/tinymce-vue/lib/cjs/main/ts/components/EditorPropTypes';
import type { Editor as EditorType } from 'tinymce/tinymce';
import type { PropType } from 'vue';
import type { AxiosProgressEvent, UploadResult } from '#/api';
import type { UploadResult } from '#/api/core/upload';
import {
computed,
nextTick,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted,
ref,
unref,
useAttrs,
watch,
} from 'vue';
import { computed, nextTick, ref, shallowRef, useAttrs, watch } from 'vue';
import { preferences, usePreferences } from '@vben/preferences';
import { buildShortUUID } from '@vben/utils';
import Editor from '@tinymce/tinymce-vue';
import { isNumber } from 'lodash-es';
import { Spin } from 'ant-design-vue';
import { camelCase } from 'lodash-es';
import { uploadApi } from '#/api/core/upload';
import { bindHandlers } from './helper';
import ImgUpload from './img-upload.vue';
import { uploadApi } from '#/api';
import {
plugins as defaultPlugins,
toolbar as defaultToolbar,
} from './tinymce';
defineOptions({ inheritAttrs: false });
const props = defineProps({
height: {
default: 400,
required: false,
type: [Number, String] as PropType<number | string>,
},
options: {
default: () => ({}),
// eslint-disable-next-line no-use-before-define
type: Object as PropType<Partial<InitOptions>>,
},
plugins: {
default: defaultPlugins,
type: String,
},
showImageUpload: {
default: true,
type: Boolean,
},
toolbar: {
default: defaultToolbar,
type: String,
},
width: {
default: 'auto',
required: false,
type: [Number, String] as PropType<number | string>,
},
});
const emit = defineEmits(['change']);
} from '#/components/tinymce/src/tinymce';
type InitOptions = IPropTypes['init'];
/**
* 外部使用 v-model 绑定值
*/
const modelValue = defineModel('modelValue', { default: '', type: String });
interface Props {
height?: number | string;
options?: Partial<InitOptions>;
plugins?: string;
toolbar?: string;
disabled?: boolean;
}
defineOptions({
name: 'Tinymce',
inheritAttrs: false,
});
const props = withDefaults(defineProps<Props>(), {
height: 400,
options: () => ({}),
plugins: defaultPlugins,
toolbar: defaultToolbar,
disabled: false,
});
const emit = defineEmits<{
mounted: [];
}>();
/**
* https://www.jianshu.com/p/59a9c3802443
* 使用自托管方案本地代替cdn 没有key的限制
@@ -82,21 +52,13 @@ const modelValue = defineModel('modelValue', { default: '', type: String });
*/
const tinymceScriptSrc = `${import.meta.env.VITE_BASE}tinymce/tinymce.min.js`;
const attrs = useAttrs();
const editorRef = ref<EditorType>();
const fullscreen = ref(false);
const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
const elRef = ref<HTMLElement | null>(null);
const containerWidth = computed(() => {
const width = props.width;
if (isNumber(width)) {
return `${width}px`;
}
return width;
const content = defineModel<string>('modelValue', {
default: '',
});
const { isDark } = usePreferences();
const editorRef = shallowRef<EditorType | null>(null);
const { isDark, locale } = usePreferences();
const skinName = computed(() => {
return isDark.value ? 'oxide-dark' : 'oxide';
});
@@ -105,30 +67,6 @@ const contentCss = computed(() => {
return isDark.value ? 'dark' : 'default';
});
/**
* 通过v-if来挂载/卸载组件
* 来完成主题切换/语言切换
*/
const init = ref(true);
watch(
() => [preferences.theme.mode, preferences.app.locale],
() => {
if (!editorRef.value) {
return;
}
destroy();
init.value = false;
// 放在下一次tick来切换
// 需要先加载组件 也就是v-if为true 然后需要拿到editorRef 必须放在setTimeout(相当于onMounted)
nextTick(() => {
init.value = true;
setTimeout(() => {
setEditorMode();
});
});
},
);
/**
* tinymce支持 en zh_CN
*/
@@ -140,6 +78,26 @@ const langName = computed(() => {
return 'zh_CN';
});
/**
* 通过v-if来挂载/卸载组件来完成主题切换切换
* 语言切换也需要监听 不监听在切换时候会显示原始<textarea>样式
*/
const init = ref(true);
watch([isDark, locale], async () => {
if (!editorRef.value) {
return;
}
// 相当于手动unmounted清理 非常重要
editorRef.value.destroy();
init.value = false;
// 放在下一次tick来切换
// 需要先加载组件 也就是v-if为true 然后需要拿到editorRef 必须放在setTimeout(相当于onMounted)
await nextTick();
init.value = true;
});
// 加载完毕前显示spin
const loading = ref(true);
const initOptions = computed((): InitOptions => {
const { height, options, plugins, toolbar } = props;
return {
@@ -163,6 +121,7 @@ const initOptions = computed((): InitOptions => {
* images_upload_handler启用时为上传
*/
paste_data_images: true,
images_file_types: 'jpeg,jpg,png,gif,bmp,webp',
plugins,
quickbars_selection_toolbar:
'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
@@ -175,12 +134,18 @@ const initOptions = computed((): InitOptions => {
* @param blobInfo
* 大坑 不要调用这两个函数 success failure:
* 使用resolve/reject代替
* (PS: 新版已经没有success failure)
*/
images_upload_handler: (blobInfo) => {
images_upload_handler: (blobInfo, progress) => {
return new Promise((resolve, reject) => {
const file = blobInfo.blob();
// const filename = blobInfo.filename();
uploadApi(file)
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
progress(percent);
};
uploadApi(file, { onUploadProgress: progressEvent })
.then((response) => {
const { url } = response as unknown as UploadResult;
console.log('tinymce上传图片:', url);
@@ -188,186 +153,70 @@ const initOptions = computed((): InitOptions => {
})
.catch((error) => {
console.error('tinymce上传图片失败:', error);
reject(error.message);
// eslint-disable-next-line prefer-promise-reject-errors
reject({ message: error.message, remove: true });
});
});
},
setup: (editor) => {
editorRef.value = editor;
editor.on('init', (e) => initSetup(e));
editor.on('init', () => {
emit('mounted');
loading.value = false;
});
},
};
});
const attrs = useAttrs();
/**
* 监听options.readonly
* 获取透传的事件 通过v-on绑定
* 可绑定的事件 https://www.tiny.cloud/docs/tinymce/latest/vue-ref/#event-binding
*/
watch(
() => props.options,
(options) => {
const getDisabled = options && Reflect.get(options, 'readonly');
const editor = unref(editorRef);
if (editor) {
editor.mode.set(getDisabled ? 'readonly' : 'design');
const events = computed(() => {
const onEvents: Record<string, any> = {};
for (const key in attrs) {
if (key.startsWith('on')) {
const eventKey = camelCase(key.split('on')[1]!);
onEvents[eventKey] = attrs[key];
}
},
);
onMounted(() => {
if (!initOptions.value.inline) {
tinymceId.value = buildShortUUID('tiny-vue');
}
nextTick(() => {
setTimeout(() => {
initEditor();
setEditorMode();
}, 30);
});
return onEvents;
});
onBeforeUnmount(() => {
destroy();
});
onDeactivated(() => {
destroy();
});
onActivated(() => {
setEditorMode();
});
function setEditorMode() {
const editor = unref(editorRef);
if (editor) {
const mode = props.options.readonly ? 'readonly' : 'design';
editor.mode.set(mode);
}
}
function destroy() {
const editor = unref(editorRef);
editor?.destroy();
}
function initEditor() {
const el = unref(elRef);
if (el) {
el.style.visibility = '';
}
}
function initSetup(e: any) {
const editor = unref(editorRef);
if (!editor) {
return;
}
const value = modelValue.value || '';
editor.setContent(value);
bindModelHandlers(editor);
bindHandlers(e, attrs, unref(editorRef));
}
function setValue(editor: Record<string, any>, val?: string, prevVal?: string) {
if (
editor &&
typeof val === 'string' &&
val !== prevVal &&
val !== editor.getContent({ format: attrs.outputFormat })
) {
editor.setContent(val);
}
}
function bindModelHandlers(editor: any) {
const modelEvents = attrs.modelEvents ?? null;
const normalizedEvents = Array.isArray(modelEvents)
? modelEvents.join(' ')
: modelEvents;
watch(
() => modelValue.value,
(val, prevVal) => {
setValue(editor, val, prevVal);
},
);
editor.on(normalizedEvents || 'change keyup undo redo', () => {
const content = editor.getContent({ format: attrs.outputFormat });
emit('change', content);
});
editor.on('FullscreenStateChanged', (e: any) => {
fullscreen.value = e.state;
});
}
const disabled = computed(() => props.options.readonly ?? false);
function getUploadingImgName(name: string) {
return `[uploading:${name}]`;
}
function handleImageUploading(name: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
const content = editor?.getContent() ?? '';
setValue(editor, content);
}
function handleDone(name: string, url: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
const content = editor?.getContent() ?? '';
const val =
content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
setValue(editor, val);
}
</script>
<template>
<div :style="{ width: containerWidth }" class="app-tinymce">
<ImgUpload
v-if="showImageUpload"
v-show="editorRef"
:disabled="disabled"
:fullscreen="fullscreen"
@done="handleDone"
@uploading="handleImageUploading"
/>
<Editor
v-if="!initOptions.inline && init"
v-model="modelValue"
:init="initOptions"
:style="{ visibility: 'hidden' }"
:tinymce-script-src="tinymceScriptSrc"
license-key="gpl"
/>
<slot v-else></slot>
<div class="app-tinymce">
<Spin :spinning="loading">
<Editor
v-if="init"
v-model="content"
:init="initOptions"
:tinymce-script-src="tinymceScriptSrc"
:disabled="disabled"
license-key="gpl"
v-on="events"
/>
</Spin>
</div>
</template>
<style lang="scss" scoped>
/**
隐藏右上角upgrade按钮
*/
:deep(.tox-promotion) {
display: none !important;
<style lang="scss">
/***
由于modal/drawer的zIndex升级后为2000
这里会造成遮挡 修改为更高的zIndex
*/
.tox.tox-silver-sink.tox-tinymce-aux {
/** 该样式默认为1300的zIndex */
z-index: 2025;
}
.app-tinymce {
position: relative;
line-height: normal;
:deep(.textarea) {
z-index: -1;
visibility: hidden;
/**
隐藏右上角upgrade按钮
*/
.tox-promotion {
display: none;
}
}
</style>

View File

@@ -1,85 +0,0 @@
const validEvents = new Set([
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforePaste',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResized',
'onObjectResizeStart',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid',
]);
const isValidKey = (key: string) => validEvents.has(key);
export const bindHandlers = (
initEvent: Event,
listeners: any,
editor: any,
): void => {
Object.keys(listeners)
.filter((element) => isValidKey(element))
.forEach((key: string) => {
const handler = listeners[key];
if (typeof handler === 'function') {
if (key === 'onInit') {
handler(initEvent, editor);
} else {
editor.on(key.slice(2), (e: any) => handler(e, editor));
}
}
});
};

View File

@@ -1,115 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
import { message, Upload } from 'ant-design-vue';
defineOptions({ name: 'TinymceImageUpload' });
const props = defineProps({
disabled: {
default: false,
type: Boolean,
},
fullscreen: {
type: Boolean,
},
});
const emit = defineEmits(['uploading', 'done', 'error']);
let uploading = false;
const { apiURL, clientId } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
const accessStore = useAccessStore();
const uploadUrl = `${apiURL}/resource/oss/upload`;
// 使用upload组件只能这样上传
const headers = {
Authorization: `Bearer ${accessStore.accessToken}`,
clientId,
};
const getButtonProps = computed(() => {
const { disabled } = props;
return {
disabled,
};
});
function handleChange(info: Record<string, any>) {
const file = info.file;
const status = file?.status;
// const url = file?.response?.data.url;
const name = file?.name;
switch (status) {
case 'done': {
// http 200会走到这里 需要再次判断
const { response } = file;
const { code, data, msg = '服务器错误' } = response;
if (code === 200) {
const { url } = data;
emit('done', name, url);
} else {
message.error(msg);
}
// emit('done', name, url);
uploading = false;
break;
}
case 'error': {
emit('error');
uploading = false;
break;
}
case 'uploading': {
if (!uploading) {
emit('uploading', name);
uploading = true;
}
break;
}
// No default
}
}
</script>
<template>
<div :class="[{ fullscreen }]" class="tinymce-image-upload">
<Upload
:action="uploadUrl"
:headers="headers"
:show-upload-list="false"
accept=".jpg,.jpeg,.gif,.png,.webp"
multiple
name="file"
@change="handleChange"
>
<!-- 这里要改成i18n -->
<a-button type="primary" v-bind="{ ...getButtonProps }">
图片上传
</a-button>
</Upload>
</div>
</template>
<style lang="scss" scoped>
.tinymce-image-upload {
position: absolute;
top: 4px;
right: 10px;
z-index: 20;
&.fullscreen {
position: fixed;
z-index: 10000;
}
}
</style>

View File

@@ -5,6 +5,7 @@ import type { MenuOption } from '#/api/system/menu/model';
import { eachTree, treeToList } from '@vben/utils';
import { notification } from 'ant-design-vue';
import { difference, isEmpty, isUndefined } from 'lodash-es';
/**
@@ -48,6 +49,7 @@ export function rowAndChildrenChecked(
*/
export function menusWithPermissions(menus: MenuOption[]) {
eachTree(menus, (item: MenuPermissionOption) => {
validateMenuTree(item);
if (item.children && item.children.length > 0) {
/**
* 所有为按钮的节点提取出来
@@ -131,3 +133,50 @@ export function setTableChecked(
tableApi.grid.setCheckboxRow(emptyRows, false);
}
}
/**
* 校验是否符合规范 给出warning提示
*
* 不符合规范
* 比如: 菜单下放目录 菜单下放菜单
* 比如: 按钮下放目录 按钮下放菜单 按钮下放按钮
* @param menu menu
*/
function validateMenuTree(menu: MenuOption) {
/**
* C: { icon: markRaw(MenuIcon), value: '菜单' },
F: { icon: markRaw(OkButtonIcon), value: '按钮' },
M: { icon: markRaw(FolderIcon), value: '目录' },
*/
// 菜单下不能放目录/菜单
if (menu.menuType === 'C') {
menu.children?.forEach?.((item) => {
if (['C', 'M'].includes(item.menuType)) {
const description = `错误用法: [${menu.label} - 菜单]下不能放 目录/菜单 -> [${item.label}]`;
console.warn(description);
notification.warning({
message: '提示',
description,
duration: 0,
});
}
});
}
// 按钮为最末级 不能再放置
if (menu.menuType === 'F') {
/**
* 其实可以直接判断length 这里为了更准确知道label 采用遍历的形式
*/
menu.children?.forEach?.((item) => {
if (['C', 'F', 'M'].includes(item.menuType)) {
const description = `错误用法: [${menu.label} - 按钮]下不能放置'目录/菜单/按钮' -> [${item.label}]`;
console.warn(description);
notification.warning({
message: '提示',
description,
duration: 0,
});
}
});
}
}

View File

@@ -55,8 +55,7 @@ const props = withDefaults(
/**
* 是否节点关联
*/
const association = defineModel('association', {
type: Boolean,
const association = defineModel<boolean>('association', {
default: true,
});
@@ -318,7 +317,7 @@ function getKeys(records: MenuPermissionOption[], addCurrent: boolean) {
function getCheckedKeys() {
// 节点关联
if (association.value) {
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];
// 子节点
const nodeKeys = getKeys(records, true);
// 所有父节点
@@ -330,7 +329,7 @@ function getCheckedKeys() {
// 节点独立
// 勾选的行
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];
// 全部数据 用于获取permissions
const allRecords = tableApi?.grid?.getData?.() ?? [];
// 表格已经选中的行ids

View File

@@ -0,0 +1,8 @@
/**
* @description: 旧版文件上传组件 使用FileUpload代替
*/
export { default as FileUploadOld } from './src/file-upload.vue';
/**
* @description: 旧版图片上传组件 使用ImageUpload代替
*/
export { default as ImageUploadOld } from './src/image-upload.vue';

View File

@@ -0,0 +1,240 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosProgressEvent, UploadApi } from '#/api';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import { UploadOutlined } from '@ant-design/icons-vue';
import { message, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString } from 'lodash-es';
import { uploadApi } from '#/api';
import { checkFileType } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
/**
* 建议使用拓展名(不带.)
* 或者文件头 image/png等(测试判断不准确) 不支持image/*类似的写法
* 需自行改造 ./helper/checkFileType方法
*/
accept?: string[];
api?: UploadApi;
disabled?: boolean;
helpText?: string;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
// 返回的字段 默认url
resultField?: 'fileName' | 'ossId' | 'url' | string;
/**
* 是否显示下面的描述
*/
showDescription?: boolean;
value?: string[];
}>(),
{
value: () => [],
disabled: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => [],
multiple: false,
api: () => uploadApi,
resultField: '',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
const isFirstRender = ref<boolean>(true);
watch(
() => props.value,
(v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string[] = [];
if (v) {
if (isArray(v)) {
value = v;
} else {
value.push(v);
}
fileList.value = value.map((item, i) => {
if (item && isString(item)) {
return {
uid: `${-i}`,
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
};
} else if (item && isObject(item)) {
return item;
}
return null;
}) as UploadProps['fileList'];
}
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
emit('delete', file);
}
};
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props;
const isAct = await checkFileType(file, accept);
if (!isAct) {
message.error($t('component.upload.acceptUpload', [accept]));
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, {
onUploadProgress: progressEvent,
});
/**
* 由getValue处理 传对象过去
* 直接传string(id)会被转为Number
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
*/
info.onSuccess!(res);
message.success($t('component.upload.uploadSuccess'));
// 获取
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response?.[props.resultField];
}
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
if (item?.url) {
return item.url;
}
// 注意这里取的key为 url
return item?.response?.url;
});
return list;
}
</script>
<template>
<div>
<Upload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:custom-request="customRequest"
:disabled="disabled"
:max-count="maxNumber"
:multiple="multiple"
list-type="text"
:progress="{ showInfo: true }"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber">
<a-button>
<UploadOutlined />
{{ $t('component.upload.upload') }}
</a-button>
</div>
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
</Upload>
</div>
</template>
<style>
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>

View File

@@ -0,0 +1,51 @@
import { fileTypeFromBlob } from '@vben/utils';
/**
* 不支持txt文件 @see https://github.com/sindresorhus/file-type/issues/55
* 需要自行修改
* @param file file对象
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
* @returns 是否通过文件类型校验
*/
export async function checkFileType(file: File, accepts: string[]) {
if (!accepts || accepts?.length === 0) {
return true;
}
console.log(file);
const fileType = await fileTypeFromBlob(file);
if (!fileType) {
console.error('无法获取文件类型');
return false;
}
console.log('文件类型', fileType);
// 是否文件拓展名/文件头任意有一个匹配
return accepts.includes(fileType.ext) || accepts.includes(fileType.mime);
}
/**
* 默认图片类型
*/
export const defaultImageAccept = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
/**
* 判断文件类型是否符合要求
* @param file file对象
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
* @returns 是否通过文件类型校验
*/
export async function checkImageFileType(file: File, accepts: string[]) {
// 空的accepts 使用默认规则
if (!accepts || accepts.length === 0) {
accepts = defaultImageAccept;
}
const fileType = await fileTypeFromBlob(file);
if (!fileType) {
console.error('无法获取文件类型');
return false;
}
console.log('文件类型', fileType);
// 是否文件拓展名/文件头任意有一个匹配
if (accepts.includes(fileType.ext) || accepts.includes(fileType.mime)) {
return true;
}
return false;
}

View File

@@ -0,0 +1,323 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosProgressEvent, UploadApi } from '#/api';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import { PlusOutlined } from '@ant-design/icons-vue';
import { message, Modal, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
import { uploadApi } from '#/api';
import { ossInfo } from '#/api/system/oss';
import { checkImageFileType, defaultImageAccept } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
/**
* 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
*/
accept?: string[];
api?: UploadApi;
disabled?: boolean;
helpText?: string;
// eslint-disable-next-line no-use-before-define
listType?: ListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
// 返回的字段 默认url
resultField?: 'fileName' | 'ossId' | 'url';
/**
* 是否显示下面的描述
*/
showDescription?: boolean;
value?: string | string[];
}>(),
{
value: () => [],
disabled: false,
listType: 'picture-card',
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => defaultImageAccept,
multiple: false,
api: () => uploadApi,
resultField: 'url',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
type ListType = 'picture' | 'picture-card' | 'text';
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
const previewOpen = ref<boolean>(false);
const previewImage = ref<string>('');
const previewTitle = ref<string>('');
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
const isFirstRender = ref<boolean>(true);
watch(
() => props.value,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string | string[] = [];
if (v) {
const _fileList: string[] = [];
if (isString(v)) {
_fileList.push(v);
}
if (isArray(v)) {
_fileList.push(...v);
}
// 直接赋值 可能为string | string[]
value = v;
const withUrlList: UploadProps['fileList'] = [];
for (const item of _fileList) {
// ossId情况
if (props.resultField === 'ossId') {
const resp = await ossInfo([item]);
if (item && isString(item)) {
withUrlList.push({
uid: item, // ossId作为uid 方便getValue获取
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: resp?.[0]?.url,
});
} else if (item && isObject(item)) {
withUrlList.push({
...(item as any),
uid: item,
url: resp?.[0]?.url,
});
}
} else {
// 非ossId情况
if (item && isString(item)) {
withUrlList.push({
uid: uniqueId(),
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
});
} else if (item && isObject(item)) {
withUrlList.push(item);
}
}
}
fileList.value = withUrlList;
}
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
return new Promise<T>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => {
resolve(reader.result as T);
});
reader.addEventListener('error', (error) => reject(error));
});
}
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64<string>(file.originFileObj!);
}
previewImage.value = file.url || file.preview || '';
previewOpen.value = true;
previewTitle.value =
file.name ||
previewImage.value.slice(
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
);
};
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
emit('delete', file);
}
};
const handleCancel = () => {
previewOpen.value = false;
previewTitle.value = '';
};
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props;
const isAct = await checkImageFileType(file, accept);
if (!isAct) {
message.error($t('component.upload.acceptUpload', [accept]));
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, {
onUploadProgress: progressEvent,
});
/**
* 由getValue处理 传对象过去
* 直接传string(id)会被转为Number
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
*/
info.onSuccess!(res);
message.success($t('component.upload.uploadSuccess'));
// 获取
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
function getValue() {
console.log(fileList.value);
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response?.[props.resultField];
}
// ossId兼容 uid为ossId直接返回
if (props.resultField === 'ossId' && item.uid) {
return item.uid;
}
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
if (item?.url) {
return item.url;
}
// 注意这里取的key为 url
return item?.response?.url;
});
// 只有一张图片 默认绑定string而非string[]
if (props.maxNumber === 1 && list.length === 1) {
return list[0];
}
// 只有一张图片 && 删除图片时 可自行修改
if (props.maxNumber === 1 && list.length === 0) {
return '';
}
return list;
}
</script>
<template>
<div>
<Upload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:custom-request="customRequest"
:disabled="disabled"
:list-type="listType"
:max-count="maxNumber"
:multiple="multiple"
:progress="{ showInfo: true }"
@preview="handlePreview"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber">
<PlusOutlined />
<div style="margin-top: 8px">{{ $t('component.upload.upload') }}</div>
</div>
</Upload>
<div
v-if="showDescription"
class="mt-2 flex flex-wrap items-center text-[14px]"
>
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
<Modal
:footer="null"
:open="previewOpen"
:title="previewTitle"
@cancel="handleCancel"
>
<img :src="previewImage" alt="" style="width: 100%" />
</Modal>
</div>
</template>
<style>
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>

View File

@@ -1,243 +1,150 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
<!--
不再支持url 统一使用ossId
去除使用`file-type`库进行文件类型检测 在Safari无法使用
-->
<script setup lang="ts">
import type { UploadListType } from 'ant-design-vue/es/upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { BaseUploadProps, UploadEmits } from './props';
import type { AxiosProgressEvent } from '#/api';
import { computed } from 'vue';
import { ref, toRefs, watch } from 'vue';
import { $t, I18nT } from '@vben/locales';
import { $t } from '@vben/locales';
import { UploadOutlined } from '@ant-design/icons-vue';
import { message, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString } from 'lodash-es';
import { InboxOutlined, UploadOutlined } from '@ant-design/icons-vue';
import { Upload } from 'ant-design-vue';
import { uploadApi } from '#/api';
import { checkFileType } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
import { defaultFileAcceptExts, defaultFilePreview } from './helper';
import { useUpload } from './hook';
defineOptions({ name: 'FileUpload', inheritAttrs: false });
interface FileUploadProps extends BaseUploadProps {
/**
* 同antdv的listType 但是排除picture-card
* 文件上传不适合用picture-card显示
* @default text
*/
listType?: Exclude<UploadListType, 'picture-card'>;
}
const props = withDefaults(
defineProps<{
/**
* 建议使用拓展名(不带.)
* 或者文件头 image/png等(测试判断不准确) 不支持image/*类似的写法
* 需自行改造 ./helper/checkFileType方法
*/
accept?: string[];
api?: (
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
disabled?: boolean;
helpText?: string;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
// 返回的字段 默认url
resultField?: 'fileName' | 'ossId' | 'url' | string;
/**
* 是否显示下面的描述
*/
showDescription?: boolean;
value?: string[];
}>(),
{
value: () => [],
disabled: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => [],
multiple: false,
api: uploadApi,
resultField: '',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
const props = withDefaults(defineProps<FileUploadProps>(), {
api: () => uploadApi,
removeOnError: true,
showSuccessMsg: true,
removeConfirm: false,
accept: defaultFileAcceptExts.join(','),
data: () => undefined,
maxCount: 1,
maxSize: 5,
disabled: false,
helpMessage: true,
preview: defaultFilePreview,
enableDragUpload: false,
directory: false,
abortOnUnmounted: true,
listType: 'text',
});
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
const isFirstRender = ref<boolean>(true);
const emit = defineEmits<UploadEmits>();
watch(
() => props.value,
(v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string[] = [];
if (v) {
if (isArray(v)) {
value = v;
} else {
value.push(v);
}
fileList.value = value.map((item, i) => {
if (item && isString(item)) {
return {
uid: `${-i}`,
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
};
} else if (item && isObject(item)) {
return item;
}
return null;
}) as UploadProps['fileList'];
}
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
/** 返回不同的上传组件 */
const CurrentUploadComponent = computed(() => {
if (props.enableDragUpload) {
return Upload.Dragger;
}
return Upload;
});
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
emit('delete', file);
}
};
// 双向绑定 ossId
const ossIdList = defineModel<string | string[]>('value', {
default: () => [],
});
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props;
const isAct = await checkFileType(file, accept);
if (!isAct) {
message.error($t('component.upload.acceptUpload', [accept]));
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
/**
* 由getValue处理 传对象过去
* 直接传string(id)会被转为Number
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
*/
info.onSuccess!(res);
message.success($t('component.upload.uploadSuccess'));
// 获取
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response?.[props.resultField];
}
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
if (item?.url) {
return item.url;
}
// 注意这里取的key为 url
return item?.response?.url;
});
return list;
}
const {
customRequest,
acceptStr,
handleChange,
handleRemove,
beforeUpload,
innerFileList,
} = useUpload(props, emit, ossIdList, 'file');
</script>
<!--
Upload.Dragger只会影响样式
使用普通Upload也是支持拖拽上传的
-->
<template>
<div>
<Upload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
<CurrentUploadComponent
v-model:file-list="innerFileList"
:accept="accept"
:list-type="listType"
:disabled="disabled"
:directory="directory"
:max-count="maxCount"
:progress="{ showInfo: true }"
:multiple="multiple"
:before-upload="beforeUpload"
:custom-request="customRequest"
:disabled="disabled"
:max-count="maxNumber"
:multiple="multiple"
list-type="text"
:progress="{ showInfo: true }"
@preview="preview"
@change="handleChange"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber">
<a-button>
<div v-if="!enableDragUpload && innerFileList?.length < maxCount">
<a-button :disabled="disabled">
<UploadOutlined />
{{ $t('component.upload.upload') }}
</a-button>
</div>
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
<div v-if="enableDragUpload">
<p class="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p class="ant-upload-text">
{{ $t('component.upload.clickOrDrag') }}
</p>
</div>
</Upload>
</CurrentUploadComponent>
<slot name="helpMessage" v-bind="{ maxCount, disabled, maxSize, accept }">
<I18nT
v-if="helpMessage"
scope="global"
keypath="component.upload.uploadHelpMessage"
tag="div"
class="mt-2"
:class="{ 'upload-text__disabled': disabled }"
>
<template #size>
<span
class="text-primary mx-1 font-medium"
:class="{ 'upload-text__disabled': disabled }"
>
{{ maxSize }}MB
</span>
</template>
<template #ext>
<span
class="text-primary mx-1 font-medium"
:class="{ 'upload-text__disabled': disabled }"
>
{{ acceptStr }}
</span>
</template>
</I18nT>
</slot>
</div>
</template>
<style lang="less">
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
}
<style lang="scss">
// 禁用的样式和antd保持一致
.upload-text__disabled {
color: rgb(50 54 57 / 25%);
cursor: not-allowed;
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
&:where(.dark, .dark *) {
color: rgb(242 242 242 / 25%);
}
}
</style>

View File

@@ -1,51 +1,28 @@
import { fileTypeFromBlob } from '@vben/utils';
import type { UploadFile } from 'ant-design-vue';
/**
* 不支持txt文件 @see https://github.com/sindresorhus/file-type/issues/55
* 需要自行修改
* @param file file对象
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
* @returns 是否通过文件类型校验
* 默认支持上传的图片文件类型
*/
export async function checkFileType(file: File, accepts: string[]) {
if (!accepts || accepts?.length === 0) {
return true;
}
console.log(file);
const fileType = await fileTypeFromBlob(file);
if (!fileType) {
console.error('无法获取文件类型');
return false;
}
console.log('文件类型', fileType);
// 是否文件拓展名/文件头任意有一个匹配
return accepts.includes(fileType.ext) || accepts.includes(fileType.mime);
}
export const defaultImageAcceptExts = [
'.jpg',
'.jpeg',
'.png',
'.gif',
'.webp',
];
/**
* 默认图片类型
* 默认支持上传的文件类型
*/
export const defaultImageAccept = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
export const defaultFileAcceptExts = ['.xlsx', '.csv', '.docx', '.pdf'];
/**
* 判断文件类型是否符合要求
* @param file file对象
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
* @returns 是否通过文件类型校验
* 文件(非图片)的默认预览逻辑
* 默认: window.open打开 交给浏览器接管
* @param file file
*/
export async function checkImageFileType(file: File, accepts: string[]) {
// 空的accepts 使用默认规则
if (!accepts || accepts.length === 0) {
accepts = defaultImageAccept;
export function defaultFilePreview(file: UploadFile) {
if (file?.url) {
window.open(file.url);
}
const fileType = await fileTypeFromBlob(file);
if (!fileType) {
console.error('无法获取文件类型');
return false;
}
console.log('文件类型', fileType);
// 是否文件拓展名/文件头任意有一个匹配
if (accepts.includes(fileType.ext) || accepts.includes(fileType.mime)) {
return true;
}
return false;
}

View File

@@ -0,0 +1,385 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
import type { FileType } from 'ant-design-vue/es/upload/interface';
import type {
RcFile,
UploadRequestOption,
} from 'ant-design-vue/es/vc-upload/interface';
import type { ModelRef } from 'vue';
import type {
BaseUploadProps,
CustomGetter,
UploadEmits,
UploadType,
} from './props';
import type { AxiosProgressEvent, UploadResult } from '#/api';
import type { OssFile } from '#/api/system/oss/model';
import { computed, onUnmounted, ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { message, Modal } from 'ant-design-vue';
import { isFunction, isString } from 'lodash-es';
import { ossInfo } from '#/api/system/oss';
/**
* 图片预览hook
* @returns 预览
*/
export function useImagePreview() {
/**
* 获取base64字符串
* @param file 文件
* @returns base64字符串
*/
function getBase64(file: File) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', (error) => reject(error));
});
}
// Modal可见
const previewVisible = ref(false);
// 预览的图片 url/base64
const previewImage = ref('');
// 预览的图片名称
const previewTitle = ref('');
function handleCancel() {
previewVisible.value = false;
previewTitle.value = '';
}
async function handlePreview(file: UploadFile) {
if (!file) {
return;
}
// 文件预览 取base64
if (!file.url && !file.preview && file.originFileObj) {
file.preview = (await getBase64(file.originFileObj)) as string;
}
// 这里不可能为空
const url = file.url ?? '';
previewImage.value = url || file.preview || '';
previewVisible.value = true;
previewTitle.value =
file.name || url.slice(Math.max(0, url.lastIndexOf('/') + 1));
}
return {
previewVisible,
previewImage,
previewTitle,
handleCancel,
handlePreview,
};
}
/**
* 图片上传和文件上传的通用hook
* @param props 组件props
* @param emit 事件
* @param bindValue 双向绑定的idList
* @param uploadType 区分是文件还是图片上传
* @returns hook
*/
export function useUpload(
props: Readonly<BaseUploadProps>,
emit: UploadEmits,
bindValue: ModelRef<string | string[]>,
uploadType: UploadType,
) {
// 组件内部维护fileList
const innerFileList = ref<UploadFile[]>([]);
const acceptStr = computed(() => {
// string类型
if (isString(props.acceptFormat)) {
return props.acceptFormat;
}
// 函数类型
if (isFunction(props.acceptFormat)) {
return props.acceptFormat(props.accept!);
}
// 默认 会对拓展名做处理
return props.accept
?.split(',')
.map((item) => {
if (item.startsWith('.')) {
return item.slice(1);
}
return item;
})
.join(', ');
});
/**
* 自定义文件显示名称 需要区分不同的接口
* @param cb callback
* @returns 文件名
*/
function transformFilename(cb: Parameters<CustomGetter<string>>[0]) {
if (isFunction(props.customFilename)) {
return props.customFilename(cb);
}
// info接口
if (cb.type === 'info') {
return cb.response.originalName;
}
// 上传接口
return cb.response.fileName;
}
/**
* 自定义缩略图 需要区分不同的接口
* @param cb callback
* @returns 缩略图地址
*/
function transformThumbUrl(cb: Parameters<CustomGetter<undefined>>[0]) {
if (isFunction(props.customThumbUrl)) {
return props.customThumbUrl(cb);
}
// image 默认返回图片链接
if (uploadType === 'image') {
// info接口
if (cb.type === 'info') {
return cb.response.url;
}
// 上传接口
return cb.response.url;
}
// 文件默认返回空 走antd默认的预览图逻辑
return undefined;
}
// 用来标识是否为上传 这样在watch内部不需要请求api
let isUpload = false;
function handleChange(info: UploadChangeParam) {
/**
* 移除当前文件
* @param currentFile 当前文件
* @param currentFileList 当前所有文件list
*/
function removeCurrentFile(
currentFile: UploadChangeParam['file'],
currentFileList: UploadChangeParam['fileList'],
) {
if (props.removeOnError) {
currentFileList.splice(currentFileList.indexOf(currentFile), 1);
} else {
currentFile.status = 'error';
}
}
const { file: currentFile, fileList } = info;
switch (currentFile.status) {
// 上传成功 只是判断httpStatus 200 需要手动判断业务code
case 'done': {
if (!currentFile.response) {
return;
}
// 获取返回结果 为customRequest的reslove参数
// 只有success才会走到这里
const { ossId, url } = currentFile.response as UploadResult;
currentFile.url = url;
currentFile.uid = ossId;
const cb = {
type: 'upload',
response: currentFile.response as UploadResult,
} as const;
currentFile.fileName = transformFilename(cb);
currentFile.name = transformFilename(cb);
currentFile.thumbUrl = transformThumbUrl(cb);
// 标记为上传 watch根据值做处理
isUpload = true;
// ossID添加 单个文件会被当做string
if (props.maxCount === 1) {
bindValue.value = ossId;
} else {
// 给默认值
if (!Array.isArray(bindValue.value)) {
bindValue.value = [];
}
// 直接使用.value无法触发useForm的更新(原生是正常的) 需要修改地址
bindValue.value = [...bindValue.value, ossId];
}
break;
}
// 上传失败 网络原因导致httpStatus 不等于200
case 'error': {
removeCurrentFile(currentFile, fileList);
}
}
emit('change', info);
}
function handleRemove(currentFile: UploadFile) {
function remove() {
// fileList会自行处理删除 这里只需要处理ossId
if (props.maxCount === 1) {
bindValue.value = '';
} else {
(bindValue.value as string[]).splice(
bindValue.value.indexOf(currentFile.uid),
1,
);
}
// 触发remove事件
emit('remove', currentFile);
}
if (!props.removeConfirm) {
remove();
return true;
}
return new Promise<boolean>((resolve) => {
Modal.confirm({
title: $t('pages.common.tip'),
content: $t('component.upload.confirmDelete', [currentFile.name]),
okButtonProps: { danger: true },
centered: true,
onOk() {
resolve(true);
remove();
},
onCancel() {
resolve(false);
},
});
});
}
/**
* 上传前检测文件大小
* 拖拽时候前置会有浏览器自身的accept校验 校验失败不会执行此方法
* @param file file
* @returns file | false
*/
function beforeUpload(file: FileType) {
const isLtMax = file.size / 1024 / 1024 < props.maxSize!;
if (!isLtMax) {
message.error($t('component.upload.maxSize', [props.maxSize]));
return false;
}
// 大坑 Safari不支持file-type库 去除文件类型的校验
return file;
}
const uploadAbort = new AbortController();
/**
* 自定义上传实现
* @param info
*/
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api(info.file as File, {
onUploadProgress: progressEvent,
signal: uploadAbort.signal,
otherData: props?.data,
});
info.onSuccess!(res);
if (props.showSuccessMsg) {
message.success($t('component.upload.uploadSuccess'));
}
emit('success', info.file as RcFile, res);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
onUnmounted(() => {
props.abortOnUnmounted && uploadAbort.abort();
});
/**
* 这里默认只监听list地址变化 即重新赋值才会触发watch
* immediate用于初始化触发
*/
watch(
() => bindValue.value,
async (value) => {
if (value.length === 0) {
// 清空绑定值时同时清空innerFileList避免外部使用时还能读取到
innerFileList.value = [];
return;
}
// 上传完毕 不需要调用获取信息接口
if (isUpload) {
// 清理 使下一次状态可用
isUpload = false;
return;
}
const resp = await ossInfo(value);
function transformFile(info: OssFile) {
const cb = { type: 'info', response: info } as const;
const fileitem: UploadFile = {
uid: info.ossId,
name: transformFilename(cb),
fileName: transformFilename(cb),
url: info.url,
thumbUrl: transformThumbUrl(cb),
status: 'done',
};
return fileitem;
}
const transformOptions = resp.map((item) => transformFile(item));
innerFileList.value = transformOptions;
// 单文件 丢弃策略
if (props.maxCount === 1 && resp.length === 0 && !props.keepMissingId) {
bindValue.value = '';
return;
}
// 多文件
// 单文件查到了也会走这里的逻辑 filter会报错 需要maxCount判断处理
if (
resp.length !== value.length &&
!props.keepMissingId &&
props.maxCount !== 1
) {
// 给默认值
if (!Array.isArray(bindValue.value)) {
bindValue.value = [];
}
bindValue.value = bindValue.value.filter((ossId) =>
resp.map((res) => res.ossId).includes(ossId),
);
}
},
{ immediate: true },
);
return {
handleChange,
handleRemove,
beforeUpload,
customRequest,
innerFileList,
acceptStr,
};
}

View File

@@ -1,326 +1,190 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
<!--
不再支持url 统一使用ossId
去除使用`file-type`库进行文件类型检测 在Safari无法使用
-->
<script setup lang="ts">
import type {
UploadFile,
UploadListType,
} from 'ant-design-vue/es/upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { BaseUploadProps, UploadEmits } from './props';
import type { AxiosProgressEvent } from '#/api';
import { $t, I18nT } from '@vben/locales';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import { PlusOutlined } from '@ant-design/icons-vue';
import { message, Modal, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
import { PlusOutlined, UploadOutlined } from '@ant-design/icons-vue';
import { Image, ImagePreviewGroup, Upload } from 'ant-design-vue';
import { isFunction } from 'lodash-es';
import { uploadApi } from '#/api';
import { ossInfo } from '#/api/system/oss';
import { checkImageFileType, defaultImageAccept } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
import { defaultImageAcceptExts } from './helper';
import { useImagePreview, useUpload } from './hook';
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
interface ImageUploadProps extends BaseUploadProps {
/**
* 同antdv的listType
* @default picture-card
*/
listType?: UploadListType;
/**
* 使用list-type: picture-card时 是否显示动画
* 会有一个`弹跳`的效果 默认关闭
* @default false
*/
withAnimation?: boolean;
}
const props = withDefaults(
defineProps<{
/**
* 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
*/
accept?: string[];
api?: (
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
disabled?: boolean;
helpText?: string;
// eslint-disable-next-line no-use-before-define
listType?: ListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
// 返回的字段 默认url
resultField?: 'fileName' | 'ossId' | 'url';
/**
* 是否显示下面的描述
*/
showDescription?: boolean;
value?: string | string[];
}>(),
{
value: () => [],
disabled: false,
listType: 'picture-card',
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => defaultImageAccept,
multiple: false,
api: uploadApi,
resultField: 'url',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
type ListType = 'picture' | 'picture-card' | 'text';
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
const props = withDefaults(defineProps<ImageUploadProps>(), {
api: () => uploadApi,
removeOnError: true,
showSuccessMsg: true,
removeConfirm: false,
accept: defaultImageAcceptExts.join(','),
data: () => undefined,
maxCount: 1,
maxSize: 5,
disabled: false,
listType: 'picture-card',
helpMessage: true,
enableDragUpload: false,
abortOnUnmounted: true,
withAnimation: false,
});
const previewOpen = ref<boolean>(false);
const previewImage = ref<string>('');
const previewTitle = ref<string>('');
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
const isFirstRender = ref<boolean>(true);
const emit = defineEmits<UploadEmits>();
watch(
() => props.value,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string | string[] = [];
if (v) {
const _fileList: string[] = [];
if (isString(v)) {
_fileList.push(v);
}
if (isArray(v)) {
_fileList.push(...v);
}
// 直接赋值 可能为string | string[]
value = v;
const withUrlList: UploadProps['fileList'] = [];
for (const item of _fileList) {
// ossId情况
if (props.resultField === 'ossId') {
const resp = await ossInfo([item]);
if (item && isString(item)) {
withUrlList.push({
uid: item, // ossId作为uid 方便getValue获取
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: resp?.[0]?.url,
});
} else if (item && isObject(item)) {
withUrlList.push({
...(item as any),
uid: item,
url: resp?.[0]?.url,
});
}
} else {
// 非ossId情况
if (item && isString(item)) {
withUrlList.push({
uid: uniqueId(),
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
});
} else if (item && isObject(item)) {
withUrlList.push(item);
}
}
}
fileList.value = withUrlList;
}
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
// 双向绑定 ossId
const ossIdList = defineModel<string | string[]>('value', {
default: () => [],
});
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
return new Promise<T>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => {
resolve(reader.result as T);
});
reader.addEventListener('error', (error) => reject(error));
});
}
const {
acceptStr,
handleChange,
handleRemove,
beforeUpload,
innerFileList,
customRequest,
} = useUpload(props, emit, ossIdList, 'image');
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64<string>(file.originFileObj!);
const { previewVisible, previewImage, handleCancel, handlePreview } =
useImagePreview();
function currentPreview(file: UploadFile) {
// 有自定义预览逻辑走自定义
if (isFunction(props.preview)) {
return props.preview(file);
}
previewImage.value = file.url || file.preview || '';
previewOpen.value = true;
previewTitle.value =
file.name ||
previewImage.value.slice(
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
);
};
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
emit('delete', file);
}
};
const handleCancel = () => {
previewOpen.value = false;
previewTitle.value = '';
};
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props;
const isAct = await checkImageFileType(file, accept);
if (!isAct) {
message.error($t('component.upload.acceptUpload', [accept]));
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
/**
* 由getValue处理 传对象过去
* 直接传string(id)会被转为Number
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
*/
info.onSuccess!(res);
message.success($t('component.upload.uploadSuccess'));
// 获取
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
function getValue() {
console.log(fileList.value);
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response?.[props.resultField];
}
// ossId兼容 uid为ossId直接返回
if (props.resultField === 'ossId' && item.uid) {
return item.uid;
}
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
if (item?.url) {
return item.url;
}
// 注意这里取的key为 url
return item?.response?.url;
});
// 只有一张图片 默认绑定string而非string[]
if (props.maxNumber === 1 && list.length === 1) {
return list[0];
}
// 只有一张图片 && 删除图片时 可自行修改
if (props.maxNumber === 1 && list.length === 0) {
return '';
}
return list;
// 否则走默认预览
return handlePreview(file);
}
</script>
<template>
<div>
<Upload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
v-model:file-list="innerFileList"
:class="{ 'upload-animation__disabled': !withAnimation }"
:list-type="listType"
:accept="accept"
:disabled="disabled"
:directory="directory"
:max-count="maxCount"
:progress="{ showInfo: true }"
:multiple="multiple"
:before-upload="beforeUpload"
:custom-request="customRequest"
:disabled="disabled"
:list-type="listType"
:max-count="maxNumber"
:multiple="multiple"
:progress="{ showInfo: true }"
@preview="handlePreview"
@preview="currentPreview"
@change="handleChange"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber">
<div
v-if="innerFileList?.length < maxCount && listType === 'picture-card'"
>
<PlusOutlined />
<div style="margin-top: 8px">{{ $t('component.upload.upload') }}</div>
<div class="mt-[8px]">{{ $t('component.upload.upload') }}</div>
</div>
<a-button
v-if="innerFileList?.length < maxCount && listType !== 'picture-card'"
:disabled="disabled"
>
<UploadOutlined />
{{ $t('component.upload.upload') }}
</a-button>
</Upload>
<div
v-if="showDescription"
class="mt-2 flex flex-wrap items-center text-[14px]"
<slot name="helpMessage" v-bind="{ maxCount, disabled, maxSize, accept }">
<I18nT
v-if="helpMessage"
scope="global"
keypath="component.upload.uploadHelpMessage"
tag="div"
:class="{
'upload-text__disabled': disabled,
'mt-2': listType !== 'picture-card',
}"
>
<template #size>
<span
class="text-primary mx-1 font-medium"
:class="{ 'upload-text__disabled': disabled }"
>
{{ maxSize }}MB
</span>
</template>
<template #ext>
<span
class="text-primary mx-1 font-medium"
:class="{ 'upload-text__disabled': disabled }"
>
{{ acceptStr }}
</span>
</template>
</I18nT>
</slot>
<ImagePreviewGroup
:preview="{
visible: previewVisible,
onVisibleChange: handleCancel,
}"
>
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
<Modal
:footer="null"
:open="previewOpen"
:title="previewTitle"
@cancel="handleCancel"
>
<img :src="previewImage" alt="" style="width: 100%" />
</Modal>
<Image class="hidden" :src="previewImage" />
</ImagePreviewGroup>
</div>
</template>
<style lang="less">
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
<style lang="scss">
.ant-upload-select-picture-card {
i {
@apply text-[32px] text-[#999];
}
.ant-upload-text {
@apply mt-[8px] text-[#666];
}
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
.ant-upload-list-picture-card {
.ant-upload-list-item::before {
border-radius: 4px;
}
}
// 禁用的样式和antd保持一致
.upload-text__disabled {
color: rgb(50 54 57 / 25%);
cursor: not-allowed;
&:where(.dark, .dark *) {
color: rgb(242 242 242 / 25%);
}
}
// list-type: picture-card动画效果关闭样式
.upload-animation__disabled {
.ant-upload-animate-inline {
animation-duration: 0s !important;
}
}
</style>

View File

@@ -0,0 +1,26 @@
Safari在执行到beforeUpload方法
有两种情况
1. 不继续执行 也无法上传(没有调用上传)
2. 报错
Unhandled Promise Rejection: TypeError: ReadableStreamBYOBReader needs a ReadableByteStreamController
https://github.com/oven-sh/bun/issues/12908#issuecomment-2490151231
刚开始以为是异步的问题 由于`file-type`调用了异步方法 调试也是在这里没有后续打印了
使用别的异步代码测试结果是正常上传的
```js
return new Promise<FileType>((resolve) =>
setTimeout(() => resolve(file), 2000),
);
```
根本原因在于`file-typ`库的`fileTypeFromBlob`方法不支持Safari 去掉可以正常上传
safari不支持`ReadableStreamBYOBReader`api
详见: https://github.com/sindresorhus/file-type/issues/690

View File

@@ -0,0 +1,122 @@
import type { UploadFile } from 'ant-design-vue';
import type { RcFile } from 'ant-design-vue/es/vc-upload/interface';
import type { UploadApi, UploadResult } from '#/api';
import type { OssFile } from '#/api/system/oss/model';
import { UploadChangeParam } from 'ant-design-vue';
export type UploadType = 'file' | 'image';
/**
* 自定义返回文件名/缩略图使用 泛型控制返回是否必填
* type 为不同的接口返回值 需要自行if判断
*/
export type CustomGetter<T extends string | undefined> = (
cb:
| { response: OssFile; type: 'info' }
| { response: UploadResult; type: 'upload' },
) => T extends undefined ? string | undefined : string;
export interface BaseUploadProps {
/**
* 上传接口
*/
api?: UploadApi;
/**
* 文件上传失败 是否从展示列表中删除
* @default true
*/
removeOnError?: boolean;
/**
* 上传成功 是否展示提示信息
* @default true
*/
showSuccessMsg?: boolean;
/**
* 删除文件前是否需要确认
* @default false
*/
removeConfirm?: boolean;
/**
* 同antdv参数
*/
accept?: string;
/**
* 你可能使用的是application/pdf这种mime类型, 但是这样用户可能看不懂, 在这里自定义逻辑
* @default 原始accept
*/
acceptFormat?: ((accept: string) => string) | string;
/**
* 附带的请求参数
*/
data?: any;
/**
* 最大上传图片数量
* maxCount为1时 会被绑定为string而非string[]
* @default 1
*/
maxCount?: number;
/**
* 文件最大 单位M
* @default 5
*/
maxSize?: number;
/**
* 是否禁用
* @default false
*/
disabled?: boolean;
/**
* 是否显示文案 请上传不超过...
* @default true
*/
helpMessage?: boolean;
/**
* 是否支持多选文件ie10+ 支持。开启后按住 ctrl 可选择多个文件。
* @default false
*/
multiple?: boolean;
/**
* 是否支持上传文件夹
* @default false
*/
directory?: boolean;
/**
* 是否支持拖拽上传
* @default false
*/
enableDragUpload?: boolean;
/**
* 当ossId查询不到文件信息时 比如被删除了
* 是否保留列表对应的ossId 默认不保留
* @default false
*/
keepMissingId?: boolean;
/**
* 自定义文件/图片预览逻辑 比如: 你可以改为下载
* 图片上传默认为预览
* 文件上传默认为window.open
* @param file file
*/
preview?: (file: UploadFile) => Promise<void> | void;
/**
* 是否在组件Unmounted时取消上传
* @default true
*/
abortOnUnmounted?: boolean;
/**
* 自定义文件名 需要区分两个接口的返回值
*/
customFilename?: CustomGetter<string>;
/**
* 自定义缩略图 需要区分两个接口的返回值
*/
customThumbUrl?: CustomGetter<undefined>;
}
export interface UploadEmits {
(e: 'success', file: RcFile, response: UploadResult): void;
(e: 'remove', file: UploadFile): void;
(e: 'change', info: UploadChangeParam): void;
}

View File

@@ -94,7 +94,7 @@ const menus = computed(() => {
});
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
return userStore.userInfo?.avatar || preferences.app.defaultAvatar;
});
async function handleLogout() {
@@ -116,7 +116,7 @@ watch(
async (enable) => {
if (enable) {
await updateWatermark({
content: `${userStore.userInfo?.username}`,
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();

View File

@@ -50,6 +50,10 @@
"uploadError": "Upload failed",
"uploading": "Uploading",
"uploadWait": "Please wait for the file upload to finish",
"reUploadFailed": "Re-upload failed files"
"reUploadFailed": "Re-upload failed files",
"uploadHelpMessage": "Please upload a file in {ext} format that does not exceed {size} .",
"unknownFileType": "Unknown file type, unable to upload",
"confirmDelete": "Confirm file deletion {0}?",
"clickOrDrag": "Click or drag file to this area to upload"
}
}

View File

@@ -18,6 +18,10 @@
"refresh": "Refresh",
"generate": "Generate",
"downloadLoading": "Downloading... Please wait.",
"preview": "Preview"
"preview": "Preview",
"tip": "Tip",
"enable": "On",
"disable": "Off",
"beforeCloseTip": "You have unsaved changes. Are you sure you want to exit?"
}
}

View File

@@ -50,6 +50,10 @@
"uploadError": "上传失败",
"uploading": "上传中",
"uploadWait": "请等待文件上传结束后操作",
"reUploadFailed": "重新上传失败文件"
"reUploadFailed": "重新上传失败文件",
"uploadHelpMessage": "请上传不超过{size}的{ext}格式文件",
"unknownFileType": "未知的文件类型, 无法上传",
"confirmDelete": "确认删除文件 {0}?",
"clickOrDrag": "点击或拖动文件到这个区域上传"
}
}

View File

@@ -18,6 +18,10 @@
"refresh": "刷新",
"generate": "生成",
"downloadLoading": "下载中, 请稍后...",
"preview": "预览"
"preview": "预览",
"tip": "提示",
"enable": "启用",
"disable": "禁用",
"beforeCloseTip": "您有未保存的更改,确认要退出吗?"
}
}

View File

@@ -46,6 +46,11 @@ export const overridesPreferences = defineOverridesPreferences({
* 浅色sidebar
*/
semiDarkSidebar: false,
/**
* 圆角大小 换算比例为1.6px = 0.1radius
* 这里为6px 与antd保持一致
*/
radius: '0.375',
},
/**
* !!! 更改配置后请清空浏览器缓存

View File

@@ -1,6 +1,6 @@
import type { Router } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
@@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH,
preferences.app.defaultHomePath,
);
}
return true;
@@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === DEFAULT_HOME_PATH
to.fullPath === preferences.app.defaultHomePath
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
@@ -108,8 +108,8 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH
? userInfo.homePath || DEFAULT_HOME_PATH
(to.path === preferences.app.defaultHomePath
? userInfo.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string;
return {

View File

@@ -1,11 +1,12 @@
import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
@@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
},
name: 'Root',
path: '/',
redirect: DEFAULT_HOME_PATH,
redirect: preferences.app.defaultHomePath,
children: [],
},
{
@@ -58,7 +59,7 @@ const coreRoutes: RouteRecordRaw[] = [
{
name: 'Login',
path: 'login',
component: Login,
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},

View File

@@ -3,6 +3,7 @@ import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
import { workflowIframeRoutes } from './workflow-iframe';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
@@ -26,11 +27,14 @@ const externalRoutes: RouteRecordRaw[] = [];
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
...workflowIframeRoutes,
fallbackNotFoundRoute,
];
/** 基本路由(登录, 第三方登录, 注册等) + workflowIframe路由不需要拦截 */
const basicRoutes = [...coreRoutes, ...workflowIframeRoutes];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
const coreRouteNames = traverseTreeValues(basicRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];

View File

@@ -2,6 +2,11 @@ import type { RouteRecordStringComponent } from '@vben/types';
import { $t } from '@vben/locales';
const {
version,
// vite inject-metadata 插件注入的全局变量
} = __VBEN_ADMIN_METADATA__ || {};
/**
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
*/
@@ -12,6 +17,7 @@ const localRoutes: RouteRecordStringComponent[] = [
icon: 'mingcute:profile-line',
title: $t('ui.widgets.profile'),
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'Profile',
path: '/profile',
@@ -23,6 +29,7 @@ const localRoutes: RouteRecordStringComponent[] = [
icon: 'ant-design:setting-outlined',
title: 'oss配置',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'OssConfig',
path: '/system/oss-config',
@@ -34,6 +41,7 @@ const localRoutes: RouteRecordStringComponent[] = [
icon: 'tabler:code',
title: '生成配置',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'GenConfig',
path: '/code-gen/edit/:tableId',
@@ -45,6 +53,7 @@ const localRoutes: RouteRecordStringComponent[] = [
icon: 'eos-icons:role-binding-outlined',
title: '分配角色',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'RoleAssign',
path: '/system/role-assign/:roleId',
@@ -56,10 +65,14 @@ const localRoutes: RouteRecordStringComponent[] = [
icon: 'fluent-mdl2:flow',
title: '流程设计',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'WorkflowDesigner',
path: '/workflow/designer',
},
/**
* 需要添加iframe路由 同目录的./workflow-iframe.ts
*/
{
component: 'workflow/leave/leave-form',
meta: {
@@ -67,22 +80,11 @@ const localRoutes: RouteRecordStringComponent[] = [
title: '请假申请',
activePath: '/demo/leave',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'WorkflowLeaveIndex',
path: '/workflow/leaveEdit/index',
},
// 这里是iframe使用的 去掉外层的BasicLayout
{
component: 'workflow/leave/leave-form',
meta: {
title: '请假申请',
hideInMenu: true,
// 不使用基础布局(仅在顶级生效)
noBasicLayout: true,
},
name: 'WorkflowLeaveInner',
path: '/workflow/leaveEdit/index/iframe',
},
];
/**
@@ -129,6 +131,18 @@ export const localMenuList: RouteRecordStringComponent[] = [
title: $t('demos.vben.document'),
},
},
{
name: 'V5UpdateLog',
path: '/changelog',
component: '/演示使用自行删除/changelog/index',
meta: {
icon: 'lucide:book-open-text',
keepAlive: true,
title: '更新记录',
badge: `当前: ${version}`,
badgeVariants: 'bg-primary',
},
},
],
},
{

View File

@@ -0,0 +1,18 @@
import type { RouteRecordRaw } from '@vben/types';
/**
* 该文件存放workflow表单的iframe内嵌路由
* 不需要权限认证 少走两个接口😅
*/
export const workflowIframeRoutes: RouteRecordRaw[] = [
// 这里是iframe使用的 去掉外层的BasicLayout
{
name: 'WorkflowLeaveInner',
path: '/workflow/leaveEdit/index/iframe',
component: () => import('#/views/workflow/leave/leave-form.vue'),
meta: {
hideInTab: true,
title: '请假申请',
},
},
];

View File

@@ -4,7 +4,8 @@ import type { UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
@@ -55,7 +56,9 @@ export const useAuthStore = defineStore('auth', () => {
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess ? await onSuccess?.() : await router.push(DEFAULT_HOME_PATH);
onSuccess
? await onSuccess?.()
: await router.push(preferences.app.defaultHomePath);
}
if (userInfo?.realName) {

View File

@@ -59,7 +59,11 @@ export const useDictStore = defineStore('app-dict', () => {
}
function resetCache() {
dictRequestCache.clear();
dictOptionsMap.clear();
/**
* 不需要清空dictRequestCache 每次请求成功/失败都清空key
*/
}
/**

View File

@@ -27,6 +27,10 @@ function fetchAndCacheDictData<T>(
// 内部处理了push的逻辑 这里不用push
setDictInfo(dictName, resp, formatNumber);
})
.catch(() => {
// 401时 移除字典缓存 下次登录重新获取
dictRequestCache.delete(dictName);
})
.finally(() => {
// 移除请求状态缓存
/**

View File

@@ -0,0 +1,126 @@
import type { ExtendedFormApi } from '@vben/common-ui';
import type { MaybePromise } from '@vben/types';
import { ref } from 'vue';
import { $t } from '@vben/locales';
import { Modal } from 'ant-design-vue';
import { isFunction } from 'lodash-es';
interface BeforeCloseDiffProps {
/**
* 初始化值如何获取
* @returns Promise<string>
*/
initializedGetter: () => MaybePromise<string>;
/**
* 当前值如何获取
* @returns Promise<string>
*/
currentGetter: () => MaybePromise<string>;
/**
* 自定义比较函数
* @param init 初始值
* @param current 当前值
* @returns boolean
*/
compare?: (init: string, current: string) => boolean;
}
/**
* 用于Drawer/Modal使用 判断表单是否有变动来决定是否弹窗提示
* @param props props
* @returns hook
*/
export function useBeforeCloseDiff(props: BeforeCloseDiffProps) {
const { initializedGetter, currentGetter, compare } = props;
/**
* 记录初始值 json
*/
const initialized = ref<string>('');
/**
* 是否已经初始化了 通过这个值判断是否需要进行对比 为false直接关闭 不弹窗
*/
const isInitialized = ref(false);
/**
* 标记是否已经完成初始化 后续需要进行对比
* @param data 自定义初始化数据 可选
*/
async function markInitialized(data?: string) {
initialized.value = data || (await initializedGetter());
isInitialized.value = true;
}
/**
* 重置初始化状态 需要在closed前调用 或者打开窗口时
*/
function resetInitialized() {
initialized.value = '';
isInitialized.value = false;
}
/**
* 提供给useVbenForm/useVbenDrawer使用
* @returns 是否允许关闭
*/
async function onBeforeClose(): Promise<boolean> {
// 如果还未初始化,直接允许关闭
if (!isInitialized.value) {
return true;
}
try {
// 获取当前表单数据
const current = await currentGetter();
// 自定义比较的情况
if (isFunction(compare) && compare(initialized.value, current)) {
return true;
} else {
// 如果数据没有变化,直接允许关闭
if (current === initialized.value) {
return true;
}
}
// 数据有变化,显示确认对话框
return new Promise<boolean>((resolve) => {
Modal.confirm({
title: $t('pages.common.tip'),
content: $t('pages.common.beforeCloseTip'),
centered: true,
okButtonProps: { danger: true },
cancelText: $t('common.cancel'),
okText: $t('common.confirm'),
onOk: () => {
resolve(true);
isInitialized.value = false;
},
onCancel: () => resolve(false),
});
});
} catch (error) {
console.error('Failed to compare data:', error);
return true;
}
}
return {
onBeforeClose,
markInitialized,
resetInitialized,
};
}
/**
* 给useVbenForm使用的 封装函数
* @param formApi 表单实例
* @returns getter
*/
export function defaultFormValueGetter(formApi: ExtendedFormApi) {
return async () => {
const v = await formApi.getValues();
return JSON.stringify(v);
};
}

View File

@@ -20,9 +20,9 @@ import {
MicromessengerIcon,
OperaIcon,
OSXIcon,
QQIcon,
QuarkIcon,
SafariIcon,
SvgQQIcon,
UcIcon,
WindowsIcon,
} from '@vben/icons';
@@ -116,7 +116,7 @@ export function renderHttpMethodTag(type: string) {
return <Tag color={color}>{title}</Tag>;
}
export function renderDictTag(value: string, dicts: DictData[]) {
export function renderDictTag(value: number | string, dicts: DictData[]) {
return <DictTag dicts={dicts} value={value}></DictTag>;
}
@@ -155,7 +155,7 @@ export function renderDictTags(
* @param dictName dictName
* @returns tag
*/
export function renderDict(value: string, dictName: string) {
export function renderDict(value: number | string, dictName: string) {
const dictInfo = getDictOptions(dictName);
return renderDictTag(value, dictInfo);
}
@@ -197,7 +197,7 @@ const browserOptions = [
{ icon: MicromessengerIcon, value: 'windowswechat' },
{ icon: QuarkIcon, value: 'quark' },
{ icon: MicromessengerIcon, value: 'wxwork' },
{ icon: QQIcon, value: 'qq' },
{ icon: SvgQQIcon, value: 'qq' },
{ icon: DingtalkIcon, value: 'dingtalk' },
{ icon: UcIcon, value: 'uc' },
{ icon: BaiduIcon, value: 'baidu' },

View File

@@ -6,6 +6,7 @@ import type { TenantResp } from '#/api';
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { DEFAULT_TENANT_ID } from '@vben/constants';
import { $t } from '@vben/locales';
import { Alert, message } from 'ant-design-vue';
@@ -17,7 +18,7 @@ import { useAuthStore } from '#/store';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const CODE_LENGTH = 4;
const tenantInfo = ref<TenantResp>({
tenantEnabled: false,
@@ -50,7 +51,7 @@ const formSchema = computed((): VbenFormSchema[] => {
})),
placeholder: $t('authentication.selectAccount'),
},
defaultValue: '000000',
defaultValue: DEFAULT_TENANT_ID,
dependencies: {
if: () => tenantInfo.value.tenantEnabled,
triggerFields: [''],
@@ -84,8 +85,8 @@ const formSchema = computed((): VbenFormSchema[] => {
: $t('authentication.sendCode');
return text;
},
// 验证码长度 在这设置
codeLength: 4,
// 验证码长度
codeLength: CODE_LENGTH,
placeholder: $t('authentication.code'),
handleSendCode: async () => {
const { valid, value } = await form.validateField('phoneNumber');

View File

@@ -7,6 +7,7 @@ import type { CaptchaResponse } from '#/api/core/captcha';
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui';
import { DEFAULT_TENANT_ID } from '@vben/constants';
import { $t } from '@vben/locales';
import { omit } from 'lodash-es';
@@ -15,6 +16,7 @@ import { tenantList } from '#/api';
import { captchaImage } from '#/api/core/captcha';
import { useAuthStore } from '#/store';
import { useLoginTenantId } from '../oauth-common';
import OAuthLogin from './oauth-login.vue';
defineOptions({ name: 'Login' });
@@ -28,13 +30,23 @@ const captchaInfo = ref<CaptchaResponse>({
img: '',
uuid: '',
});
// 验证码loading
const captchaLoading = ref(false);
async function loadCaptcha() {
const resp = await captchaImage();
if (resp.captchaEnabled) {
resp.img = `data:image/png;base64,${resp.img}`;
try {
captchaLoading.value = true;
const resp = await captchaImage();
if (resp.captchaEnabled) {
resp.img = `data:image/png;base64,${resp.img}`;
}
captchaInfo.value = resp;
} catch (error) {
console.error(error);
} finally {
captchaLoading.value = false;
}
captchaInfo.value = resp;
}
const tenantInfo = ref<TenantResp>({
@@ -56,6 +68,8 @@ onMounted(async () => {
await Promise.all([loadCaptcha(), loadTenant()]);
});
const { loginTenantId } = useLoginTenantId();
const formSchema = computed((): VbenFormSchema[] => {
return [
{
@@ -69,16 +83,13 @@ const formSchema = computed((): VbenFormSchema[] => {
})),
placeholder: $t('authentication.selectAccount'),
},
defaultValue: '000000',
defaultValue: DEFAULT_TENANT_ID,
dependencies: {
if: () => tenantInfo.value.tenantEnabled,
// 这里大致上是watch的一个效果
componentProps: (model) => {
localStorage.setItem(
'__oauth_tenant_id',
model?.tenantId ?? '000000',
);
return {};
// 可以把这里当做watch
trigger: (model) => {
// 给oauth登录使用
loginTenantId.value = model?.tenantId ?? DEFAULT_TENANT_ID;
},
triggerFields: ['', 'tenantId'],
},
@@ -115,6 +126,7 @@ const formSchema = computed((): VbenFormSchema[] => {
class: 'focus:border-primary',
onCaptchaClick: loadCaptcha,
placeholder: $t('authentication.code'),
loading: captchaLoading.value,
},
dependencies: {
if: () => captchaInfo.value.captchaEnabled,
@@ -122,7 +134,9 @@ const formSchema = computed((): VbenFormSchema[] => {
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
rules: z
.string()
.min(1, { message: $t('authentication.verifyRequiredTip') }),
},
];
});

View File

@@ -3,21 +3,16 @@ import { $t } from '@vben/locales';
import { Col, Row, Tooltip } from 'ant-design-vue';
import { accountBindList } from '../oauth-common';
import { accountBindList, handleAuthBinding } from '../oauth-common';
defineOptions({
name: 'OAuthLogin',
});
/**
* 有action方法才会显示
*/
const clientList = accountBindList.filter((item) => item.action);
</script>
<template>
<div class="w-full sm:mx-auto md:max-w-md">
<div class="mt-4 flex items-center justify-between">
<div class="my-4 flex items-center justify-between">
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
<span class="text-muted-foreground text-center text-xs uppercase">
{{ $t('authentication.thirdPartyLogin') }}
@@ -26,15 +21,20 @@ const clientList = accountBindList.filter((item) => item.action);
</div>
<Row class="enter-x flex items-center justify-evenly">
<!-- todo 这里在点击登录时要disabled -->
<Col v-for="item in clientList" :key="item.key" :span="4" class="my-2">
<Col
v-for="item in accountBindList"
:key="item.source"
:span="4"
class="my-2"
>
<Tooltip :title="`${item.title}登录`">
<span class="flex cursor-pointer items-center justify-center">
<component
:is="item.avatar"
v-if="item.avatar"
:style="{ color: item.color }"
:is="item.avatar"
:style="item?.style ?? {}"
class="size-[24px]"
@click="item.action"
@click="handleAuthBinding(item.source)"
/>
</span>
</Tooltip>

View File

@@ -1,104 +1,102 @@
import type { Component } from 'vue';
import type { Component, CSSProperties } from 'vue';
import { ref } from 'vue';
import { DEFAULT_TENANT_ID } from '@vben/constants';
import {
AlipayIcon,
DingdingIcon,
GiteeIcon,
GithubOAuthIcon,
TaobaoIcon,
SvgMaxKeyIcon,
SvgTopiamIcon,
SvgWechatIcon,
} from '@vben/icons';
import { createGlobalState } from '@vueuse/core';
import { authBinding } from '#/api/core/auth';
/**
* @description: 菜单
* @param key key
* @description: oauth登录
* @param title 标题
* @param description 描述
* @param extra 按钮文字
* @param avatar 图标
* @param color 图标颜色可直接写英文颜色/hex
*/
export interface ListItem {
key: string;
title: string;
description: string;
extra?: string;
avatar?: Component;
color?: string;
style?: CSSProperties;
}
/**
* @description: 绑定账号
* @param source 来源 如gitee github 与后端的social-callback?source=xxx对应
* @param bound 是否已经绑定
* @param action 账号绑定回调
*/
export interface BindItem extends ListItem {
source: string;
bound?: boolean;
action?: (source: string) => Promise<any>;
}
/**
* todo tenantId
* 绑定授权从userStore.userInfo获取
* 登录从localStorage获取
* 这里存储登录页的tenantId 由于个人中心也会用到 需要共享
* 所以使用`createGlobalState`
* @see https://vueuse.org/shared/createGlobalState/
*/
export const useLoginTenantId = createGlobalState(() => {
const loginTenantId = ref(DEFAULT_TENANT_ID);
return {
loginTenantId,
};
});
/**
* 绑定授权
* @param source
*/
async function handleAuthBinding(source: string) {
const tenantId = localStorage.getItem('__oauth_tenant_id') ?? '000000';
export async function handleAuthBinding(source: string) {
const { loginTenantId } = useLoginTenantId();
// 这里返回打开授权页面的链接
const href = await authBinding(source, tenantId);
const href = await authBinding(source, loginTenantId.value);
window.location.href = href;
}
/**
* 账号绑定 list
* 添加账号绑定只需要在这里增加即可
* 添加过的项目会在个人主页-绑定账号中显示
* action不为空的会在登录页显示
*/
export const accountBindList: BindItem[] = [
{
avatar: TaobaoIcon,
color: '#ff4000',
description: '绑定淘宝账号',
key: '1',
source: 'taobao',
title: '淘宝',
},
{
avatar: AlipayIcon,
color: '#2eabff',
description: '绑定支付宝账号',
key: '2',
source: 'alipay',
title: '支付宝',
},
{
avatar: DingdingIcon,
color: '#2eabff',
description: '绑定钉钉账号',
key: '3',
source: 'ding',
title: '钉钉',
},
{
action: () => handleAuthBinding('gitee'),
avatar: GiteeIcon,
color: '#c71d23',
description: '绑定GITEE账号',
key: '4',
description: '绑定Gitee账号',
source: 'gitee',
title: 'GITEE',
title: 'Gitee',
style: { color: '#c71d23' },
},
{
action: () => handleAuthBinding('github'),
avatar: GithubOAuthIcon,
color: '',
description: '绑定GITHUB账号',
key: '5',
description: '绑定Github账号',
source: 'github',
title: 'GITHUB',
title: 'Github',
},
{
avatar: SvgMaxKeyIcon,
description: '绑定MaxKey账号',
source: 'maxkey',
title: 'MaxKey',
},
{
avatar: SvgTopiamIcon,
description: '绑定topiam账号',
source: 'topiam',
title: 'Topiam',
},
{
avatar: SvgWechatIcon,
description: '绑定wechat账号',
source: 'wechat',
title: 'Wechat',
},
];

View File

@@ -7,27 +7,12 @@ import { computed, ref, unref } from 'vue';
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
import {
Alert,
Avatar,
Card,
List,
ListItem,
message,
Modal,
} from 'ant-design-vue';
import { Alert, Avatar, Card, List, ListItem, Modal } from 'ant-design-vue';
import { authUnbinding } from '#/api';
import { socialList } from '#/api/system/social';
import { accountBindList } from '../../oauth-common';
/**
* 没有传递action事件则不支持绑定 弹出默认提示
*/
function defaultTip(title: string) {
message.info({ content: `暂不支持绑定${title}` });
}
import { accountBindList, handleAuthBinding } from '../../oauth-common';
function buttonText(item: BindItem) {
return item.bound ? '已绑定' : '绑定';
@@ -46,12 +31,6 @@ const bindList = computed<BindItem[]>(() => {
});
const gridOptions: VxeGridProps = {
checkboxConfig: {
// 高亮
highlight: true,
// 翻页时保留选中状态
reserve: true,
},
columns: [
{
field: 'source',
@@ -147,14 +126,12 @@ function handleUnbind(record: Record<string, any>) {
<ListItem>
<Card>
<div class="flex w-full items-center gap-4">
<div>
<component
:is="item.avatar"
v-if="item.avatar"
:style="{ color: item.color }"
class="size-[40px]"
/>
</div>
<component
:is="item.avatar"
v-if="item.avatar"
:style="item?.style ?? {}"
class="size-[40px]"
/>
<div class="flex flex-1 items-center justify-between">
<div class="flex flex-col">
<h4
@@ -170,9 +147,7 @@ function handleUnbind(record: Record<string, any>) {
:disabled="item.bound"
size="small"
type="link"
@click="
item.action ? item.action() : defaultTip(item.title)
"
@click="handleAuthBinding(item.source)"
>
{{ buttonText(item) }}
</a-button>
@@ -191,10 +166,6 @@ function handleUnbind(record: Record<string, any>) {
</span>
中accountBindList按模板添加
</p>
<p>
添加对应模板后会在此处显示绑定, 但只有
<span class="font-bold">实现了action才能在登录页显示</span>
</p>
</template>
</Alert>
</div>

View File

@@ -24,7 +24,7 @@ defineEmits<{
}>();
const avatar = computed(
() => props.profile?.user.avatar ?? preferences.app.defaultAvatar,
() => props.profile?.user.avatar || preferences.app.defaultAvatar,
);
const { isDark } = usePreferences();

View File

@@ -1,6 +1,4 @@
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { TabPane, Tabs } from 'ant-design-vue';
import AccountBind from './components/account-bind.vue';
@@ -8,52 +6,34 @@ import BaseSetting from './components/base-setting.vue';
import OnlineDevice from './components/online-device.vue';
import SecureSetting from './components/secure-setting.vue';
export default defineComponent({
components: {
AccountBind,
BaseSetting,
OnlineDevice,
SecureSetting,
TabPane,
Tabs,
const settingList = [
{
component: BaseSetting,
key: '1',
name: '基本设置',
},
setup() {
const settingList = [
{
component: 'BaseSetting',
key: '1',
name: '基本设置',
},
{
component: 'SecureSetting',
key: '2',
name: '安全设置',
},
{
component: 'AccountBind',
key: '3',
name: '账号绑定',
},
{
component: 'OnlineDevice',
key: '4',
name: '在线设备',
},
];
return {
settingList,
};
{
component: SecureSetting,
key: '2',
name: '安全设置',
},
});
{
component: AccountBind,
key: '3',
name: '账号绑定',
},
{
component: OnlineDevice,
key: '4',
name: '在线设备',
},
];
</script>
<template>
<Tabs class="bg-background rounded-[var(--radius)] px-[16px] lg:flex-1">
<template v-for="item in settingList" :key="item.key">
<TabPane :tab="item.name">
<component :is="item.component" v-bind="$attrs" />
</TabPane>
</template>
<TabPane v-for="item in settingList" :key="item.key" :tab="item.name">
<component :is="item.component" v-bind="$attrs" />
</TabPane>
</Tabs>
</template>

View File

@@ -4,7 +4,8 @@ import type { AuthApi } from '#/api';
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH } from '@vben/constants';
import { DEFAULT_TENANT_ID } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
@@ -22,7 +23,7 @@ const stateJson = JSON.parse(atob(state));
// 来源
const source = route.query.source as string;
// 租户ID
const defaultTenantId = '000000';
const defaultTenantId = DEFAULT_TENANT_ID;
const tenantId = (stateJson.tenantId as string) ?? defaultTenantId;
const domain = stateJson.domain as string;
@@ -44,7 +45,7 @@ onMounted(async () => {
try {
// 已经实现的平台
const currentClient = accountBindList.find(
(item) => item.source === source && item.action,
(item) => item.source === source,
);
if (!currentClient) {
message.error({ content: `未找到${source}平台` });
@@ -70,7 +71,7 @@ onMounted(async () => {
// 500 你还没有绑定第三方账号,绑定后才可以登录!
} finally {
setTimeout(() => {
router.push(DEFAULT_HOME_PATH);
router.push(preferences.app.defaultHomePath);
}, 1500);
}
});

View File

@@ -15,10 +15,10 @@ import {
} from '@vben/icons';
import AnalyticsTrends from './analytics-trends.vue';
import AnalyticsVisits from './analytics-visits.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue';
import AnalyticsVisits from './analytics-visits.vue';
const overviewItems: AnalysisOverviewItem[] = [
{

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