117 Commits

Author SHA1 Message Date
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
193 changed files with 6439 additions and 540 deletions

View File

@@ -1,3 +1,18 @@
# 1.2.2
**FEATURES**
- 代码生成支持路径方式生成
- 代码生成 支持选择表单生成类型(需要模板支持)
- 工作流 支持按钮权限
# 1.2.1
# BUG FIXES
- 客户端管理 错误的status disabled
- modal/drawer升级后zIndex(2000)会遮挡Tinymce的下拉框zIndex(1300)
# 1.2.0
**REFACTOR**

View File

@@ -6,12 +6,18 @@
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
目前对应后端版本: **分布式5.3.0/微服务2.2.3**
目前对应后端版本: **分布式5.3.0/微服务2.2.2**
V1.1.0版本已支持离线图标
V1.2.0版本对接warmflow工作流
`微服务最新版暂不支持warmflow工作流`
`微服务最新版暂不支持warmflow工作流`
`微服务最新版暂不支持warmflow工作流`
## 简介
基于 [vben5 & ant-design-vue](https://github.com/vbenjs/vue-vben-admin) 的 RuoYi-Vue-Plus 前端项目
@@ -46,6 +52,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)

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

@@ -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

@@ -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

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "1.2.0",
"version": "1.2.2",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -3,11 +3,12 @@
* 可用于 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 { defineComponent, getCurrentInstance, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -44,10 +45,30 @@ const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
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({
inheritAttrs: false,
name: component.name,
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, { ...props, ...attrs, placeholder, ref: innerRef }, slots);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
@@ -131,7 +152,13 @@ async function initComponentAdapter() {
IconPicker: (props, { attrs, slots }) => {
return h(
IconPicker,
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
{
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
...props,
...attrs,
},
slots,
);
},

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

View File

@@ -105,11 +105,7 @@ function filterOption(input: string, option: TenantOption) {
show-search
@deselect="onDeselect"
@select="onSelected"
>
<template #suffixIcon>
<span class="icon-mdi--company"></span>
</template>
</Select>
/>
</div>
</template>

View File

@@ -345,7 +345,7 @@ function handleDone(name: string, url: string) {
v-if="!initOptions.inline && init"
v-model="modelValue"
:init="initOptions"
:style="{ visibility: 'hidden' }"
:style="{ visibility: 'hidden', zIndex: 3000 }"
:tinymce-script-src="tinymceScriptSrc"
license-key="gpl"
/>
@@ -353,6 +353,17 @@ function handleDone(name: string, url: string) {
</div>
</template>
<style lang="scss">
/***
由于modal/drawer的zIndex升级后为2000
这里会造成遮挡 修改为更高的zIndex
*/
.tox.tox-silver-sink.tox-tinymce-aux {
/** 该样式默认为1300的zIndex */
z-index: 2025;
}
</style>
<style lang="scss" scoped>
/**
隐藏右上角upgrade按钮

View File

@@ -230,10 +230,10 @@ function getValue() {
</div>
</template>
<style lang="less">
<style>
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {

View File

@@ -313,10 +313,10 @@ function getValue() {
</div>
</template>
<style lang="less">
<style>
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {

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() {

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

@@ -60,6 +60,9 @@ const localRoutes: RouteRecordStringComponent[] = [
name: 'WorkflowDesigner',
path: '/workflow/designer',
},
/**
* 需要添加iframe路由 同目录的./workflow-iframe.ts
*/
{
component: 'workflow/leave/leave-form',
meta: {
@@ -71,18 +74,6 @@ const localRoutes: RouteRecordStringComponent[] = [
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',
},
];
/**

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

@@ -122,7 +122,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

@@ -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

@@ -55,6 +55,14 @@ function setupForm(update: boolean) {
]);
}
// 提取生成状态字段Schema的函数
const getStatusSchema = (disabled: boolean) => [
{
componentProps: { disabled },
fieldName: 'status',
},
];
const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel,
onConfirm: handleConfirm,
@@ -70,15 +78,11 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
if (isUpdate.value && id) {
const record = await clientInfo(id);
// 不能禁用id为1的记录
formApi.updateSchema([
{
componentProps: {
disabled: record.id === 1,
},
fieldName: 'status',
},
]);
formApi.updateSchema(getStatusSchema(record.id === 1));
await formApi.setValues(record);
} else {
// 新增模式: 确保状态字段可用
formApi.updateSchema(getStatusSchema(false));
}
drawerApi.drawerLoading(false);
},

View File

@@ -99,7 +99,7 @@ export const modalSchema: FormSchemaGetter = () => [
},
{
component: 'Textarea',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
fieldName: 'configValue',
label: '参数键值',
componentProps: {
@@ -122,7 +122,7 @@ export const modalSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -89,7 +89,7 @@ export const drawerSchema: FormSchemaGetter = () => [
placeholder: '可使用tailwind类名 如bg-blue w-full h-full等',
},
fieldName: 'cssClass',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
help: '标签的css样式, 可添加已经编译的css类名',
label: 'css类名',
},
@@ -102,7 +102,7 @@ export const drawerSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -71,7 +71,7 @@ export const modalSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -1,14 +1,23 @@
<!--
2025年03月08日重构为原生表单(反向重构??)
该文件作为例子 使用原生表单而非useVbenForm
-->
<script setup lang="ts">
import type { RuleObject } from 'ant-design-vue/es/form';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DictEnum } from '@vben/constants';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { noticeAdd, noticeInfo, noticeUpdate } from '#/api/system/notice';
import { Form, FormItem, Input, RadioGroup } from 'ant-design-vue';
import { pick } from 'lodash-es';
import { modalSchema } from './data';
import { noticeAdd, noticeInfo, noticeUpdate } from '#/api/system/notice';
import { Tinymce } from '#/components/tinymce';
import { getDictOptions } from '#/utils/dict';
const emit = defineEmits<{ reload: [] }>();
@@ -17,20 +26,59 @@ const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
const [BasicForm, formApi] = useVbenForm({
layout: 'vertical',
commonConfig: {
formItemClass: 'col-span-2',
labelWidth: 100,
},
schema: modalSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
/**
* 定义表单数据类型
*/
interface FormData {
noticeId?: number;
noticeTitle?: string;
status?: string;
noticeType?: string;
noticeContent?: string;
}
/**
* 定义默认值 用于reset
*/
const defaultValues: FormData = {
noticeId: undefined,
noticeTitle: '',
status: '0',
noticeType: '1',
noticeContent: '',
};
/**
* 表单数据ref
*/
const formData = ref(defaultValues);
type AntdFormRules<T> = Partial<Record<keyof T, RuleObject[]>> & {
[key: string]: RuleObject[];
};
/**
* 表单校验规则
*/
const formRules = ref<AntdFormRules<FormData>>({
status: [{ required: true, message: $t('ui.formRules.selectRequired') }],
noticeContent: [{ required: true, message: $t('ui.formRules.required') }],
noticeType: [{ required: true, message: $t('ui.formRules.selectRequired') }],
noticeTitle: [{ required: true, message: $t('ui.formRules.required') }],
});
/**
* useForm解构出表单方法
*/
const { validate, validateInfos, resetFields } = Form.useForm(
formData,
formRules,
);
const [BasicModal, modalApi] = useVbenModal({
class: 'w-[800px]',
fullscreenButton: false,
onCancel: handleCancel,
closeOnClickModal: false,
onClosed: handleCancel,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
@@ -41,7 +89,9 @@ const [BasicModal, modalApi] = useVbenModal({
isUpdate.value = !!id;
if (isUpdate.value && id) {
const record = await noticeInfo(id);
await formApi.setValues(record);
// 只赋值存在的字段
const filterRecord = pick(record, Object.keys(defaultValues));
formData.value = filterRecord;
}
modalApi.modalLoading(false);
},
@@ -50,11 +100,9 @@ const [BasicModal, modalApi] = useVbenModal({
async function handleConfirm() {
try {
modalApi.modalLoading(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const data = cloneDeep(await formApi.getValues());
await validate();
// 可能会做数据处理 使用cloneDeep深拷贝
const data = cloneDeep(formData.value);
await (isUpdate.value ? noticeUpdate(data) : noticeAdd(data));
emit('reload');
await handleCancel();
@@ -67,12 +115,41 @@ async function handleConfirm() {
async function handleCancel() {
modalApi.close();
await formApi.resetForm();
formData.value = defaultValues;
resetFields();
}
</script>
<template>
<BasicModal :fullscreen-button="true" :title="title" class="w-[800px]">
<BasicForm />
<BasicModal :title="title">
<Form layout="vertical">
<FormItem label="公告标题" v-bind="validateInfos.noticeTitle">
<Input
:placeholder="$t('ui.formRules.required')"
v-model:value="formData.noticeTitle"
/>
</FormItem>
<div class="grid sm:grid-cols-1 lg:grid-cols-2">
<FormItem label="公告状态" v-bind="validateInfos.status">
<RadioGroup
button-style="solid"
option-type="button"
v-model:value="formData.status"
:options="getDictOptions(DictEnum.SYS_NOTICE_STATUS)"
/>
</FormItem>
<FormItem label="公告类型" v-bind="validateInfos.noticeType">
<RadioGroup
button-style="solid"
option-type="button"
v-model:value="formData.noticeType"
:options="getDictOptions(DictEnum.SYS_NOTICE_TYPE)"
/>
</FormItem>
</div>
<FormItem label="公告内容" v-bind="validateInfos.noticeContent">
<Tinymce v-model="formData.noticeContent" />
</FormItem>
</Form>
</BasicModal>
</template>

View File

@@ -213,7 +213,7 @@ export const drawerSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -126,7 +126,7 @@ export const drawerSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -219,7 +219,7 @@ export const authModalSchemas: FormSchemaGetter = () => [
triggerFields: ['dataScope'],
},
fieldName: 'deptIds',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
help: '更改后立即生效',
label: '部门权限',
},

View File

@@ -255,13 +255,13 @@ export const drawerSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'intro',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '企业介绍',
},
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -65,7 +65,7 @@ export const drawerSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -201,7 +201,7 @@ export const drawerSchema: FormSchemaGetter = () => [
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
label: '备注',
},
];

View File

@@ -82,6 +82,7 @@ async function handleSave() {
...requestData.params,
parentMenuId: requestData.parentMenuId,
popupComponent: requestData.popupComponent,
formComponent: requestData.formComponent,
};
}
// 保存

View File

@@ -109,9 +109,12 @@ onMounted(async () => {
await formApi.setValues(info);
// 弹出框类型需要手动赋值
if (info.options) {
const popupComponent = JSON.parse(info.options)?.popupComponent;
const { popupComponent, formComponent } = JSON.parse(info.options);
if (popupComponent) {
await formApi.setFieldValue('popupComponent', popupComponent);
formApi.setFieldValue('popupComponent', popupComponent);
}
if (formComponent) {
formApi.setFieldValue('formComponent', formComponent);
}
}
await Promise.all([initTreeSelect(info.columns), initMenuSelect()]);

View File

@@ -157,6 +157,21 @@ export const formSchema: FormSchemaGetter = () => [
fieldName: 'popupComponent',
label: '弹窗组件类型',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: 'useVbenForm', value: 'useForm' },
{ label: 'antd原生表单', value: 'native' },
],
optionType: 'button',
},
help: '自定义功能, 需要后端支持\n复杂(布局, 联动等)表单建议用antd原生表单',
defaultValue: 'useForm',
fieldName: 'formComponent',
label: '生成表单类型',
},
{
component: 'RadioGroup',
componentProps: {

View File

@@ -18,6 +18,7 @@ import {
batchGenCode,
generatedList,
genRemove,
genWithPath,
getDataSourceNames,
syncDb,
} from '#/api/tool/gen';
@@ -139,8 +140,15 @@ async function handleBatchGen() {
}
async function handleDownload(record: Recordable<any>) {
const hideLoading = message.loading('载中...');
const hideLoading = message.loading('载中...');
try {
// 路径生成
if (record.genType === '1' && record.genPath) {
await genWithPath(record.tableId);
message.success(`生成成功: ${record.genPath}`);
return;
}
// zip生成
const blob = await batchGenCode(record.tableId);
const filename = `代码生成_${record.tableName}_${dayjs().valueOf()}.zip`;
downloadByData(blob, filename);

View File

@@ -8,7 +8,7 @@ import { useVbenModal } from '@vben/common-ui';
import { cloneDeep } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { completeTask } from '#/api/workflow/task';
import { completeTask, getTaskByTaskId } from '#/api/workflow/task';
import { CopyComponent } from '.';
@@ -31,6 +31,31 @@ const [BasicModal, modalApi] = useVbenModal({
title: '流程发起',
fullscreenButton: false,
onConfirm: handleSubmit,
async onOpenChange(isOpen) {
if (!isOpen) {
return null;
}
const { taskId } = modalApi.getData() as ModalProps;
// 查询是否有按钮权限
const resp = await getTaskByTaskId(taskId);
const buttonPermissions: Record<string, boolean> = {};
resp.buttonList.forEach((item) => {
buttonPermissions[item.code] = item.show;
});
// 是否具有抄送权限
const copyPermission = buttonPermissions?.copy ?? false;
formApi.updateSchema([
{
fieldName: 'flowCopyList',
dependencies: {
if: copyPermission,
triggerFields: [''],
},
},
]);
},
});
const [BasicForm, formApi] = useVbenForm({

View File

@@ -0,0 +1,41 @@
<!--
审批详情
约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加
apps/web-antd/src/router/routes/workflow-iframe.ts
-->
<script setup lang="ts">
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
import type { TaskInfo } from '#/api/workflow/task/model';
import { Divider, Skeleton } from 'ant-design-vue';
import { ApprovalTimeline } from '.';
defineOptions({
name: 'ApprovalDetails',
inheritAttrs: false,
});
defineProps<{
currentFlowInfo: FlowInfoResponse;
iframeHeight: number;
iframeLoaded: boolean;
task: TaskInfo;
}>();
</script>
<template>
<div>
<!-- 约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加 -->
<iframe
v-show="iframeLoaded"
:src="`${task.formPath}/iframe?readonly=true&id=${task.businessId}`"
:style="{ height: `${iframeHeight}px` }"
class="w-full"
></iframe>
<Skeleton v-show="!iframeLoaded" :paragraph="{ rows: 6 }" active />
<Divider />
<ApprovalTimeline :list="currentFlowInfo.list" />
</div>
</template>

View File

@@ -1,14 +1,21 @@
<!-- 审批同意的弹窗 -->
<script setup lang="ts">
import type { CompleteTaskReqData } from '#/api/workflow/task/model';
import type { User } from '#/api/system/user/model';
import type {
CompleteTaskReqData,
NextNodeInfo,
} from '#/api/workflow/task/model';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { cloneDeep } from '@vben/utils';
import { message } from 'ant-design-vue';
import { omit } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { completeTask } from '#/api/workflow/task';
import { completeTask, getNextNodeList } from '#/api/workflow/task';
import { CopyComponent } from '.';
@@ -77,11 +84,16 @@ const [BasicForm, formApi] = useVbenForm({
defaultValue: [],
label: '抄送人',
},
{
fieldName: 'assigneeMap',
component: 'Input',
label: '下一步审批人',
},
{
fieldName: 'message',
component: 'Textarea',
label: '审批意见',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
},
],
showDefaultActions: false,
@@ -90,8 +102,14 @@ const [BasicForm, formApi] = useVbenForm({
interface ModalProps {
taskId: string;
// 是否具有抄送权限
copyPermission: boolean;
// 是有具有选人权限
assignPermission: boolean;
}
// 自定义添加选人属性 给组件v-for绑定
const nextNodeInfo = ref<(NextNodeInfo & { selectUserList: User[] })[]>([]);
const [BasicModal, modalApi] = useVbenModal({
title: '审批通过',
fullscreenButton: false,
@@ -104,7 +122,36 @@ const [BasicModal, modalApi] = useVbenModal({
}
modalApi.modalLoading(true);
const { taskId } = modalApi.getData() as ModalProps;
const { taskId, copyPermission, assignPermission } =
modalApi.getData() as ModalProps;
// 是否显示抄送选择
formApi.updateSchema([
{
fieldName: 'flowCopyList',
dependencies: {
if: copyPermission,
triggerFields: [''],
},
},
{
fieldName: 'assigneeMap',
dependencies: {
if: assignPermission,
triggerFields: [''],
},
},
]);
// 获取下一节点名称
if (assignPermission) {
const resp = await getNextNodeList({ taskId });
nextNodeInfo.value = resp.map((item) => ({
...item,
// 用于给组件绑定
selectUserList: [],
}));
}
await formApi.setFieldValue('taskId', taskId);
modalApi.modalLoading(false);
@@ -131,6 +178,26 @@ async function handleSubmit() {
variables: {},
flowCopyList,
} as CompleteTaskReqData;
// 选人
if (modalApi.getData()?.assignPermission) {
// 判断是否选中
for (const item of nextNodeInfo.value) {
if (item.selectUserList.length === 0) {
message.warn(`未选择节点[${item.nodeName}]审批人`);
return;
}
}
const assigneeMap: { [key: string]: string } = {};
nextNodeInfo.value.forEach((item) => {
assigneeMap[item.nodeCode] = item.selectUserList
.map((u) => u.userId)
.join(',');
});
requestData.assigneeMap = assigneeMap;
}
await completeTask(requestData);
modalApi.close();
emit('complete');
@@ -148,6 +215,24 @@ async function handleSubmit() {
<template #flowCopyList="slotProps">
<CopyComponent v-model:user-list="slotProps.modelValue" />
</template>
<template #assigneeMap>
<div
v-for="item in nextNodeInfo"
:key="item.nodeCode"
class="flex items-center gap-2"
>
<template v-if="item.permissionFlag">
<span class="opacity-70">{{ item.nodeName }}</span>
<CopyComponent
:allow-user-ids="item.permissionFlag"
v-model:user-list="item.selectUserList"
/>
</template>
<template v-else>
<span class="text-red-500">没有权限, 请联系管理员</span>
</template>
</div>
</template>
</BasicForm>
</BasicModal>
</template>

View File

@@ -1,3 +1,4 @@
<!-- 该文件需要重构 但我没空 -->
<script setup lang="ts">
import type { User } from '#/api/core/user';
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
@@ -20,7 +21,6 @@ import {
MenuItem,
message,
Modal,
Skeleton,
Space,
TabPane,
Tabs,
@@ -40,12 +40,8 @@ import {
} from '#/api/workflow/task';
import { renderDict } from '#/utils/render';
import {
approvalModal,
approvalRejectionModal,
ApprovalTimeline,
flowInterfereModal,
} from '.';
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
import ApprovalDetails from './approval-details.vue';
import { approveWithReasonModal } from './helper';
import userSelectModal from './user-select-modal.vue';
@@ -76,6 +72,28 @@ const showMultiActions = computed(() => {
return false;
});
/**
* 按钮权限
*/
const buttonPermissions = computed(() => {
const record: Record<string, boolean> = {};
if (!currentTask.value) {
return record;
}
currentTask.value.buttonList.forEach((item) => {
record[item.code] = item.show;
});
return record;
});
// 是否显示 `其他` 按钮
const showButtonOther = computed(() => {
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
return Object.keys(buttonPermissions.value).some(
(key) => moreCollections.has(key) && buttonPermissions.value[key],
);
});
/**
* myself 我发起的
* readonly 只读 只用于查看
@@ -227,7 +245,15 @@ const [ApprovalModal, approvalModalApi] = useVbenModal({
connectedComponent: approvalModal,
});
function handleApproval() {
approvalModalApi.setData({ taskId: props.task?.id });
// 是否具有抄送权限
const copyPermission = buttonPermissions.value?.copy ?? false;
// 是否具有选人权限
const assignPermission = buttonPermissions.value?.pop ?? false;
approvalModalApi.setData({
taskId: props.task?.id,
copyPermission,
assignPermission,
});
approvalModalApi.open();
}
@@ -408,18 +434,12 @@ async function handleCopy(text: string) {
</div>
<Tabs v-if="currentFlowInfo" class="flex-1">
<TabPane key="1" tab="审批详情">
<div class="h-fulloverflow-y-auto">
<!-- 约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加 -->
<iframe
v-show="iframeLoaded"
:src="`${task.formPath}/iframe?readonly=true&id=${task.businessId}`"
:style="{ height: `${iframeHeight}px` }"
class="w-full"
></iframe>
<Skeleton v-show="!iframeLoaded" :paragraph="{ rows: 6 }" active />
<Divider />
<ApprovalTimeline :list="currentFlowInfo.list" />
</div>
<ApprovalDetails
:current-flow-info="currentFlowInfo"
:iframe-loaded="iframeLoaded"
:iframe-height="iframeHeight"
:task="task"
/>
</TabPane>
<TabPane key="2" tab="审批流程图">
<img
@@ -459,10 +479,20 @@ async function handleCopy(text: string) {
</Space>
<Space v-if="type === 'approve'">
<a-button type="primary" @click="handleApproval">通过</a-button>
<a-button danger type="primary" @click="handleTermination">
<a-button
v-if="buttonPermissions?.termination"
danger
type="primary"
@click="handleTermination"
>
终止
</a-button>
<a-button danger type="primary" @click="handleRejection">
<a-button
v-if="buttonPermissions?.back"
danger
type="primary"
@click="handleRejection"
>
驳回
</a-button>
<Dropdown
@@ -471,21 +501,29 @@ async function handleCopy(text: string) {
>
<template #overlay>
<Menu>
<MenuItem key="1" @click="() => delegationModalApi.open()">
<MenuItem
v-if="buttonPermissions?.trust"
key="1"
@click="() => delegationModalApi.open()"
>
委托
</MenuItem>
<MenuItem key="2" @click="() => transferModalApi.open()">
<MenuItem
v-if="buttonPermissions?.transfer"
key="2"
@click="() => transferModalApi.open()"
>
转办
</MenuItem>
<MenuItem
v-if="showMultiActions"
v-if="showMultiActions && buttonPermissions?.addSign"
key="3"
@click="() => addSignatureModalApi.open()"
>
加签
</MenuItem>
<MenuItem
v-if="showMultiActions"
v-if="showMultiActions && buttonPermissions?.subSign"
key="4"
@click="() => reductionSignatureModalApi.open()"
>
@@ -493,7 +531,7 @@ async function handleCopy(text: string) {
</MenuItem>
</Menu>
</template>
<a-button> 其他 </a-button>
<a-button v-if="showButtonOther"> 其他 </a-button>
</Dropdown>
<ApprovalModal @complete="$emit('reload')" />
<RejectionModal @complete="$emit('reload')" />

View File

@@ -77,7 +77,7 @@ const [BasicForm, formApi] = useVbenForm({
fieldName: 'message',
component: 'Textarea',
label: '审批意见',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
},
],
showDefaultActions: false,

View File

@@ -13,7 +13,6 @@ import { renderDict } from '#/utils/render';
defineOptions({
name: 'ApprovalTimelineItem',
inheritAttrs: false,
});
const props = defineProps<{ item: Flow }>();
@@ -42,7 +41,7 @@ onMounted(async () => {
</script>
<template>
<TimelineItem :key="item.id">
<TimelineItem>
<template #dot>
<div class="relative rounded-full border">
<VbenAvatar

View File

@@ -13,8 +13,8 @@ const props = defineProps<{
<template>
<Timeline v-if="props.list.length > 0">
<ApprovalTimelineItem
v-for="(item, index) in props.list"
:key="index"
v-for="item in props.list"
:key="item.id"
:item="item"
/>
</Timeline>

View File

@@ -1,8 +1,10 @@
<!--抄送组件-->
<script setup lang="ts">
import type { PropType } from 'vue';
import type { User } from '#/api/system/user/model';
import { computed, type PropType } from 'vue';
import { computed } from 'vue';
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
@@ -15,12 +17,19 @@ defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<{ ellipseNumber?: number }>(), {
/**
* 最大显示的头像数量 超过显示为省略号头像
*/
ellipseNumber: 3,
});
const props = withDefaults(
defineProps<{ allowUserIds?: string; ellipseNumber?: number }>(),
{
/**
* 最大显示的头像数量 超过显示为省略号头像
*/
ellipseNumber: 3,
/**
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
*/
allowUserIds: '',
},
);
const emit = defineEmits<{ cancel: []; finish: [User[]] }>();
@@ -80,6 +89,10 @@ const displayedList = computed(() => {
</Tooltip>
</AvatarGroup>
<a-button size="small" @click="handleOpen">选择人员</a-button>
<UserSelectModal @cancel="$emit('cancel')" @finish="handleFinish" />
<UserSelectModal
:allow-user-ids="allowUserIds"
@cancel="$emit('cancel')"
@finish="handleFinish"
/>
</div>
</template>

View File

@@ -18,9 +18,16 @@ defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<{ mode?: 'multiple' | 'single' }>(), {
mode: 'multiple',
});
const props = withDefaults(
defineProps<{ allowUserIds?: string; mode?: 'multiple' | 'single' }>(),
{
mode: 'multiple',
/**
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
*/
allowUserIds: '',
},
);
const emit = defineEmits<{
/**
@@ -136,11 +143,17 @@ const gridOptions: VxeGridProps = {
}
}
return await userList({
const params: any = {
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
};
// 添加参数
if (props.allowUserIds) {
params.userIds = props.allowUserIds;
}
return await userList(params);
},
},
},

View File

@@ -24,6 +24,7 @@ export const leaveFlowOptions = [
{ label: '请假流程-并行网关', value: 'leave3' },
{ label: '请假流程-会签', value: 'leave4' },
{ label: '请假申请-并行会签网关', value: 'leave5' },
{ label: '请假申请-排他并行网关', value: 'leave6' },
];
export const querySchema: FormSchemaGetter = () => [
@@ -168,6 +169,6 @@ export const modalSchema: (isEdit: boolean) => VbenFormSchema[] = (
label: '请假原因',
fieldName: 'remark',
component: 'Textarea',
formItemClass: 'items-baseline',
formItemClass: 'items-start',
},
];

View File

@@ -182,13 +182,6 @@ const cardSize = computed(() => {
<style lang="scss">
html:has(#leave-form) {
/**
去除 '菜单加载中' 主要是iframe内嵌使用
*/
.ant-message-notice-content:has(.ant-message-loading) {
display: none;
}
/**
去除顶部进度条样式
*/

View File

@@ -27,7 +27,7 @@ import { ApprovalCard, ApprovalPanel } from '../components';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
const taskTotal = ref(0);
const page = ref(1);
const loading = ref(false);

View File

@@ -29,7 +29,7 @@ import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
const taskTotal = ref(0);
const page = ref(1);
const loading = ref(false);

View File

@@ -29,7 +29,7 @@ import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
const taskTotal = ref(0);
const page = ref(1);
const loading = ref(false);

View File

@@ -30,7 +30,7 @@ import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
const taskTotal = ref(0);
const page = ref(1);
const loading = ref(false);

View File

@@ -3,7 +3,7 @@
社区交流群主要是为了方便大家交流,提问,解答问题,分享经验等。偏自助方式,如果你有问题,可以通过以下方式加入社区交流群:
- [QQ频道](https://pd.qq.com/s/16p8lvvob):推荐!!!主要提供问题解答,分享经验等。
- QQ群[大群](https://qm.qq.com/q/MEmHoCLbG0)[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E)[4群](https://qm.qq.com/q/sCzSlm3504),主要使用者交流群。
- QQ群[大群](https://qm.qq.com/q/MEmHoCLbG0)[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E)[4群](https://qm.qq.com/q/sCzSlm3504)[5群](https://qm.qq.com/q/ya9XrtbS6s)主要使用者交流群。
- [Discord](https://discord.com/invite/VU62jTecad): 主要提供问题解答,分享经验等。
::: tip

View File

@@ -42,11 +42,18 @@ outline: deep
| transition | 动画效果 | `string` | `linear` |
| decimals | 保留小数点位数 | `number` | `0` |
### Events
| 事件名 | 描述 | 类型 |
| -------------- | -------------- | -------------- |
| started | 动画已开始 | `()=>void` |
| finished | 动画已结束 | `()=>void` |
| ~~onStarted~~ | ~~动画已开始~~ | ~~`()=>void`~~ |
| ~~onFinished~~ | ~~动画已结束~~ | ~~`()=>void`~~ |
### Methods
以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。
| 事件名 | 描述 | 类型 |
| 方法名 | 描述 | 类型 |
| ------ | ------------ | ---------- |
| start | 开始执行动画 | `()=>void` |
| reset | 重置 | `()=>void` |

View File

@@ -137,11 +137,19 @@ const [Drawer, drawerApi] = useVbenDrawer({
### drawerApi
| 方法 | 描述 | 类型 |
| --- | --- | --- |
| 方法 | 描述 | 类型 | 版本限制 |
| --- | --- | --- | --- |
| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>drawerApi` |
| open | 打开弹窗 | `()=>void` |
| close | 关闭弹窗 | `()=>void` |
| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` |
| getData | 获取共享数据 | `<T>()=>T` |
| useStore | 获取可响应式状态 | - |
| open | 打开弹窗 | `()=>void` | --- |
| close | 关闭弹窗 | `()=>void` | --- |
| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` | --- |
| getData | 获取共享数据 | `<T>()=>T` | --- |
| useStore | 获取可响应式状态 | - | --- |
| lock | 将抽屉标记为提交中,锁定当前状态 | `(isLock:boolean)=>drawerApi` | >5.5.3 |
| unlock | lock方法的反操作解除抽屉的锁定状态也是lock(false)的别名 | `()=>drawerApi` | >5.5.3 |
::: info lock
`lock`方法用于锁定抽屉的状态一般用于提交数据的过程中防止用户重复提交或者抽屉被意外关闭、表单数据被改变等等。当处于锁定状态时抽屉的确认按钮会变为loading状态同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭抽屉、开启抽屉的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的抽屉时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
:::

View File

@@ -279,22 +279,24 @@ const [Form, formApi] = useVbenForm({
useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。
| 方法名 | 描述 | 类型 |
| --- | --- | --- |
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` |
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` |
| resetForm | 重置表单 | `()=>Promise<void>` |
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
| validate | 表单校验 | `()=>Promise<void>` |
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
| setState | 设置组件状态props | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` |
| getState | 获取组件状态props | `()=>Promise<VbenFormProps>` |
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - |
| 方法名 | 描述 | 类型 | 版本号 |
| --- | --- | --- | --- |
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
| resetForm | 重置表单 | `()=>Promise<void>` | - |
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` | - |
| validate | 表单校验 | `()=>Promise<void>` | - |
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` | - |
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` | - |
| resetValidate | 重置表单校验 | `()=>Promise<void>` | - |
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - |
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` | - |
| setState | 设置组件状态props | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` | - |
| getState | 获取组件状态props | `()=>Promise<VbenFormProps>` | - |
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - |
| getFieldComponentRef | 获取指定字段的组件实例 | `<T=unknown>(fieldName: string)=>T` | >5.5.3 |
| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
## Props
@@ -347,7 +349,7 @@ export interface ActionButtonOptions {
/** 是否显示 */
show?: boolean;
/** 按钮文本 */
text?: string;
content?: string;
/** 任意属性 */
[key: string]: any;
}
@@ -445,9 +447,9 @@ export interface FormSchema<
/** 字段名,也作为自定义插槽的名称 */
fieldName: string;
/** 帮助信息 */
help?: string;
/** 表单 */
label?: string;
help?: CustomRenderType;
/** 表单的标签如果是一个string会用于默认必选规则的消息提示 */
label?: CustomRenderType;
/** 自定义组件内部渲染 */
renderComponentContent?: RenderComponentContentType;
/** 字段规则 */
@@ -518,20 +520,25 @@ import { z } from '#/adapter/form';
// 可选(可以是undefined)并且携带默认值。注意zod的optional不包括空字符串''
{
rules: z.string().default('默认值').optional(),
rules: z.string().default('默认值').optional();
}
// 可以是空字符串、undefined或者一个邮箱地址
// 可以是空字符串、undefined或者一个邮箱地址(两种不同的用法)
{
rules: z.union(z.string().email().optional(), z.literal(""))
rules: z.union([z.string().email().optional(), z.literal('')]);
}
{
rules: z.string().email().or(z.literal('')).optional();
}
// 复杂校验
{
z.string().min(1, { message: "请输入" })
.refine((value) => value === "123", {
message: "值必须为123",
});
z.string()
.min(1, { message: '请输入' })
.refine((value) => value === '123', {
message: '值必须为123',
});
}
```

View File

@@ -155,9 +155,10 @@ const [Modal, modalApi] = useVbenModal({
| getData | 获取共享数据 | `<T>()=>T` | - |
| useStore | 获取可响应式状态 | - | - |
| lock | 将弹窗标记为提交中,锁定当前状态 | `(isLock:boolean)=>modalApi` | >5.5.2 |
| unlock | lock方法的反操作解除弹窗的锁定状态也是lock(false)的别名 | `()=>modalApi` | >5.5.3 |
::: info lock
`lock`方法用于锁定当前弹窗的状态一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时弹窗的确认按钮会变为loading状态同时禁用确认按钮、隐藏关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。
`lock`方法用于锁定当前弹窗的状态一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时弹窗的确认按钮会变为loading状态同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
:::

View File

@@ -165,7 +165,7 @@ vxeUI.renderer.add('CellLink', {
**表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。
当启用了表单搜索时可以在toolbarConfig中配置`search``true`来让表格在工具栏区域显示一个搜索表单控制按钮。
当启用了表单搜索时可以在toolbarConfig中配置`search``true`来让表格在工具栏区域显示一个搜索表单控制按钮。表格的所有以`form-`开头的命名插槽都会被传递给搜索表单。
<DemoPreview dir="demos/vben-vxe-table/form" />
@@ -231,12 +231,28 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
所有属性都可以传入 `useVbenVxeGrid` 的第一个参数中。
| 属性名 | 描述 | 类型 |
| -------------- | ------------------ | ------------------- |
| tableTitle | 表格标题 | `string` |
| tableTitleHelp | 表格标题帮助信息 | `string` |
| gridClass | grid组件的class | `string` |
| gridOptions | grid组件的参数 | `VxeTableGridProps` |
| gridEvents | grid组件的触发的⌚️ | `VxeGridListeners` |
| formOptions | 表单参数 | `VbenFormProps` |
| showSearchForm | 是否显示搜索表单 | `boolean` |
| 属性名 | 描述 | 类型 |
| -------------- | -------------------- | ------------------- |
| tableTitle | 表格标题 | `string` |
| tableTitleHelp | 表格标题帮助信息 | `string` |
| gridClass | grid组件的class | `string` |
| gridOptions | grid组件的参数 | `VxeTableGridProps` |
| gridEvents | grid组件的触发的事件 | `VxeGridListeners` |
| formOptions | 表单参数 | `VbenFormProps` |
| showSearchForm | 是否显示搜索表单 | `boolean` |
## Slots
大部分插槽的说明请参考 [vxe-table 官方文档](https://vxetable.cn/v4/#/grid/api),但工具栏部分由于做了一些定制封装,需使用以下插槽定制表格的工具栏:
| 插槽名 | 描述 |
| --------------- | -------------------------------------------- |
| toolbar-actions | 工具栏左侧部分(表格标题附近) |
| toolbar-tools | 工具栏右侧部分vxeTable原生工具按钮的左侧 |
| table-title | 表格标题插槽 |
::: info 搜索表单的插槽
对于使用了搜索表单的表格来说,所有以`form-`开头的命名插槽都会传递给表单。
:::

View File

@@ -47,7 +47,7 @@ cd apps/web-antd/dist
# 本地预览默认端口8080
live-server
# 指定端口
live-server --port 9000
live-server --port=9000
```
## 压缩

View File

@@ -231,19 +231,17 @@ function createRequestClient(baseURL: string) {
},
});
// response数据解构
client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => {
const { data: responseData, status } = response;
const { code, data } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw Object.assign({}, response, { response });
},
});
// 处理返回的响应数据格式。会根据responseReturn指定的类型返回对应的数据
client.addResponseInterceptor(
defaultResponseInterceptor({
// 指定接口返回的数据中的 code 字段名
codeField: 'code',
// 指定接口返回的数据中装载了主要数据的字段名
dataField: 'data',
// 请求成功的 code 值,如果接口返回的 code 等于 successCode 则会认为是成功的请求
successCode: 0,
}),
);
// token过期的处理
client.addResponseInterceptor(

View File

@@ -538,4 +538,6 @@ interface Preferences {
- `overridesPreferences`方法只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置。
- 任何配置项都可以覆盖,只需要在`overridesPreferences`方法内覆盖即可,不要修改默认配置文件。
- 更改配置后请清空缓存,否则可能不生效。:::
- 更改配置后请清空缓存,否则可能不生效。
:::

View File

@@ -296,7 +296,7 @@ const { hasAccessByRoles } = useAccess();
#### 指令方式
> 指令支持绑定单个或多个权限码。单个时可以直接传入字符串或数组中包含一个权限码,多个权限码则传入数组。
> 指令支持绑定单个或多个角色。单个时可以直接传入字符串或数组中包含一个角色,多个角色均可访问则传入数组。
```vue
<template>

View File

@@ -72,7 +72,7 @@ pnpm install
## 其他
如果你想更进一步精简,你可以删除参考下文件或者文件夹的作用,判断自己是否需要,不需要删除即可:
如果你想更进一步精简,你可以删除参考下文件或者文件夹的作用,判断自己是否需要,不需要删除即可:
- `.changeset` 文件夹用于管理版本变更
- `.github` 文件夹用于存放 GitHub 的配置文件

View File

@@ -174,7 +174,7 @@ export async function javascript(): Promise<Linter.Config[]> {
],
'no-use-before-define': [
'error',
{ classes: false, functions: false, variables: true },
{ classes: false, functions: false, variables: false },
],
'no-useless-backreference': 'error',
'no-useless-call': 'error',

View File

@@ -95,7 +95,7 @@
"node": ">=20.10.0",
"pnpm": ">=9.12.0"
},
"packageManager": "pnpm@9.15.3",
"packageManager": "pnpm@9.15.7",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {

View File

@@ -1,9 +1,7 @@
export {
ArrowDown,
ArrowLeft,
ArrowLeftFromLine as MdiMenuOpen,
ArrowLeftToLine,
ArrowRightFromLine as MdiMenuClose,
ArrowRightLeft,
ArrowRightToLine,
ArrowUp,
@@ -16,6 +14,8 @@ export {
ChevronRight,
ChevronsLeft,
ChevronsRight,
Circle,
CircleCheckBig,
CircleHelp,
Copy,
CornerDownLeft,
@@ -29,6 +29,7 @@ export {
Github,
Grip,
GripVertical,
Menu as IconDefault,
Info,
InspectionPanel,
Languages,
@@ -37,7 +38,8 @@ export {
LogOut,
MailCheck,
Maximize,
Menu as IconDefault,
ArrowRightFromLine as MdiMenuClose,
ArrowLeftFromLine as MdiMenuOpen,
Menu,
Minimize,
Minimize2,
@@ -47,11 +49,15 @@ export {
PanelRight,
Pin,
PinOff,
Plus,
RotateCw,
Search,
SearchX,
Settings,
Shrink,
Square,
SquareCheckBig,
SquareMinus,
Sun,
SunMoon,
SwatchBook,

View File

@@ -80,6 +80,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"enable": true,
"height": 38,
"keepAlive": true,
"maxCount": 0,
"middleClickToClose": false,
"persist": true,
"showIcon": true,

View File

@@ -80,6 +80,7 @@ const defaultPreferences: Preferences = {
enable: true,
height: 38,
keepAlive: true,
maxCount: 0,
middleClickToClose: false,
persist: true,
showIcon: true,

View File

@@ -168,6 +168,8 @@ interface TabbarPreferences {
height: number;
/** 开启标签页缓存功能 */
keepAlive: boolean;
/** 限制最大数量 */
maxCount: number;
/** 是否点击中键时关闭标签 */
middleClickToClose: boolean;
/** 是否持久化标签 */

View File

@@ -5,6 +5,8 @@ import type {
ValidationOptions,
} from 'vee-validate';
import type { ComponentPublicInstance } from 'vue';
import type { Recordable } from '@vben-core/typings';
import type { FormActions, FormSchema, VbenFormProps } from './types';
@@ -56,6 +58,11 @@ export class FormApi {
public store: Store<VbenFormProps>;
/**
* 组件实例映射
*/
private componentRefMap: Map<string, unknown> = new Map();
// 最后一次点击提交时的表单值
private latestSubmissionValues: null | Recordable<any> = null;
@@ -85,6 +92,46 @@ export class FormApi {
bindMethods(this);
}
/**
* 获取字段组件实例
* @param fieldName 字段名
* @returns 组件实例
*/
getFieldComponentRef<T = ComponentPublicInstance>(
fieldName: string,
): T | undefined {
return this.componentRefMap.has(fieldName)
? (this.componentRefMap.get(fieldName) as T)
: undefined;
}
/**
* 获取当前聚焦的字段如果没有聚焦的字段则返回undefined
*/
getFocusedField() {
for (const fieldName of this.componentRefMap.keys()) {
const ref = this.getFieldComponentRef(fieldName);
if (ref) {
let el: HTMLElement | null = null;
if (ref instanceof HTMLElement) {
el = ref;
} else if (ref.$el instanceof HTMLElement) {
el = ref.$el;
}
if (!el) {
continue;
}
if (
el === document.activeElement ||
el.contains(document.activeElement)
) {
return fieldName;
}
}
}
return undefined;
}
getLatestSubmissionValues() {
return this.latestSubmissionValues || {};
}
@@ -93,9 +140,9 @@ export class FormApi {
return this.state;
}
async getValues() {
async getValues<T = Recordable<any>>() {
const form = await this.getForm();
return form.values ? this.handleRangeTimeValue(form.values) : {};
return (form.values ? this.handleRangeTimeValue(form.values) : {}) as T;
}
async isFieldValid(fieldName: string) {
@@ -143,13 +190,14 @@ export class FormApi {
return proxy;
}
mount(formActions: FormActions) {
mount(formActions: FormActions, componentRefMap: Map<string, unknown>) {
if (!this.isMounted) {
Object.assign(this.form, formActions);
this.stateHandler.setConditionTrue();
this.setLatestSubmissionValues({
...toRaw(this.handleRangeTimeValue(this.form.values)),
});
this.componentRefMap = componentRefMap;
this.isMounted = true;
}
}

View File

@@ -3,7 +3,7 @@ import type { ZodType } from 'zod';
import type { FormSchema, MaybeComponentProps } from '../types';
import { computed, nextTick, useTemplateRef, watch } from 'vue';
import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
import {
FormControl,
@@ -18,6 +18,7 @@ import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
import { toTypedSchema } from '@vee-validate/zod';
import { useFieldError, useFormValues } from 'vee-validate';
import { injectComponentRefMap } from '../use-form-context';
import { injectRenderFormProps, useFormContext } from './context';
import useDependencies from './dependencies';
import FormLabel from './form-label.vue';
@@ -193,7 +194,7 @@ const fieldProps = computed(() => {
const rules = fieldRules.value;
return {
keepValue: true,
label,
label: isString(label) ? label : '',
...(rules ? { rules } : {}),
...(formFieldProps as Record<string, any>),
};
@@ -267,6 +268,15 @@ function autofocus() {
fieldComponentRef.value?.focus?.();
}
}
const componentRefMap = injectComponentRefMap();
watch(fieldComponentRef, (componentRef) => {
componentRefMap?.set(fieldName, componentRef);
});
onUnmounted(() => {
if (componentRefMap?.has(fieldName)) {
componentRefMap.delete(fieldName);
}
});
</script>
<template>
@@ -285,7 +295,7 @@ function autofocus() {
'pb-6': !compact,
'pb-2': compact,
}"
class="flex"
class="relative flex"
v-bind="$attrs"
>
<FormLabel
@@ -301,55 +311,61 @@ function autofocus() {
)
"
:help="help"
:colon="colon"
:label="label"
:required="shouldRequired && !hideRequiredMark"
:style="labelStyle"
>
<template v-if="label">
<span>{{ label }}</span>
<span v-if="colon" class="ml-[2px]">:</span>
<VbenRenderContent :content="label" />
</template>
</FormLabel>
<div :class="cn('relative flex w-full items-center', wrapperClass)">
<FormControl :class="cn(controlClass)">
<slot
v-bind="{
...slotProps,
...createComponentProps(slotProps),
disabled: shouldDisabled,
isInValid,
}"
>
<component
:is="FieldComponent"
ref="fieldComponentRef"
:class="{
'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
isInValid,
<div class="flex-auto overflow-hidden">
<div :class="cn('relative flex w-full items-center', wrapperClass)">
<FormControl :class="cn(controlClass)">
<slot
v-bind="{
...slotProps,
...createComponentProps(slotProps),
disabled: shouldDisabled,
isInValid,
}"
v-bind="createComponentProps(slotProps)"
:disabled="shouldDisabled"
>
<template v-for="name in renderContentKey" :key="name" #[name]>
<VbenRenderContent
:content="customContentRender[name]"
v-bind="slotProps"
/>
</template>
<!-- <slot></slot> -->
</component>
</slot>
</FormControl>
<!-- 自定义后缀 -->
<div v-if="suffix" class="ml-1">
<VbenRenderContent :content="suffix" />
<component
:is="FieldComponent"
ref="fieldComponentRef"
:class="{
'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
isInValid,
}"
v-bind="createComponentProps(slotProps)"
:disabled="shouldDisabled"
>
<template
v-for="name in renderContentKey"
:key="name"
#[name]="renderSlotProps"
>
<VbenRenderContent
:content="customContentRender[name]"
v-bind="{ ...renderSlotProps, formContext: slotProps }"
/>
</template>
<!-- <slot></slot> -->
</component>
</slot>
</FormControl>
<!-- 自定义后缀 -->
<div v-if="suffix" class="ml-1">
<VbenRenderContent :content="suffix" />
</div>
<FormDescription v-if="description" class="ml-1">
<VbenRenderContent :content="description" />
</FormDescription>
</div>
<FormDescription v-if="description">
<VbenRenderContent :content="description" />
</FormDescription>
<Transition name="slide-up">
<FormMessage class="absolute -bottom-[22px]" />
<FormMessage class="absolute bottom-1" />
</Transition>
</div>
</FormItem>

View File

@@ -1,10 +1,14 @@
<script setup lang="ts">
import type { CustomRenderType } from '../types';
import { FormLabel, VbenHelpTooltip } from '@vben-core/shadcn-ui';
import { cn } from '@vben-core/shared/utils';
interface Props {
class?: string;
help?: string;
colon?: boolean;
help?: CustomRenderType;
label?: CustomRenderType;
required?: boolean;
}
@@ -20,6 +24,8 @@ const props = defineProps<Props>();
<span class="whitespace-pre-line">
{{ help }}
</span>
<!-- <VbenRenderContent :content="help" /> -->
</VbenHelpTooltip>
<span v-if="colon && label" class="ml-[2px]">:</span>
</FormLabel>
</template>

View File

@@ -244,13 +244,13 @@ export interface FormSchema<
/** 依赖 */
dependencies?: FormItemDependencies;
/** 描述 */
description?: string;
description?: CustomRenderType;
/** 字段名 */
fieldName: string;
/** 帮助信息 */
help?: string;
help?: CustomRenderType;
/** 表单项 */
label?: string;
label?: CustomRenderType;
// 自定义组件内部渲染
renderComponentContent?: RenderComponentContentType;
/** 字段规则 */

View File

@@ -20,6 +20,9 @@ export const [injectFormProps, provideFormProps] =
'VbenFormProps',
);
export const [injectComponentRefMap, provideComponentRefMap] =
createContext<Map<string, unknown>>('ComponentRefMap');
export function useFormInitial(
props: ComputedRef<VbenFormProps> | VbenFormProps,
) {

View File

@@ -17,7 +17,11 @@ import {
DEFAULT_FORM_COMMON_CONFIG,
} from './config';
import { Form } from './form-render';
import { provideFormProps, useFormInitial } from './use-form-context';
import {
provideComponentRefMap,
provideFormProps,
useFormInitial,
} from './use-form-context';
// 通过 extends 会导致热更新卡死,所以重复写了一遍
interface Props extends VbenFormProps {
formApi: ExtendedFormApi;
@@ -29,11 +33,14 @@ const state = props.formApi?.useStore?.();
const forward = useForwardPriorityValues(props, state);
const componentRefMap = new Map<string, unknown>();
const { delegatedSlots, form } = useFormInitial(forward);
provideFormProps([forward, form]);
provideComponentRefMap(componentRefMap);
props.formApi?.mount?.(form);
props.formApi?.mount?.(form, componentRefMap);
const handleUpdateCollapsed = (value: boolean) => {
props.formApi?.setState({ collapsed: !!value });

View File

@@ -3,6 +3,8 @@ import type { CSSProperties } from 'vue';
import type { VbenLayoutProps } from './vben-layout';
import { computed, ref, watch } from 'vue';
import {
SCROLL_FIXED_CLASS,
useLayoutFooterStyle,
@@ -11,8 +13,8 @@ import {
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
import { computed, ref, watch } from 'vue';
import {
LayoutContent,
@@ -60,10 +62,16 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>();
const sidebarCollapse = defineModel<boolean>('sidebarCollapse');
const sidebarCollapse = defineModel<boolean>('sidebarCollapse', {
default: false,
});
const sidebarExtraVisible = defineModel<boolean>('sidebarExtraVisible');
const sidebarExtraCollapse = defineModel<boolean>('sidebarExtraCollapse');
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
const sidebarExtraCollapse = defineModel<boolean>('sidebarExtraCollapse', {
default: false,
});
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover', {
default: false,
});
const sidebarEnable = defineModel<boolean>('sidebarEnable', { default: true });
// side是否处于hover状态展开菜单中

View File

@@ -38,6 +38,7 @@ export class DrawerApi {
const defaultState: DrawerState = {
class: '',
closable: true,
closeIconPlacement: 'right',
closeOnClickModal: true,
closeOnPressEscape: true,
confirmLoading: false,
@@ -51,6 +52,7 @@ export class DrawerApi {
placement: 'right',
showCancelButton: true,
showConfirmButton: true,
submitting: false,
title: '',
};
@@ -91,7 +93,11 @@ export class DrawerApi {
// 如果 onBeforeClose 返回 false则不关闭弹窗
const allowClose = this.api.onBeforeClose?.() ?? true;
if (allowClose) {
this.store.setState((prev) => ({ ...prev, isOpen: false }));
this.store.setState((prev) => ({
...prev,
isOpen: false,
submitting: false,
}));
}
}
@@ -107,6 +113,15 @@ export class DrawerApi {
return (this.sharedData?.payload ?? {}) as T;
}
/**
* 锁定抽屉状态(用于提交过程中的等待状态)
* @description 锁定状态将禁用默认的取消按钮使用spinner覆盖抽屉内容隐藏关闭按钮阻止手动关闭弹窗将默认的提交按钮标记为loading状态
* @param isLocked 是否锁定
*/
lock(isLocked: boolean = true) {
return this.setState({ submitting: isLocked });
}
/**
* 取消操作
*/
@@ -164,4 +179,12 @@ export class DrawerApi {
}
return this;
}
/**
* 解除抽屉的锁定状态
* @description 解除由lock方法设置的锁定状态是lock(false)的别名
*/
unlock() {
return this.lock(false);
}
}

View File

@@ -75,12 +75,12 @@ export interface DrawerProps {
* @default false
*/
loading?: boolean;
/**
* 是否显示遮罩
* @default true
*/
modal?: boolean;
/**
* 是否自动聚焦
*/
@@ -89,12 +89,12 @@ export interface DrawerProps {
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/**
* 抽屉位置
* @default right
*/
placement?: DrawerPlacement;
/**
* 是否显示取消按钮
* @default true
@@ -105,6 +105,10 @@ export interface DrawerProps {
* @default true
*/
showConfirmButton?: boolean;
/**
* 提交中(锁定抽屉状态)
*/
submitting?: boolean;
/**
* 弹窗标题
*/

View File

@@ -36,6 +36,7 @@ const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
closeIconPlacement: 'right',
drawerApi: undefined,
submitting: false,
zIndex: 1000,
});
@@ -55,6 +56,7 @@ const {
cancelText,
class: drawerClass,
closable,
closeIconPlacement,
closeOnClickModal,
closeOnPressEscape,
confirmLoading,
@@ -72,6 +74,7 @@ const {
placement,
showCancelButton,
showConfirmButton,
submitting,
title,
titleTooltip,
zIndex,
@@ -90,12 +93,12 @@ watch(
);
function interactOutside(e: Event) {
if (!closeOnClickModal.value) {
if (!closeOnClickModal.value || submitting.value) {
e.preventDefault();
}
}
function escapeKeyDown(e: KeyboardEvent) {
if (!closeOnPressEscape.value) {
if (!closeOnPressEscape.value || submitting.value) {
e.preventDefault();
}
}
@@ -103,7 +106,11 @@ function escapeKeyDown(e: KeyboardEvent) {
function pointerDownOutside(e: Event) {
const target = e.target as HTMLElement;
const dismissableDrawer = target?.dataset.dismissableDrawer;
if (!closeOnClickModal.value || dismissableDrawer !== id) {
if (
submitting.value ||
!closeOnClickModal.value ||
dismissableDrawer !== id
) {
e.preventDefault();
}
}
@@ -120,7 +127,9 @@ function handleFocusOutside(e: Event) {
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
return appendToMain.value
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
: undefined;
});
</script>
<template>
@@ -168,6 +177,7 @@ const getAppendTo = computed(() => {
<SheetClose
v-if="closable && closeIconPlacement === 'left'"
as-child
:disabled="submitting"
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<slot name="close-icon">
@@ -208,6 +218,7 @@ const getAppendTo = computed(() => {
<SheetClose
v-if="closable && closeIconPlacement === 'right'"
as-child
:disabled="submitting"
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<slot name="close-icon">
@@ -232,7 +243,11 @@ const getAppendTo = computed(() => {
})
"
>
<VbenLoading v-if="showLoading" class="size-full" spinning />
<VbenLoading
v-if="showLoading || submitting"
class="size-full"
spinning
/>
<slot></slot>
</div>
@@ -252,6 +267,7 @@ const getAppendTo = computed(() => {
:is="components.DefaultButton || VbenButton"
v-if="showCancelButton"
variant="ghost"
:disabled="submitting"
@click="() => drawerApi?.onCancel()"
>
<slot name="cancelText">
@@ -262,7 +278,7 @@ const getAppendTo = computed(() => {
<component
:is="components.PrimaryButton || VbenButton"
v-if="showConfirmButton"
:loading="confirmLoading"
:loading="confirmLoading || submitting"
@click="() => drawerApi?.onConfirm()"
>
<slot name="confirmText">

View File

@@ -188,4 +188,12 @@ export class ModalApi {
}
return this;
}
/**
* 解除弹窗的锁定状态
* @description 解除由lock方法设置的锁定状态是lock(false)的别名
*/
unlock() {
return this.lock(false);
}
}

View File

@@ -172,7 +172,9 @@ function handleFocusOutside(e: Event) {
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
return appendToMain.value
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
: undefined;
});
</script>
<template>
@@ -200,12 +202,13 @@ const getAppendTo = computed(() => {
"
:modal="modal"
:open="state?.isOpen"
:show-close="submitting ? false : closable"
:show-close="closable"
:z-index="zIndex"
:overlay-blur="overlayBlur"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"
:close-disabled="submitting"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"
@interact-outside="interactOutside"

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import type {
AvatarFallbackProps,
AvatarImageProps,
AvatarRootProps,
} from 'radix-vue';
import type { ClassType } from '@vben-core/typings';
import { computed } from 'vue';
import { Avatar, AvatarFallback, AvatarImage } from '../../ui';
@@ -15,6 +16,7 @@ interface Props extends AvatarFallbackProps, AvatarImageProps, AvatarRootProps {
class?: ClassType;
dot?: boolean;
dotClass?: ClassType;
size?: number;
}
defineOptions({
@@ -31,10 +33,23 @@ const props = withDefaults(defineProps<Props>(), {
const text = computed(() => {
return props.alt.slice(-2).toUpperCase();
});
const rootStyle = computed(() => {
return props.size !== undefined && props.size > 0
? {
height: `${props.size}px`,
width: `${props.size}px`,
}
: {};
});
</script>
<template>
<div :class="props.class" class="relative flex flex-shrink-0 items-center">
<div
:class="props.class"
:style="rootStyle"
class="relative flex flex-shrink-0 items-center"
>
<Avatar :class="props.class" class="size-full">
<AvatarImage :alt="alt" :src="src" />
<AvatarFallback>{{ text }}</AvatarFallback>

View File

@@ -3,8 +3,8 @@ import type { BreadcrumbProps } from './types';
import { useForwardPropsEmits } from 'radix-vue';
import Breadcrumb from './breadcrumb.vue';
import BreadcrumbBackground from './breadcrumb-background.vue';
import Breadcrumb from './breadcrumb.vue';
interface Props extends BreadcrumbProps {
class?: any;
@@ -17,6 +17,23 @@ const emit = defineEmits<{ select: [string] }>();
const forward = useForwardPropsEmits(props, emit);
</script>
<template>
<Breadcrumb v-if="styleType === 'normal'" v-bind="forward" />
<BreadcrumbBackground v-if="styleType === 'background'" v-bind="forward" />
<Breadcrumb
v-if="styleType === 'normal'"
v-bind="forward"
class="vben-breadcrumb"
/>
<BreadcrumbBackground
v-if="styleType === 'background'"
v-bind="forward"
class="vben-breadcrumb"
/>
</template>
<style lang="scss" scoped>
/** 修复全局引入Antd时ol和ul的默认样式会被修改的问题 */
.vben-breadcrumb {
:deep(ol),
:deep(ul) {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,98 @@
<script lang="ts" setup>
import { cn } from '@vben-core/shared/utils';
defineOptions({ name: 'VbenButtonGroup' });
withDefaults(
defineProps<{
border?: boolean;
gap?: number;
size?: 'large' | 'middle' | 'small';
}>(),
{ border: false, gap: 0, size: 'middle' },
);
</script>
<template>
<div
:class="
cn(
'vben-button-group rounded-md',
`size-${size}`,
gap ? 'with-gap' : 'no-gap',
$attrs.class as string,
)
"
:style="{ gap: gap ? `${gap}px` : '0px' }"
>
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.vben-button-group {
display: inline-flex;
&.size-large :deep(button) {
height: 2.25rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
.icon-wrapper {
margin-right: 0.4rem;
svg {
width: 1rem;
height: 1rem;
}
}
}
&.size-middle :deep(button) {
height: 2rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
line-height: 1rem;
.icon-wrapper {
margin-right: 0.2rem;
svg {
width: 0.75rem;
height: 0.75rem;
}
}
}
&.size-small :deep(button) {
height: 1.75rem;
padding: 0.2rem 0.4rem;
font-size: 0.65rem;
line-height: 0.75rem;
.icon-wrapper {
margin-right: 0.1rem;
svg {
width: 0.65rem;
height: 0.65rem;
}
}
}
&.no-gap > :deep(button):nth-of-type(1) {
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
}
&.no-gap > :deep(button):last-of-type {
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
}
&.no-gap {
:deep(button + button) {
border-left-width: 0;
border-radius: 0;
}
}
}
</style>

View File

@@ -1,4 +1,5 @@
import type { AsTag } from 'radix-vue';
import type { Component } from 'vue';
import type { ButtonVariants, ButtonVariantSize } from '../../ui';
@@ -21,3 +22,21 @@ export interface VbenButtonProps {
size?: ButtonVariantSize;
variant?: ButtonVariants;
}
export type CustomRenderType = (() => Component | string) | string;
export type ValueType = boolean | number | string;
export interface VbenButtonGroupProps
extends Pick<VbenButtonProps, 'disabled'> {
beforeChange?: (
value: ValueType,
isChecked: boolean,
) => boolean | PromiseLike<boolean | undefined> | undefined;
btnClass?: any;
gap?: number;
multiple?: boolean;
options?: { label: CustomRenderType; value: ValueType }[];
showIcon?: boolean;
size?: 'large' | 'middle' | 'small';
}

View File

@@ -0,0 +1,163 @@
<script lang="ts" setup>
import type { Arrayable } from '@vueuse/core';
import type { ValueType, VbenButtonGroupProps } from './button';
import { computed, ref, watch } from 'vue';
import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
import { VbenRenderContent } from '@vben-core/shadcn-ui';
import { cn, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
import VbenButtonGroup from './button-group.vue';
import Button from './button.vue';
const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
gap: 0,
multiple: false,
showIcon: true,
size: 'middle',
});
const btnDefaultProps = computed(() => {
return {
...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),
class: cn(props.btnClass),
};
});
const modelValue = defineModel<Arrayable<ValueType> | undefined>();
const innerValue = ref<Array<ValueType>>([]);
const loadingValues = ref<Array<ValueType>>([]);
watch(
() => props.multiple,
(val) => {
if (val) {
modelValue.value = innerValue.value;
} else {
modelValue.value =
innerValue.value.length > 0 ? innerValue.value[0] : undefined;
}
},
{ immediate: true },
);
watch(
() => modelValue.value,
(val) => {
if (Array.isArray(val)) {
const arrVal = val.filter((v) => v !== undefined);
if (arrVal.length > 0) {
innerValue.value = props.multiple
? [...arrVal]
: [arrVal[0] as ValueType];
} else {
innerValue.value = [];
}
} else {
innerValue.value = val === undefined ? [] : [val as ValueType];
}
},
{ deep: true },
);
async function onBtnClick(value: ValueType) {
if (props.beforeChange && isFunction(props.beforeChange)) {
try {
loadingValues.value.push(value);
const canChange = await props.beforeChange(
value,
!innerValue.value.includes(value),
);
if (canChange === false) {
return;
}
} finally {
loadingValues.value.splice(loadingValues.value.indexOf(value), 1);
}
}
if (props.multiple) {
if (innerValue.value.includes(value)) {
innerValue.value = innerValue.value.filter((item) => item !== value);
} else {
innerValue.value.push(value);
}
modelValue.value = innerValue.value;
} else {
innerValue.value = [value];
modelValue.value = value;
}
}
</script>
<template>
<VbenButtonGroup
:size="props.size"
:gap="props.gap"
class="vben-check-button-group"
>
<Button
v-for="(btn, index) in props.options"
:key="index"
:class="cn('border', props.btnClass)"
:disabled="
props.disabled ||
loadingValues.includes(btn.value) ||
(!props.multiple && loadingValues.length > 0)
"
v-bind="btnDefaultProps"
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
@click="onBtnClick(btn.value)"
>
<div class="icon-wrapper" v-if="props.showIcon">
<LoaderCircle
class="animate-spin"
v-if="loadingValues.includes(btn.value)"
/>
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
</div>
<slot name="option" :label="btn.label" :value="btn.value">
<VbenRenderContent :content="btn.label" />
</slot>
</Button>
</VbenButtonGroup>
</template>
<style lang="scss" scoped>
.vben-check-button-group {
&:deep(.size-large) button {
.icon-wrapper {
margin-right: 0.3rem;
svg {
width: 1rem;
height: 1rem;
}
}
}
&:deep(.size-middle) button {
.icon-wrapper {
margin-right: 0.2rem;
svg {
width: 0.75rem;
height: 0.75rem;
}
}
}
&:deep(.size-small) button {
.icon-wrapper {
margin-right: 0.1rem;
svg {
width: 0.65rem;
height: 0.65rem;
}
}
}
}
</style>

View File

@@ -1,3 +1,5 @@
export type * from './button';
export { default as VbenButtonGroup } from './button-group.vue';
export { default as VbenButton } from './button.vue';
export { default as VbenCheckButtonGroup } from './check-button-group.vue';
export { default as VbenIconButton } from './icon-button.vue';

View File

@@ -7,7 +7,7 @@ import { useForwardPropsEmits } from 'radix-vue';
import { Checkbox } from '../../ui/checkbox';
const props = defineProps<CheckboxRootProps>();
const props = defineProps<CheckboxRootProps & { indeterminate?: boolean }>();
const emits = defineEmits<CheckboxRootEmits>();

View File

@@ -37,7 +37,18 @@ const props = withDefaults(defineProps<Props>(), {
useEasing: true,
});
const emit = defineEmits(['onStarted', 'onFinished']);
const emit = defineEmits<{
finished: [];
/**
* @deprecated 请使用{@link finished}事件
*/
onFinished: [];
/**
* @deprecated 请使用{@link started}事件
*/
onStarted: [];
started: [];
}>();
const source = ref(props.startVal);
const disabled = ref(false);
@@ -73,8 +84,14 @@ function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => emit('onFinished'),
onStarted: () => emit('onStarted'),
onFinished: () => {
emit('finished');
emit('onFinished');
},
onStarted: () => {
emit('started');
emit('onStarted');
},
...(props.useEasing
? { transition: TransitionPresets[props.transition] }
: {}),

View File

@@ -52,7 +52,8 @@ withDefaults(defineProps<Props>(), {
v-if="src"
:alt="text"
:src="src"
class="relative w-8 rounded-none bg-transparent"
:size="logoSize"
class="relative rounded-none bg-transparent"
/>
<span
v-if="!collapsed"

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