Browse Source

Merge branch 'vbenjs:main' into main

pull/8367/head
失忆 1 year ago
committed by GitHub
parent
commit
2731d4ebd7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .gitignore
  2. 6
      README.ja-JP.md
  3. 6
      README.md
  4. 6
      README.zh-CN.md
  5. 4
      apps/backend-mock/api/auth/codes.ts
  6. 8
      apps/backend-mock/api/auth/login.post.ts
  7. 2
      apps/backend-mock/api/auth/logout.post.ts
  8. 4
      apps/backend-mock/api/auth/refresh.post.ts
  9. 4
      apps/backend-mock/api/demo/bigint.ts
  10. 4
      apps/backend-mock/api/menu/all.ts
  11. 3
      apps/backend-mock/api/status.ts
  12. 1
      apps/backend-mock/api/system/dept/.post.ts
  13. 1
      apps/backend-mock/api/system/dept/[id].delete.ts
  14. 1
      apps/backend-mock/api/system/dept/[id].put.ts
  15. 1
      apps/backend-mock/api/system/dept/list.ts
  16. 1
      apps/backend-mock/api/system/menu/list.ts
  17. 3
      apps/backend-mock/api/system/menu/name-exists.ts
  18. 3
      apps/backend-mock/api/system/menu/path-exists.ts
  19. 1
      apps/backend-mock/api/system/role/list.ts
  20. 80
      apps/backend-mock/api/table/list.ts
  21. 2
      apps/backend-mock/api/test.get.ts
  22. 2
      apps/backend-mock/api/test.post.ts
  23. 3
      apps/backend-mock/api/upload.ts
  24. 3
      apps/backend-mock/api/user/info.ts
  25. 1
      apps/backend-mock/middleware/1.api.ts
  26. 2
      apps/backend-mock/routes/[...].ts
  27. 2
      apps/backend-mock/utils/cookie-utils.ts
  28. 26
      apps/backend-mock/utils/jwt-utils.ts
  29. 2
      apps/backend-mock/utils/response.ts
  30. 2
      apps/web-antd/package.json
  31. 2
      apps/web-ele/package.json
  32. 2
      apps/web-naive/package.json
  33. 2
      docs/package.json
  34. 76
      docs/src/components/common-ui/vben-form.md
  35. 10
      docs/src/components/common-ui/vben-modal.md
  36. 36
      docs/src/demos/vben-modal/animation-type/index.vue
  37. 2
      internal/lint-configs/commitlint-config/package.json
  38. 2
      internal/lint-configs/stylelint-config/package.json
  39. 2
      internal/node-utils/package.json
  40. 2
      internal/tailwind-config/package.json
  41. 2
      internal/tsconfig/package.json
  42. 2
      internal/vite-config/package.json
  43. 4
      package.json
  44. 2
      packages/@core/base/design/package.json
  45. 2
      packages/@core/base/icons/package.json
  46. 2
      packages/@core/base/shared/package.json
  47. 82
      packages/@core/base/shared/src/utils/__tests__/resources.test.ts
  48. 1
      packages/@core/base/shared/src/utils/index.ts
  49. 21
      packages/@core/base/shared/src/utils/resources.ts
  50. 2
      packages/@core/base/shared/src/utils/window.ts
  51. 2
      packages/@core/base/typings/package.json
  52. 2
      packages/@core/composables/package.json
  53. 2
      packages/@core/preferences/package.json
  54. 2
      packages/@core/ui-kit/form-ui/package.json
  55. 73
      packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  56. 6
      packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
  57. 32
      packages/@core/ui-kit/form-ui/src/form-render/form.vue
  58. 15
      packages/@core/ui-kit/form-ui/src/types.ts
  59. 2
      packages/@core/ui-kit/layout-ui/package.json
  60. 2
      packages/@core/ui-kit/menu-ui/package.json
  61. 8
      packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
  62. 5
      packages/@core/ui-kit/popup-ui/src/modal/modal.ts
  63. 6
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  64. 2
      packages/@core/ui-kit/shadcn-ui/package.json
  65. 26
      packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue
  66. 6
      packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue
  67. 35
      packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue
  68. 2
      packages/@core/ui-kit/tabs-ui/package.json
  69. 2
      packages/constants/package.json
  70. 2
      packages/effects/access/package.json
  71. 2
      packages/effects/common-ui/package.json
  72. 113
      packages/effects/common-ui/src/ui/authentication/dingding-login.vue
  73. 37
      packages/effects/common-ui/src/ui/authentication/third-party-login.vue
  74. 2
      packages/effects/hooks/package.json
  75. 17
      packages/effects/hooks/src/use-app-config.ts
  76. 2
      packages/effects/layouts/package.json
  77. 21
      packages/effects/layouts/src/basic/layout.vue
  78. 17
      packages/effects/layouts/src/basic/menu/use-navigation.ts
  79. 2
      packages/effects/layouts/src/widgets/global-search/search-panel.vue
  80. 6
      packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue
  81. 2
      packages/effects/plugins/package.json
  82. 2
      packages/effects/plugins/src/vxe-table/use-vxe-grid.vue
  83. 2
      packages/effects/request/package.json
  84. 2
      packages/icons/package.json
  85. 2
      packages/icons/src/iconify/index.ts
  86. 2
      packages/locales/package.json
  87. 5
      packages/locales/src/langs/en-US/authentication.json
  88. 5
      packages/locales/src/langs/zh-CN/authentication.json
  89. 2
      packages/preferences/package.json
  90. 2
      packages/stores/package.json
  91. 2
      packages/styles/package.json
  92. 10
      packages/types/global.d.ts
  93. 2
      packages/types/package.json
  94. 2
      packages/utils/package.json
  95. 4
      playground/.env.development
  96. 2
      playground/package.json
  97. 129
      playground/src/views/examples/form/query.vue
  98. 2
      playground/src/views/examples/vxe-table/remote.vue
  99. 2
      playground/src/views/system/dept/list.vue
  100. 2
      playground/src/views/system/menu/list.vue

1
.gitignore

@ -49,3 +49,4 @@ vite.config.ts.*
*.sln
*.sw?
.history
.cursor

6
README.ja-JP.md

@ -140,8 +140,12 @@ pnpm build
## 貢献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord

6
README.md

@ -140,8 +140,12 @@ If you think this project is helpful to you, you can help the author buy a cup o
## Contributors
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord

6
README.zh-CN.md

@ -140,8 +140,12 @@ pnpm build
## 贡献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord

4
apps/backend-mock/api/auth/codes.ts

@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_CODES } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

8
apps/backend-mock/api/auth/login.post.ts

@ -1,9 +1,15 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
import { MOCK_USERS } from '~/utils/mock-data';
import {
forbiddenResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);

2
apps/backend-mock/api/auth/logout.post.ts

@ -1,7 +1,9 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);

4
apps/backend-mock/api/auth/refresh.post.ts

@ -1,9 +1,11 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {

4
apps/backend-mock/api/demo/bigint.ts

@ -1,3 +1,7 @@
import { eventHandler, setHeader } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {

4
apps/backend-mock/api/menu/all.ts

@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_MENUS } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);

3
apps/backend-mock/api/status.ts

@ -1,3 +1,6 @@
import { eventHandler, getQuery, setResponseStatus } from 'h3';
import { useResponseError } from '~/utils/response';
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));

1
apps/backend-mock/api/system/dept/.post.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

1
apps/backend-mock/api/system/dept/[id].delete.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

1
apps/backend-mock/api/system/dept/[id].put.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

1
apps/backend-mock/api/system/dept/list.ts

@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

1
apps/backend-mock/api/system/menu/list.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

3
apps/backend-mock/api/system/menu/name-exists.ts

@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {};

3
apps/backend-mock/api/system/menu/path-exists.ts

@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };

1
apps/backend-mock/api/system/role/list.ts

@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';

80
apps/backend-mock/api/table/list.ts

@ -1,6 +1,11 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
// 规范化分页参数,处理 string[]
const pageRaw = Array.isArray(page) ? page[0] : page;
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
const pageNumber = Math.max(
1,
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
);
const pageSizeNumber = Math.min(
100,
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
);
const listData = structuredClone(mockData);
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
// 规范化 query 入参,兼容 string[]
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
// 检查 sortBy 是否是 listData 元素的合法属性键
if (
typeof sortKeyRaw === 'string' &&
listData[0] &&
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
) {
// 定义数组元素的类型
type ItemType = (typeof listData)[0];
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
const isDesc = sortOrderRaw === 'desc';
listData.sort((a, b) => {
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
const aValue = a[sortKey] as unknown;
const bValue = b[sortKey] as unknown;
let result = 0;
if (typeof aValue === 'number' && typeof bValue === 'number') {
result = aValue - bValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
result = aValue.getTime() - bValue.getTime();
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
if (aValue === bValue) {
result = 0;
} else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
result = aValue ? 1 : -1;
}
} else {
if (sortBy === 'price') {
return (
Number.parseFloat(b[sortBy as string]) -
Number.parseFloat(a[sortBy as string])
);
} else {
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
}
const aStr = String(aValue);
const bStr = String(bValue);
const aNum = Number(aStr);
const bNum = Number(bStr);
result =
Number.isFinite(aNum) && Number.isFinite(bNum)
? aNum - bNum
: aStr.localeCompare(bStr, undefined, {
numeric: true,
sensitivity: 'base',
});
}
return isDesc ? -result : result;
});
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
});

2
apps/backend-mock/api/test.get.ts

@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test get handler');

2
apps/backend-mock/api/test.post.ts

@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test post handler');

3
apps/backend-mock/api/upload.ts

@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

3
apps/backend-mock/api/user/info.ts

@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

1
apps/backend-mock/middleware/1.api.ts

@ -1,3 +1,4 @@
import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {

2
apps/backend-mock/routes/[...].ts

@ -1,3 +1,5 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => {
return `
<h1>Hello Vben Admin</h1>

2
apps/backend-mock/utils/cookie-utils.ts

@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,

26
apps/backend-mock/utils/jwt-utils.ts

@ -1,8 +1,11 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import type { UserInfo } from './mock-data';
import { getHeader } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
@ -31,12 +34,22 @@ export function verifyAccessToken(
return null;
}
const token = authHeader.split(' ')[1];
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2) {
return null;
}
const token = tokenParts[1] as string;
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const decoded = jwt.verify(
token,
ACCESS_TOKEN_SECRET,
) as unknown as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
@ -50,7 +63,12 @@ export function verifyRefreshToken(
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const user = MOCK_USERS.find(
(item) => item.username === username,
) as UserInfo;
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {

2
apps/backend-mock/utils/response.ts

@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,

2
apps/web-antd/package.json

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

2
apps/web-ele/package.json

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

2
apps/web-naive/package.json

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

2
docs/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/docs",
"version": "5.5.7",
"version": "5.5.9",
"private": true,
"scripts": {
"build": "vitepress build",

76
docs/src/components/common-ui/vben-form.md

@ -90,30 +90,52 @@ import { h } from 'vue';
import { globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = <T extends Component>(
component: T,
@ -304,10 +326,12 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` |
| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
| wrapperClass | 表单的布局,基于tailwindcss | `any` | - |
| actionWrapperClass | 表单操作区域class | `any` | - |
| actionLayout | 表单操作按钮位置 | `'newLine' \| 'rowEnd' \| 'inline'` | `rowEnd` |
| actionPosition | 表单操作按钮对齐方式 | `'left' \| 'center' \| 'right'` | `right` |
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
@ -395,7 +419,7 @@ export interface FormCommonConfig {
* 所有表单项的栅格布局
* @default ""
*/
formItemClass?: string;
formItemClass?: (() => string) | string;
/**
* 隐藏所有表单项label
* @default false

10
docs/src/components/common-ui/vben-modal.md

@ -56,6 +56,15 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
<DemoPreview dir="demos/vben-modal/shared-data" />
## 动画类型
通过 `animationType` 属性可以控制弹窗的动画效果:
- `slide`(默认):从顶部向下滑动进入/退出
- `scale`:缩放淡入/淡出效果
<DemoPreview dir="demos/vben-modal/animation-type" />
::: info 注意
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
@ -112,6 +121,7 @@ const [Modal, modalApi] = useVbenModal({
| bordered | 是否显示border | `boolean` | `false` |
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
| overlayBlur | 遮罩模糊度 | `number` | - |
| animationType | 动画类型 | `'slide' \| 'scale'` | `'slide'` |
| submitting | 标记为提交中,锁定弹窗当前状态 | `boolean` | `false` |
::: info appendToMain

36
docs/src/demos/vben-modal/animation-type/index.vue

@ -0,0 +1,36 @@
<script lang="ts" setup>
import { useVbenModal, VbenButton } from '@vben/common-ui';
const [SlideModal, slideModalApi] = useVbenModal({
animationType: 'slide',
});
const [ScaleModal, scaleModalApi] = useVbenModal({
animationType: 'scale',
});
function openSlideModal() {
slideModalApi.open();
}
function openScaleModal() {
scaleModalApi.open();
}
</script>
<template>
<div class="space-y-4">
<div class="flex gap-4">
<VbenButton @click="openSlideModal">滑动动画</VbenButton>
<VbenButton @click="openScaleModal">缩放动画</VbenButton>
</div>
<SlideModal title="滑动动画示例" class="w-[500px]">
<p>这是使用滑动动画的弹窗,从顶部向下滑动进入。</p>
</SlideModal>
<ScaleModal title="缩放动画示例" class="w-[500px]">
<p>这是使用缩放动画的弹窗,以缩放淡入淡出的方式显示。</p>
</ScaleModal>
</div>
</template>

2
internal/lint-configs/commitlint-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/commitlint-config",
"version": "5.5.7",
"version": "5.5.9",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
internal/lint-configs/stylelint-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/stylelint-config",
"version": "5.5.7",
"version": "5.5.9",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
internal/node-utils/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/node-utils",
"version": "5.5.7",
"version": "5.5.9",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
internal/tailwind-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/tailwind-config",
"version": "5.5.7",
"version": "5.5.9",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
internal/tsconfig/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/tsconfig",
"version": "5.5.7",
"version": "5.5.9",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
internal/vite-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/vite-config",
"version": "5.5.6",
"version": "5.5.9",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

4
package.json

@ -1,6 +1,6 @@
{
"name": "vben-admin-monorepo",
"version": "5.5.7",
"version": "5.5.9",
"private": true,
"keywords": [
"monorepo",
@ -98,7 +98,7 @@
"node": ">=20.10.0",
"pnpm": ">=9.12.0"
},
"packageManager": "pnpm@10.12.4",
"packageManager": "pnpm@10.14.0",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {

2
packages/@core/base/design/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/@core/base/icons/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/@core/base/shared/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

82
packages/@core/base/shared/src/utils/__tests__/resources.test.ts

@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { loadScript } from '../resources';
const testJsPath =
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js';
describe('loadScript', () => {
beforeEach(() => {
// 每个测试前清空 head,保证环境干净
document.head.innerHTML = '';
});
it('should resolve when the script loads successfully', async () => {
const promise = loadScript(testJsPath);
// 此时脚本元素已被创建并插入
const script = document.querySelector(
`script[src="${testJsPath}"]`,
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 模拟加载成功
script.dispatchEvent(new Event('load'));
// 等待 promise resolve
await expect(promise).resolves.toBeUndefined();
});
it('should not insert duplicate script and resolve immediately if already loaded', async () => {
// 先手动插入一个相同 src 的 script
const existing = document.createElement('script');
existing.src = 'bar.js';
document.head.append(existing);
// 再次调用
const promise = loadScript('bar.js');
// 立即 resolve
await expect(promise).resolves.toBeUndefined();
// head 中只保留一个
const scripts = document.head.querySelectorAll('script[src="bar.js"]');
expect(scripts).toHaveLength(1);
});
it('should reject when the script fails to load', async () => {
const promise = loadScript('error.js');
const script = document.querySelector(
'script[src="error.js"]',
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 模拟加载失败
script.dispatchEvent(new Event('error'));
await expect(promise).rejects.toThrow('Failed to load script: error.js');
});
it('should handle multiple concurrent calls and only insert one script tag', async () => {
const p1 = loadScript(testJsPath);
const p2 = loadScript(testJsPath);
const script = document.querySelector(
`script[src="${testJsPath}"]`,
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 触发一次 load,两个 promise 都应该 resolve
script.dispatchEvent(new Event('load'));
await expect(p1).resolves.toBeUndefined();
await expect(p2).resolves.toBeUndefined();
// 只插入一次
const scripts = document.head.querySelectorAll(
`script[src="${testJsPath}"]`,
);
expect(scripts).toHaveLength(1);
});
});

1
packages/@core/base/shared/src/utils/index.ts

@ -7,6 +7,7 @@ export * from './inference';
export * from './letter';
export * from './merge';
export * from './nprogress';
export * from './resources';
export * from './state-handler';
export * from './to';
export * from './tree';

21
packages/@core/base/shared/src/utils/resources.ts

@ -0,0 +1,21 @@
/**
* 加载js文件
* @param src js文件地址
*/
function loadScript(src: string) {
return new Promise<void>((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
// 如果已经加载过,直接 resolve
return resolve();
}
const script = document.createElement('script');
script.src = src;
script.addEventListener('load', () => resolve());
script.addEventListener('error', () =>
reject(new Error(`Failed to load script: ${src}`)),
);
document.head.append(script);
});
}
export { loadScript };

2
packages/@core/base/shared/src/utils/window.ts

@ -30,7 +30,7 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
function openRouteInNewWindow(path: string) {
const { hash, origin } = location;
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
}

2
packages/@core/base/typings/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/@core/composables/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/@core/preferences/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/@core/ui-kit/form-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/form-ui",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

73
packages/@core/ui-kit/form-ui/src/components/form-actions.vue

@ -34,17 +34,6 @@ const submitButtonOptions = computed(() => {
// return !!unref(rootProps).showCollapseButton;
// });
const queryFormStyle = computed(() => {
if (!unref(rootProps).actionWrapperClass) {
return {
'grid-column': `-2 / -1`,
marginLeft: 'auto',
};
}
return {};
});
async function handleSubmit(e: Event) {
e?.preventDefault();
e?.stopPropagation();
@ -86,22 +75,59 @@ watch(
},
);
const actionWrapperClass = computed(() => {
const props = unref(rootProps);
const actionLayout = props.actionLayout || 'rowEnd';
const actionPosition = props.actionPosition || 'right';
const cls = [
'flex',
'items-center',
'gap-3',
props.compact ? 'pb-2' : 'pb-4',
props.layout === 'vertical' ? 'self-end' : 'self-center',
props.layout === 'inline' ? '' : 'w-full',
props.actionWrapperClass,
];
switch (actionLayout) {
case 'newLine': {
cls.push('col-span-full');
break;
}
case 'rowEnd': {
cls.push('col-[-2/-1]');
break;
}
// 'inline' 不需要额外类名,保持默认
}
switch (actionPosition) {
case 'center': {
cls.push('justify-center');
break;
}
case 'left': {
cls.push('justify-start');
break;
}
default: {
// case 'right': 默认右对齐
cls.push('justify-end');
break;
}
}
return cls.join(' ');
});
defineExpose({
handleReset,
handleSubmit,
});
</script>
<template>
<div
:class="
cn(
'col-span-full w-full text-right',
rootProps.compact ? 'pb-2' : 'pb-6',
rootProps.actionWrapperClass,
)
"
:style="queryFormStyle"
>
<div :class="cn(actionWrapperClass)">
<template v-if="rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
@ -109,7 +135,6 @@ defineExpose({
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
@ -124,7 +149,6 @@ defineExpose({
<component
:is="COMPONENT_MAP.DefaultButton"
v-if="resetButtonOptions.show"
class="ml-3"
type="button"
@click="handleReset"
v-bind="resetButtonOptions"
@ -139,7 +163,6 @@ defineExpose({
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
@ -152,9 +175,9 @@ defineExpose({
<slot name="expand-before"></slot>
<VbenExpandableArrow
class="ml-[-0.3em]"
v-if="rootProps.showCollapseButton"
v-model:model-value="collapsed"
class="ml-2"
>
<span>{{ collapsed ? $t('expand') : $t('collapse') }}</span>
</VbenExpandableArrow>

6
packages/@core/ui-kit/form-ui/src/form-render/form-field.vue

@ -59,7 +59,7 @@ const values = useFormValues();
const errors = useFieldError(fieldName);
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
const formApi = formRenderProps.form;
const compact = formRenderProps.compact;
const compact = computed(() => formRenderProps.compact);
const isInValid = computed(() => errors.value?.length > 0);
const FieldComponent = computed(() => {
@ -295,7 +295,7 @@ onUnmounted(() => {
'form-is-required': shouldRequired,
'flex-col': isVertical,
'flex-row items-center': !isVertical,
'pb-6': !compact,
'pb-4': !compact,
'pb-2': compact,
}"
class="relative flex"
@ -386,7 +386,7 @@ onUnmounted(() => {
</div>
<Transition name="slide-up" v-if="!compact">
<FormMessage class="absolute bottom-1" />
<FormMessage class="absolute" />
</Transition>
</div>
</FormItem>

32
packages/@core/ui-kit/form-ui/src/form-render/form.vue

@ -12,7 +12,12 @@ import type {
import { computed } from 'vue';
import { Form } from '@vben-core/shadcn-ui';
import { cn, isString, mergeWithArrayOverride } from '@vben-core/shared/utils';
import {
cn,
isFunction,
isString,
mergeWithArrayOverride,
} from '@vben-core/shared/utils';
import { provideFormRenderProps } from './context';
import { useExpandable } from './expandable';
@ -36,6 +41,16 @@ const emits = defineEmits<{
submit: [event: any];
}>();
const wrapperClass = computed(() => {
const cls = ['flex'];
if (props.layout === 'inline') {
cls.push('flex-wrap gap-x-2');
} else {
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
}
return cn(...cls, props.wrapperClass);
});
provideFormRenderProps(props);
const { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);
@ -110,6 +125,17 @@ const computedSchema = computed(
? keepIndex <= index
: false;
// 处理函数形式的formItemClass
let resolvedSchemaFormItemClass = schema.formItemClass;
if (isFunction(schema.formItemClass)) {
try {
resolvedSchemaFormItemClass = schema.formItemClass();
} catch (error) {
console.error('Error calling formItemClass function:', error);
resolvedSchemaFormItemClass = '';
}
}
return {
colon,
disabled,
@ -133,7 +159,7 @@ const computedSchema = computed(
'flex-shrink-0',
{ hidden },
formItemClass,
schema.formItemClass,
resolvedSchemaFormItemClass,
),
labelClass: cn(labelClass, schema.labelClass),
};
@ -144,7 +170,7 @@ const computedSchema = computed(
<template>
<component :is="formComponent" v-bind="formComponentProps">
<div ref="wrapperRef" :class="wrapperClass" class="grid">
<div ref="wrapperRef" :class="wrapperClass">
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>

15
packages/@core/ui-kit/form-ui/src/types.ts

@ -8,7 +8,7 @@ import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
import type { FormApi } from './form-api';
export type FormLayout = 'horizontal' | 'vertical';
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type BaseFormComponentType =
| 'DefaultButton'
@ -174,10 +174,10 @@ export interface FormCommonConfig {
*/
formFieldProps?: FormFieldOptions;
/**
* 所有表单项的栅格布局
* 所有表单项的栅格布局,支持函数形式
* @default ""
*/
formItemClass?: string;
formItemClass?: (() => string) | string;
/**
* 隐藏所有表单项label
* @default false
@ -354,6 +354,15 @@ export interface VbenFormProps<
* 操作按钮是否反转(提交按钮前置)
*/
actionButtonsReverse?: boolean;
/**
* 操作按钮组的样式
* newLine: 在新行显示。rowEnd: 在行内显示,靠右对齐(默认)。inline: 使用grid默认样式
*/
actionLayout?: 'inline' | 'newLine' | 'rowEnd';
/**
* 操作按钮组显示位置,默认靠右显示
*/
actionPosition?: 'center' | 'left' | 'right';
/**
* 表单操作区域class
*/

2
packages/@core/ui-kit/layout-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/@core/ui-kit/menu-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

8
packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts

@ -59,6 +59,7 @@ export class ModalApi {
showCancelButton: true,
showConfirmButton: true,
title: '',
animationType: 'slide',
};
this.store = new Store<ModalState>(
@ -106,7 +107,6 @@ export class ModalApi {
this.store.setState((prev) => ({
...prev,
isOpen: false,
submitting: false,
}));
}
}
@ -161,7 +161,11 @@ export class ModalApi {
}
open() {
this.store.setState((prev) => ({ ...prev, isOpen: true }));
this.store.setState((prev) => ({
...prev,
isOpen: true,
submitting: false,
}));
}
setData<T>(payload: T) {

5
packages/@core/ui-kit/popup-ui/src/modal/modal.ts

@ -5,6 +5,11 @@ import type { MaybePromise } from '@vben-core/typings';
import type { ModalApi } from './modal-api';
export interface ModalProps {
/**
* 动画类型
* @default 'slide'
*/
animationType?: 'scale' | 'slide';
/**
* 是否要挂载到内容区域
* @default false

6
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@ -94,12 +94,11 @@ const {
submitting,
title,
titleTooltip,
animationType,
zIndex,
} = usePriorityValues(props, state);
const shouldFullscreen = computed(
() => (fullscreen.value && header.value) || isMobile.value,
);
const shouldFullscreen = computed(() => fullscreen.value || isMobile.value);
const shouldDraggable = computed(
() => draggable.value && !shouldFullscreen.value && header.value,
@ -244,6 +243,7 @@ function handleClosed() {
:modal="modal"
:open="state?.isOpen"
:show-close="closable"
:animation-type="animationType"
:z-index="zIndex"
:overlay-blur="overlayBlur"
close-class="top-3"

2
packages/@core/ui-kit/shadcn-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.5.7",
"version": "5.5.9",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"homepage": "https://github.com/vbenjs/vue-vben-admin",

26
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

@ -8,18 +8,14 @@ import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { X } from 'lucide-vue-next';
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue';
import { DialogClose, DialogContent, useForwardPropsEmits } from 'radix-vue';
import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
DialogContentProps & {
animationType?: 'scale' | 'slide';
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
@ -31,7 +27,12 @@ const props = withDefaults(
zIndex?: number;
}
>(),
{ appendTo: 'body', closeDisabled: false, showClose: true },
{
appendTo: 'body',
animationType: 'slide',
closeDisabled: false,
showClose: true,
},
);
const emits = defineEmits<
DialogContentEmits & { close: []; closed: []; opened: [] }
@ -43,6 +44,7 @@ const delegatedProps = computed(() => {
modal: _modal,
open: _open,
showClose: __,
animationType: ___,
...delegated
} = props;
@ -80,7 +82,7 @@ defineExpose({
</script>
<template>
<DialogPortal :to="appendTo">
<Teleport defer :to="appendTo">
<Transition name="fade">
<DialogOverlay
v-if="open && modal"
@ -100,7 +102,11 @@ defineExpose({
v-bind="forwarded"
:class="
cn(
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full p-6 shadow-lg outline-none sm:rounded-xl',
{
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
animationType === 'slide',
},
props.class,
)
"
@ -121,5 +127,5 @@ defineExpose({
<X class="h-4 w-4" />
</DialogClose>
</DialogContent>
</DialogPortal>
</Teleport>
</template>

6
packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue

@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DialogContent, DialogPortal, useForwardPropsEmits } from 'radix-vue';
import { DialogContent, useForwardPropsEmits } from 'radix-vue';
import { sheetVariants } from './sheet';
import SheetOverlay from './SheetOverlay.vue';
@ -73,7 +73,7 @@ function onAnimationEnd(event: AnimationEvent) {
</script>
<template>
<DialogPortal :to="appendTo">
<Teleport defer :to="appendTo">
<Transition name="fade">
<SheetOverlay
v-if="open && modal"
@ -103,5 +103,5 @@ function onAnimationEnd(event: AnimationEvent) {
<Cross2Icon class="h-5 w-" />
</DialogClose> -->
</DialogContent>
</DialogPortal>
</Teleport>
</template>

35
packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue

@ -103,10 +103,15 @@ function updateTreeValue() {
treeValue.value = undefined;
} else {
if (Array.isArray(val)) {
const filteredValues = val.filter((v) => {
let filteredValues = val.filter((v) => {
const item = getItemByValue(v);
return item && !get(item, props.disabledField);
});
if (!props.checkStrictly && props.autoCheckParent) {
filteredValues = processParentSelection(filteredValues);
}
treeValue.value = filteredValues.map((v) => getItemByValue(v));
if (filteredValues.length !== val.length) {
@ -123,7 +128,35 @@ function updateTreeValue() {
}
}
}
function processParentSelection(
selectedValues: Array<number | string>,
): Array<number | string> {
if (props.checkStrictly) return selectedValues;
const result = [...selectedValues];
for (let i = result.length - 1; i >= 0; i--) {
const currentValue = result[i];
if (currentValue === undefined) continue;
const currentItem = getItemByValue(currentValue);
if (!currentItem) continue;
const children = get(currentItem, props.childrenField);
if (Array.isArray(children) && children.length > 0) {
const hasSelectedChildren = children.some((child) => {
const childValue = get(child, props.valueField);
return result.includes(childValue);
});
if (!hasSelectedChildren) {
result.splice(i, 1);
}
}
}
return result;
}
function updateModelValue(val: Arrayable<Recordable<any>>) {
if (Array.isArray(val)) {
const filteredVal = val.filter((v) => !get(v, props.disabledField));

2
packages/@core/ui-kit/tabs-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/tabs-ui",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/constants/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/constants",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/effects/access/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/effects/common-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

113
packages/effects/common-ui/src/ui/authentication/dingding-login.vue

@ -0,0 +1,113 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { RiDingding } from '@vben/icons';
import { $t } from '@vben/locales';
import { alert, useVbenModal } from '@vben-core/popup-ui';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { loadScript } from '@vben-core/shared/utils';
interface Props {
clientId: string;
corpId: string;
// 登录回调地址
redirectUri?: string;
// 是否内嵌二维码登录
isQrCode?: boolean;
}
const props = defineProps<Props>();
const route = useRoute();
const [Modal, modalApi] = useVbenModal({
header: false,
footer: false,
fullscreenButton: false,
class: 'w-[302px] h-[302px] dingding-qrcode-login-modal',
onOpened() {
handleQrCodeLogin();
},
});
const getRedirectUri = () => {
const { redirectUri } = props;
if (redirectUri) {
return redirectUri;
}
return window.location.origin + route.fullPath;
};
/**
* 内嵌二维码登录
*/
const handleQrCodeLogin = async () => {
const { clientId, corpId } = props;
if (!(window as any).DTFrameLogin) {
// 二维码登录 加载资源
await loadScript(
'https://g.alicdn.com/dingding/h5-dingtalk-login/0.21.0/ddlogin.js',
);
}
(window as any).DTFrameLogin(
{
id: 'dingding_qrcode_login_element',
width: 300,
height: 300,
},
{
// 注意:redirect_uri 需为完整URL,扫码后钉钉会带code跳转到这里
redirect_uri: encodeURIComponent(getRedirectUri()),
client_id: clientId,
scope: 'openid corpid',
response_type: 'code',
state: '1',
prompt: 'consent',
corpId,
},
(loginResult: any) => {
const { redirectUrl } = loginResult;
// 这里可以直接进行重定向
window.location.href = redirectUrl;
},
(errorMsg: string) => {
// 这里一般需要展示登录失败的具体原因
alert(`Login Error: ${errorMsg}`);
},
);
};
const handleLogin = () => {
const { clientId, corpId, isQrCode } = props;
if (isQrCode) {
// 内嵌二维码登录
modalApi.open();
} else {
window.location.href = `https://login.dingtalk.com/oauth2/auth?redirect_uri=${encodeURIComponent(getRedirectUri())}&response_type=code&client_id=${clientId}&scope=openid&corpid=${corpId}&prompt=consent`;
}
};
</script>
<template>
<div>
<VbenIconButton
@click="handleLogin"
:tooltip="$t('authentication.dingdingLogin')"
tooltip-side="top"
>
<RiDingding />
</VbenIconButton>
<Modal>
<div id="dingding_qrcode_login_element"></div>
</Modal>
</div>
</template>
<style>
.dingding-qrcode-login-modal {
.relative {
padding: 0 !important;
}
}
</style>

37
packages/effects/common-ui/src/ui/authentication/third-party-login.vue

@ -1,12 +1,19 @@
<script setup lang="ts">
import { useAppConfig } from '@vben/hooks';
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import DingdingLogin from './dingding-login.vue';
defineOptions({
name: 'ThirdPartyLogin',
});
const {
auth: { dingding: dingdingAuthConfig },
} = useAppConfig(import.meta.env, import.meta.env.PROD);
</script>
<template>
@ -20,18 +27,40 @@ defineOptions({
</div>
<div class="mt-4 flex flex-wrap justify-center">
<VbenIconButton class="mb-3">
<VbenIconButton
:tooltip="$t('authentication.wechatLogin')"
tooltip-side="top"
class="mb-3"
>
<MdiWechat />
</VbenIconButton>
<VbenIconButton class="mb-3">
<VbenIconButton
:tooltip="$t('authentication.qqLogin')"
tooltip-side="top"
class="mb-3"
>
<MdiQqchat />
</VbenIconButton>
<VbenIconButton class="mb-3">
<VbenIconButton
:tooltip="$t('authentication.githubLogin')"
tooltip-side="top"
class="mb-3"
>
<MdiGithub />
</VbenIconButton>
<VbenIconButton class="mb-3">
<VbenIconButton
:tooltip="$t('authentication.googleLogin')"
tooltip-side="top"
class="mb-3"
>
<MdiGoogle />
</VbenIconButton>
<DingdingLogin
v-if="dingdingAuthConfig"
:corp-id="dingdingAuthConfig.corpId"
:client-id="dingdingAuthConfig.clientId"
class="mb-3"
/>
</div>
</div>
</template>

2
packages/effects/hooks/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

17
packages/effects/hooks/src/use-app-config.ts

@ -15,9 +15,22 @@ export function useAppConfig(
? window._VBEN_ADMIN_PRO_APP_CONF_
: (env as VbenAdminProAppConfigRaw);
const { VITE_GLOB_API_URL } = config;
const {
VITE_GLOB_API_URL,
VITE_GLOB_AUTH_DINGDING_CORP_ID,
VITE_GLOB_AUTH_DINGDING_CLIENT_ID,
} = config;
return {
const applicationConfig: ApplicationConfig = {
apiURL: VITE_GLOB_API_URL,
auth: {},
};
if (VITE_GLOB_AUTH_DINGDING_CORP_ID && VITE_GLOB_AUTH_DINGDING_CLIENT_ID) {
applicationConfig.auth.dingding = {
clientId: VITE_GLOB_AUTH_DINGDING_CLIENT_ID,
corpId: VITE_GLOB_AUTH_DINGDING_CORP_ID,
};
}
return applicationConfig;
}

2
packages/effects/layouts/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/layouts",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

21
packages/effects/layouts/src/basic/layout.vue

@ -1,9 +1,11 @@
<script lang="ts" setup>
import type { SetupContext } from 'vue';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import type { MenuRecordRaw } from '@vben/types';
import { computed, useSlots, watch } from 'vue';
import { computed, onMounted, useSlots, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useRefresh } from '@vben/hooks';
import { $t, i18n } from '@vben/locales';
@ -153,6 +155,23 @@ function clickLogo() {
emit('clickLogo');
}
function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {
// 只在双列模式下生效
if (
preferences.app.layout === 'sidebar-mixed-nav' &&
route.meta &&
route.meta.hideInMenu
) {
sidebarExtraVisible.value = false;
}
}
const route = useRoute();
onMounted(() => {
autoCollapseMenuByRouteMeta(route);
});
watch(
() => preferences.app.layout,
async (val) => {

17
packages/effects/layouts/src/basic/menu/use-navigation.ts

@ -29,18 +29,29 @@ function useNavigation() {
return true;
}
const route = routeMetaMap.get(path);
return route?.meta?.openInNewWindow ?? false;
// 如果有外链或者设置了在新窗口打开,返回 true
return !!(route?.meta?.link || route?.meta?.openInNewWindow);
};
const resolveHref = (path: string): string => {
return router.resolve(path).href;
};
const navigation = async (path: string) => {
try {
const route = routeMetaMap.get(path);
const { openInNewWindow = false, query = {} } = route?.meta ?? {};
const { openInNewWindow = false, query = {}, link } = route?.meta ?? {};
// 检查是否有外链
if (link && typeof link === 'string') {
openWindow(link, { target: '_blank' });
return;
}
if (isHttpUrl(path)) {
openWindow(path, { target: '_blank' });
} else if (openInNewWindow) {
openRouteInNewWindow(path);
openRouteInNewWindow(resolveHref(path));
} else {
await router.push({
path,

2
packages/effects/layouts/src/widgets/global-search/search-panel.vue

@ -98,7 +98,7 @@ async function handleEnter() {
}
const to = result[index];
if (to) {
searchHistory.value.push(to);
searchHistory.value = uniqueByField([...searchHistory.value, to], 'path');
handleClose();
await nextTick();
if (isHttpUrl(to.path)) {

6
packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue

@ -104,7 +104,7 @@ function selectColor() {
watch(
() => [modelValue.value, props.isDark] as [BuiltinThemeType, boolean],
([themeType, isDark]) => {
([themeType, isDark], [_, isDarkPrev]) => {
const theme = builtinThemePresets.value.find(
(item) => item.type === themeType,
);
@ -113,7 +113,9 @@ watch(
? theme.darkPrimaryColor || theme.primaryColor
: theme.primaryColor;
themeColorPrimary.value = primaryColor || theme.color;
if (!(theme.type === 'custom' && isDark !== isDarkPrev)) {
themeColorPrimary.value = primaryColor || theme.color;
}
}
},
);

2
packages/effects/plugins/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/plugins",
"version": "5.5.6",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/effects/plugins/src/vxe-table/use-vxe-grid.vue

@ -300,7 +300,7 @@ async function init() {
const enableProxyConfig = options.value.proxyConfig?.enabled;
if (enableProxyConfig && autoLoad) {
props.api.grid.commitProxy?.(
'_init',
'query',
formOptions.value ? ((await formApi.getValues()) ?? {}) : {},
);
// props.api.reload(formApi.form?.values ?? {});

2
packages/effects/request/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/request",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/icons/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/icons",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/icons/src/iconify/index.ts

@ -11,3 +11,5 @@ export const MdiGithub = createIconifyIcon('mdi:github');
export const MdiGoogle = createIconifyIcon('mdi:google');
export const MdiQqchat = createIconifyIcon('mdi:qqchat');
export const RiDingding = createIconifyIcon('ri:dingding-fill');

2
packages/locales/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/locales",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

5
packages/locales/src/langs/en-US/authentication.json

@ -36,6 +36,11 @@
"qrcodeSubtitle": "Scan the QR code with your phone to login",
"qrcodePrompt": "Click 'Confirm' after scanning to complete login",
"qrcodeLogin": "QR Code Login",
"wechatLogin": "Wechat Login",
"qqLogin": "QQ Login",
"githubLogin": "Github Login",
"googleLogin": "Google Login",
"dingdingLogin": "Dingding Login",
"codeSubtitle": "Enter your phone number to start managing your project",
"code": "Security code",
"codeTip": "Security code required {0} characters",

5
packages/locales/src/langs/zh-CN/authentication.json

@ -36,6 +36,11 @@
"qrcodeSubtitle": "请用手机扫描二维码登录",
"qrcodePrompt": "扫码后点击 '确认',即可完成登录",
"qrcodeLogin": "扫码登录",
"wechatLogin": "微信登录",
"qqLogin": "QQ登录",
"githubLogin": "Github登录",
"googleLogin": "Google登录",
"dingdingLogin": "钉钉登录",
"codeSubtitle": "请输入您的手机号码以开始管理您的项目",
"code": "验证码",
"codeTip": "请输入{0}位验证码",

2
packages/preferences/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/preferences",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/stores/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/stores",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/styles/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/styles",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

10
packages/types/global.d.ts

@ -9,10 +9,20 @@ declare module 'vue-router' {
export interface VbenAdminProAppConfigRaw {
VITE_GLOB_API_URL: string;
VITE_GLOB_AUTH_DINGDING_CLIENT_ID: string;
VITE_GLOB_AUTH_DINGDING_CORP_ID: string;
}
interface AuthConfig {
dingding?: {
clientId: string;
corpId: string;
};
}
export interface ApplicationConfig {
apiURL: string;
auth: AuthConfig;
}
declare global {

2
packages/types/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/types",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
packages/utils/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/utils",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

4
playground/.env.development

@ -14,3 +14,7 @@ VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 钉钉登录配置
VITE_GLOB_AUTH_DINGDING_CLIENT_ID=应用的clientId
VITE_GLOB_AUTH_DINGDING_CORP_ID=应用的corpId

2
playground/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/playground",
"version": "5.5.7",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

129
playground/src/views/examples/form/query.vue

@ -86,6 +86,62 @@ const [QueryForm] = useVbenForm({
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
const [InlineForm] = useVbenForm({
layout: 'inline',
schema: [
{
// 组件需要在 #/adapter.ts内注册,并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入用户名',
},
// 字段名
fieldName: 'username',
// 界面显示的label
label: '字符串',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
fieldName: 'password',
label: '密码',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字(带后缀)',
suffix: () => '¥',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'options',
label: '下拉选',
},
],
});
const [QueryForm1] = useVbenForm({
// 默认展开
collapsed: true,
@ -125,6 +181,70 @@ const [QueryForm1] = useVbenForm({
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
const [QueryForm2] = useVbenForm({
// 操作按钮组 newLine: 在新行显示。rowEnd: 在行内显示,靠右对齐(默认)。inline: 使用grid默认样式
actionLayout: 'newLine',
actionPosition: 'left', // 操作按钮组在左侧显示
// 默认折叠
collapsed: true,
collapsedRows: 3,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 提交函数
handleSubmit: onSubmit,
// 垂直布局,label和input在不同行,值为vertical
// 水平布局,label和input在同一行
layout: 'vertical',
schema: [
{
// 组件需要在 #/adapter.ts内注册,并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入用户名',
},
// 字段名
fieldName: 'username',
// 界面显示的label
label: '字符串',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
fieldName: 'password',
label: '密码',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字(带后缀)',
suffix: () => '¥',
},
{
component: 'DatePicker',
fieldName: 'datePicker',
label: '日期选择框',
},
],
// 是否可展开
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
@ -140,6 +260,15 @@ function onSubmit(values: Record<string, any>) {
<Card class="mb-5" title="查询表单,默认展开">
<QueryForm />
</Card>
<Card class="mb-5" title="查询表单,单行表单">
<InlineForm />
</Card>
<Card class="mb-5" title="查询表单,默认展开,垂直布局">
<QueryForm2 />
</Card>
<Card title="查询表单,默认折叠,折叠时保留2行">
<QueryForm1 />
</Card>

2
playground/src/views/examples/vxe-table/remote.vue

@ -55,7 +55,7 @@ const gridOptions: VxeGridProps<RowType> = {
custom: true,
export: true,
// import: true,
refresh: { code: 'query' },
refresh: true,
zoom: true,
},
};

2
playground/src/views/system/dept/list.vue

@ -110,7 +110,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
refresh: true,
zoom: true,
},
treeConfig: {

2
playground/src/views/system/menu/list.vue

@ -44,7 +44,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
refresh: true,
zoom: true,
},
treeConfig: {

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

Loading…
Cancel
Save