Merge branch 'main' into fix-downloader

This commit is contained in:
Jin Mao 2025-10-26 15:16:32 +08:00 committed by GitHub
commit 1d9cd88dd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
146 changed files with 1856 additions and 667 deletions

1
.gitignore vendored
View File

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

View File

@ -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"> <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> </a>
## Discord ## Discord

View File

@ -140,8 +140,12 @@ If you think this project is helpful to you, you can help the author buy a cup o
## Contributors ## 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"> <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> </a>
## Discord ## Discord

View File

@ -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"> <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> </a>
## Discord ## Discord

View File

@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; 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) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);

View File

@ -1,9 +1,15 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
setRefreshTokenCookie, setRefreshTokenCookie,
} from '~/utils/cookie-utils'; } from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-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) => { export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event); const { password, username } = await readBody(event);

View File

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

View File

@ -1,9 +1,11 @@
import { defineEventHandler } from 'h3';
import { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
getRefreshTokenFromCookie, getRefreshTokenFromCookie,
setRefreshTokenCookie, setRefreshTokenCookie,
} from '~/utils/cookie-utils'; } 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'; import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {

View File

@ -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) => { export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);
if (!userinfo) { if (!userinfo) {

View File

@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; 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) => { export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,11 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) { function generateMockDataList(count: number) {
const dataList = []; const dataList = [];
@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600); await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event); 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); 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) => { listData.sort((a, b) => {
if (sortOrder === 'asc') { const aValue = a[sortKey] as unknown;
if (sortBy === 'price') { const bValue = b[sortKey] as unknown;
return (
Number.parseFloat(a[sortBy as string]) - let result = 0;
Number.parseFloat(b[sortBy as string])
); 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 { } else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1; result = aValue ? 1 : -1;
} }
} else { } else {
if (sortBy === 'price') { const aStr = String(aValue);
return ( const bStr = String(bValue);
Number.parseFloat(b[sortBy as string]) - const aNum = Number(aStr);
Number.parseFloat(a[sortBy as string]) const bNum = Number(bStr);
); result =
} else { Number.isFinite(aNum) && Number.isFinite(bNum)
return a[sortBy as string] < b[sortBy as string] ? 1 : -1; ? aNum - bNum
} : aStr.localeCompare(bStr, undefined, {
} numeric: true,
sensitivity: 'base',
}); });
} }
return usePageResponseSuccess(page as string, pageSize as string, listData); return isDesc ? -result : result;
});
}
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -276,7 +276,7 @@ export const MOCK_MENU_LIST = [
children: [ children: [
{ {
id: 20_401, id: 20_401,
pid: 201, pid: 202,
name: 'SystemDeptCreate', name: 'SystemDeptCreate',
status: 1, status: 1,
type: 'button', type: 'button',
@ -285,7 +285,7 @@ export const MOCK_MENU_LIST = [
}, },
{ {
id: 20_402, id: 20_402,
pid: 201, pid: 202,
name: 'SystemDeptEdit', name: 'SystemDeptEdit',
status: 1, status: 1,
type: 'button', type: 'button',
@ -294,7 +294,7 @@ export const MOCK_MENU_LIST = [
}, },
{ {
id: 20_403, id: 20_403,
pid: 201, pid: 202,
name: 'SystemDeptDelete', name: 'SystemDeptDelete',
status: 1, status: 1,
type: 'button', type: 'button',

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import { computed, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks'; import { useWatermark } from '@vben/hooks';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
LockScreen, LockScreen,
@ -76,7 +76,7 @@ const menus = computed(() => [
target: '_blank', target: '_blank',
}); });
}, },
icon: MdiGithub, icon: SvgGithubIcon,
text: 'GitHub', text: 'GitHub',
}, },
{ {
@ -106,11 +106,16 @@ function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true)); notifications.value.forEach((item) => (item.isRead = true));
} }
watch( watch(
() => preferences.app.watermark, () => ({
async (enable) => { enable: preferences.app.watermark,
content: preferences.app.watermarkContent,
}),
async ({ enable, content }) => {
if (enable) { if (enable) {
await updateWatermark({ await updateWatermark({
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, content:
content ||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
}); });
} else { } else {
destroyWatermark(); destroyWatermark();

View File

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

View File

@ -6,7 +6,7 @@ import { computed, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks'; import { useWatermark } from '@vben/hooks';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
LockScreen, LockScreen,
@ -76,7 +76,7 @@ const menus = computed(() => [
target: '_blank', target: '_blank',
}); });
}, },
icon: MdiGithub, icon: SvgGithubIcon,
text: 'GitHub', text: 'GitHub',
}, },
{ {
@ -106,11 +106,16 @@ function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true)); notifications.value.forEach((item) => (item.isRead = true));
} }
watch( watch(
() => preferences.app.watermark, () => ({
async (enable) => { enable: preferences.app.watermark,
content: preferences.app.watermarkContent,
}),
async ({ enable, content }) => {
if (enable) { if (enable) {
await updateWatermark({ await updateWatermark({
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, content:
content ||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
}); });
} else { } else {
destroyWatermark(); destroyWatermark();

View File

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

View File

@ -6,7 +6,7 @@ import { computed, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks'; import { useWatermark } from '@vben/hooks';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
LockScreen, LockScreen,
@ -76,7 +76,7 @@ const menus = computed(() => [
target: '_blank', target: '_blank',
}); });
}, },
icon: MdiGithub, icon: SvgGithubIcon,
text: 'GitHub', text: 'GitHub',
}, },
{ {
@ -107,11 +107,16 @@ function handleMakeAll() {
} }
watch( watch(
() => preferences.app.watermark, () => ({
async (enable) => { enable: preferences.app.watermark,
content: preferences.app.watermarkContent,
}),
async ({ enable, content }) => {
if (enable) { if (enable) {
await updateWatermark({ await updateWatermark({
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, content:
content ||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
}); });
} else { } else {
destroyWatermark(); destroyWatermark();

View File

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

View File

@ -90,30 +90,52 @@ import { h } from 'vue';
import { globalShareState, IconPicker } from '@vben/common-ui'; import { globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { const AutoComplete = defineAsyncComponent(
AutoComplete, () => import('ant-design-vue/es/auto-complete'),
Button, );
Checkbox, const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
CheckboxGroup, const Checkbox = defineAsyncComponent(
DatePicker, () => import('ant-design-vue/es/checkbox'),
Divider, );
Input, const CheckboxGroup = defineAsyncComponent(() =>
InputNumber, import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
InputPassword, );
Mentions, const DatePicker = defineAsyncComponent(
notification, () => import('ant-design-vue/es/date-picker'),
Radio, );
RadioGroup, const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
RangePicker, const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
Rate, const InputNumber = defineAsyncComponent(
Select, () => import('ant-design-vue/es/input-number'),
Space, );
Switch, const InputPassword = defineAsyncComponent(() =>
Textarea, import('ant-design-vue/es/input').then((res) => res.InputPassword),
TimePicker, );
TreeSelect, const Mentions = defineAsyncComponent(
Upload, () => import('ant-design-vue/es/mentions'),
} from 'ant-design-vue'; );
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>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: T,
@ -304,7 +326,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| 属性名 | 描述 | 类型 | 默认值 | | 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` | | layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` | | showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
| wrapperClass | 表单的布局基于tailwindcss | `any` | - | | wrapperClass | 表单的布局基于tailwindcss | `any` | - |
| actionWrapperClass | 表单操作区域class | `any` | - | | actionWrapperClass | 表单操作区域class | `any` | - |
@ -451,6 +473,8 @@ export interface FormSchema<
fieldName: string; fieldName: string;
/** 帮助信息 */ /** 帮助信息 */
help?: CustomRenderType; help?: CustomRenderType;
/** 是否隐藏表单项 */
hide?: boolean;
/** 表单的标签如果是一个string会用于默认必选规则的消息提示 */ /** 表单的标签如果是一个string会用于默认必选规则的消息提示 */
label?: CustomRenderType; label?: CustomRenderType;
/** 自定义组件内部渲染 */ /** 自定义组件内部渲染 */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
.side-content { .side-content {
animation-duration: 0.2s; animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
} }
@ -37,7 +37,7 @@
@keyframes slide-down { @keyframes slide-down {
from { from {
opacity: 0; opacity: 0;
transform: translateY(-10px); transform: translateY(50px);
} }
to { to {
@ -49,7 +49,7 @@
@keyframes slide-left { @keyframes slide-left {
from { from {
opacity: 0; opacity: 0;
transform: translateX(-10px); transform: translateX(-50px);
} }
to { to {
@ -61,7 +61,7 @@
@keyframes slide-right { @keyframes slide-right {
from { from {
opacity: 0; opacity: 0;
transform: translateX(-10px); transform: translateX(50px);
} }
to { to {
@ -73,7 +73,7 @@
@keyframes slide-up { @keyframes slide-up {
from { from {
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(-50px);
} }
to { to {
@ -85,3 +85,17 @@
.z-popup { .z-popup {
z-index: var(--popup-z-index); z-index: var(--popup-z-index);
} }
@keyframes shrink {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}

View File

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

View File

@ -32,6 +32,7 @@ export {
Grip, Grip,
GripVertical, GripVertical,
Menu as IconDefault, Menu as IconDefault,
Inbox,
Info, Info,
InspectionPanel, InspectionPanel,
Languages, Languages,

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"enableCheckUpdates": true, "enableCheckUpdates": true,
"enablePreferences": true, "enablePreferences": true,
"enableRefreshToken": false, "enableRefreshToken": false,
"enableStickyPreferencesNavigationBar": true,
"isMobile": false, "isMobile": false,
"layout": "sidebar-nav", "layout": "sidebar-nav",
"locale": "zh-CN", "locale": "zh-CN",
@ -29,6 +30,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"name": "Vben Admin", "name": "Vben Admin",
"preferencesButtonPosition": "auto", "preferencesButtonPosition": "auto",
"watermark": false, "watermark": false,
"watermarkContent": "",
"zIndex": 200, "zIndex": 200,
}, },
"breadcrumb": { "breadcrumb": {

View File

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

View File

@ -1,4 +1,4 @@
import type { Preferences } from './types'; import type { Preferences } from "./types";
const defaultPreferences: Preferences = { const defaultPreferences: Preferences = {
app: { app: {
@ -22,6 +22,7 @@ const defaultPreferences: Preferences = {
enableCheckUpdates: true, enableCheckUpdates: true,
enablePreferences: true, enablePreferences: true,
enableRefreshToken: false, enableRefreshToken: false,
enableStickyPreferencesNavigationBar: true,
isMobile: false, isMobile: false,
layout: 'sidebar-nav', layout: 'sidebar-nav',
locale: 'zh-CN', locale: 'zh-CN',
@ -29,7 +30,9 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin', name: 'Vben Admin',
preferencesButtonPosition: 'auto', preferencesButtonPosition: 'auto',
watermark: false, watermark: false,
watermarkContent: '',
zIndex: 200, zIndex: 200,
}, },
breadcrumb: { breadcrumb: {
enable: true, enable: true,

View File

@ -59,6 +59,10 @@ interface AppPreferences {
* @zh_CN refreshToken * @zh_CN refreshToken
*/ */
enableRefreshToken: boolean; enableRefreshToken: boolean;
/**
* @zh_CN
*/
enableStickyPreferencesNavigationBar: boolean;
/** 是否移动端 */ /** 是否移动端 */
isMobile: boolean; isMobile: boolean;
/** 布局方式 */ /** 布局方式 */
@ -75,6 +79,10 @@ interface AppPreferences {
* @zh_CN * @zh_CN
*/ */
watermark: boolean; watermark: boolean;
/**
* @zh_CN
*/
watermarkContent: string;
/** z-index */ /** z-index */
zIndex: number; zIndex: number;
} }

View File

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

View File

@ -82,11 +82,11 @@ const actionWrapperClass = computed(() => {
const cls = [ const cls = [
'flex', 'flex',
'w-full',
'items-center', 'items-center',
'gap-3', 'gap-3',
props.compact ? 'pb-2' : 'pb-4', props.compact ? 'pb-2' : 'pb-4',
props.layout === 'vertical' ? 'self-end' : 'self-center', props.layout === 'vertical' ? 'self-end' : 'self-center',
props.layout === 'inline' ? '' : 'w-full',
props.actionWrapperClass, props.actionWrapperClass,
]; ];

View File

@ -342,13 +342,12 @@ export class FormApi {
isObject(obj[key]) && isObject(obj[key]) &&
!isDayjsObject(obj[key]) && !isDayjsObject(obj[key]) &&
!isDate(obj[key]) !isDate(obj[key])
? fieldMergeFn(obj[key], value) ? fieldMergeFn(value, obj[key])
: value; : value;
} }
return true; return true;
}); });
const filteredFields = fieldMergeFn(fields, form.values); const filteredFields = fieldMergeFn(fields, form.values);
this.handleStringToArrayFields(filteredFields);
form.setValues(filteredFields, shouldValidate); form.setValues(filteredFields, shouldValidate);
} }
@ -358,7 +357,6 @@ export class FormApi {
const form = await this.getForm(); const form = await this.getForm();
await form.submitForm(); await form.submitForm();
const rawValues = toRaw(await this.getValues()); const rawValues = toRaw(await this.getValues());
this.handleArrayToStringFields(rawValues);
await this.state?.handleSubmit?.(rawValues); await this.state?.handleSubmit?.(rawValues);
return rawValues; return rawValues;
@ -458,16 +456,31 @@ export class FormApi {
return this.form; return this.form;
} }
private handleArrayToStringFields = (originValues: Record<string, any>) => { private handleMultiFields = (originValues: Record<string, any>) => {
const arrayToStringFields = this.state?.arrayToStringFields; const arrayToStringFields = this.state?.arrayToStringFields;
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) { if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
return; return;
} }
const processFields = (fields: string[], separator: string = ',') => { const processFields = (fields: string[], separator: string = ',') => {
this.processFields(fields, separator, originValues, (value, sep) => this.processFields(fields, separator, originValues, (value, sep) => {
Array.isArray(value) ? value.join(sep) : value, if (Array.isArray(value)) {
return value.join(sep);
} else if (typeof value === 'string') {
// 处理空字符串的情况
if (value === '') {
return [];
}
// 处理复杂分隔符的情况
const escapedSeparator = sep.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
); );
return value.split(new RegExp(escapedSeparator));
} else {
return value;
}
});
}; };
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2'] // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
@ -503,8 +516,7 @@ export class FormApi {
const values = { ...originValues }; const values = { ...originValues };
const fieldMappingTime = this.state?.fieldMappingTime; const fieldMappingTime = this.state?.fieldMappingTime;
this.handleStringToArrayFields(values); this.handleMultiFields(values);
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) { if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values; return values;
} }
@ -550,65 +562,6 @@ export class FormApi {
return values; return values;
}; };
private handleStringToArrayFields = (originValues: Record<string, any>) => {
const arrayToStringFields = this.state?.arrayToStringFields;
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
return;
}
const processFields = (fields: string[], separator: string = ',') => {
this.processFields(fields, separator, originValues, (value, sep) => {
if (typeof value !== 'string') {
return value;
}
// 处理空字符串的情况
if (value === '') {
return [];
}
// 处理复杂分隔符的情况
const escapedSeparator = sep.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
);
return value.split(new RegExp(escapedSeparator));
});
};
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
if (arrayToStringFields.every((item) => typeof item === 'string')) {
const lastItem =
arrayToStringFields[arrayToStringFields.length - 1] || '';
const fields =
lastItem.length === 1
? arrayToStringFields.slice(0, -1)
: arrayToStringFields;
const separator = lastItem.length === 1 ? lastItem : ',';
processFields(fields, separator);
return;
}
// 处理嵌套数组格式 [['field1'], ';']
arrayToStringFields.forEach((fieldConfig) => {
if (Array.isArray(fieldConfig)) {
const [fields, separator = ','] = fieldConfig;
if (Array.isArray(fields)) {
processFields(fields, separator);
} else if (typeof originValues[fields] === 'string') {
const value = originValues[fields];
if (value === '') {
originValues[fields] = [];
} else {
const escapedSeparator = separator.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
);
originValues[fields] = value.split(new RegExp(escapedSeparator));
}
}
}
});
};
private processFields = ( private processFields = (
fields: string[], fields: string[],
separator: string, separator: string,

View File

@ -41,6 +41,7 @@ const {
emptyStateValue, emptyStateValue,
fieldName, fieldName,
formFieldProps, formFieldProps,
hide,
label, label,
labelClass, labelClass,
labelWidth, labelWidth,
@ -59,7 +60,7 @@ const values = useFormValues();
const errors = useFieldError(fieldName); const errors = useFieldError(fieldName);
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef'); const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
const formApi = formRenderProps.form; const formApi = formRenderProps.form;
const compact = formRenderProps.compact; const compact = computed(() => formRenderProps.compact);
const isInValid = computed(() => errors.value?.length > 0); const isInValid = computed(() => errors.value?.length > 0);
const FieldComponent = computed(() => { const FieldComponent = computed(() => {
@ -95,7 +96,7 @@ const currentRules = computed(() => {
}); });
const visible = computed(() => { const visible = computed(() => {
return isIf.value && isShow.value; return !hide && isIf.value && isShow.value;
}); });
const shouldRequired = computed(() => { const shouldRequired = computed(() => {
@ -283,7 +284,7 @@ onUnmounted(() => {
<template> <template>
<FormField <FormField
v-if="isIf" v-if="!hide && isIf"
v-bind="fieldProps" v-bind="fieldProps"
v-slot="slotProps" v-slot="slotProps"
:name="fieldName" :name="fieldName"

View File

@ -42,11 +42,11 @@ const emits = defineEmits<{
}>(); }>();
const wrapperClass = computed(() => { const wrapperClass = computed(() => {
const cls = ['flex flex-col']; const cls = ['flex'];
if (props.layout === 'vertical') { if (props.layout === 'inline') {
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4'); cls.push('flex-wrap gap-x-2');
} else { } else {
cls.push('gap-2'); cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
} }
return cn(...cls, props.wrapperClass); return cn(...cls, props.wrapperClass);
}); });
@ -170,7 +170,7 @@ const computedSchema = computed(
<template> <template>
<component :is="formComponent" v-bind="formComponentProps"> <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"> <template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass"> <!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot> <slot :definition="cSchema" :name="cSchema.fieldName"> </slot>

View File

@ -8,7 +8,7 @@ import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
import type { FormApi } from './form-api'; import type { FormApi } from './form-api';
export type FormLayout = 'horizontal' | 'vertical'; export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type BaseFormComponentType = export type BaseFormComponentType =
| 'DefaultButton' | 'DefaultButton'
@ -255,6 +255,8 @@ export interface FormSchema<
fieldName: string; fieldName: string;
/** 帮助信息 */ /** 帮助信息 */
help?: CustomRenderType; help?: CustomRenderType;
/** 是否隐藏表单项 */
hide?: boolean;
/** 表单项 */ /** 表单项 */
label?: CustomRenderType; label?: CustomRenderType;
// 自定义组件内部渲染 // 自定义组件内部渲染
@ -277,7 +279,8 @@ export interface FormRenderProps<
*/ */
arrayToStringFields?: ArrayToStringFields; arrayToStringFields?: ArrayToStringFields;
/** /**
* showCollapseButton=true下生效 * showCollapseButton=true下生效
* true: false:
*/ */
collapsed?: boolean; collapsed?: boolean;
/** /**

View File

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

View File

@ -10,7 +10,7 @@ import {
useLayoutFooterStyle, useLayoutFooterStyle,
useLayoutHeaderStyle, useLayoutHeaderStyle,
} from '@vben-core/composables'; } from '@vben-core/composables';
import { Menu } from '@vben-core/icons'; import { IconifyIcon } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui'; import { VbenIconButton } from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants'; import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
@ -559,7 +559,8 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
class="my-0 mr-1 rounded-md" class="my-0 mr-1 rounded-md"
@click="handleHeaderToggle" @click="handleHeaderToggle"
> >
<Menu class="size-4" /> <IconifyIcon v-if="showSidebar" icon="ep:fold" />
<IconifyIcon v-else icon="ep:expand" />
</VbenIconButton> </VbenIconButton>
</template> </template>
<slot name="header"></slot> <slot name="header"></slot>

View File

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

View File

@ -107,7 +107,6 @@ export class ModalApi {
this.store.setState((prev) => ({ this.store.setState((prev) => ({
...prev, ...prev,
isOpen: false, isOpen: false,
submitting: false,
})); }));
} }
} }
@ -162,7 +161,11 @@ export class ModalApi {
} }
open() { open() {
this.store.setState((prev) => ({ ...prev, isOpen: true })); this.store.setState((prev) => ({
...prev,
isOpen: true,
submitting: false,
}));
} }
setData<T>(payload: T) { setData<T>(payload: T) {

View File

@ -180,7 +180,7 @@ function escapeKeyDown(e: KeyboardEvent) {
} }
} }
function handerOpenAutoFocus(e: Event) { function handleOpenAutoFocus(e: Event) {
if (!openAutoFocus.value) { if (!openAutoFocus.value) {
e?.preventDefault(); e?.preventDefault();
} }
@ -209,6 +209,12 @@ const getForceMount = computed(() => {
return !unref(destroyOnClose) && unref(firstOpened); return !unref(destroyOnClose) && unref(firstOpened);
}); });
const handleOpened = () => {
requestAnimationFrame(() => {
props.modalApi?.onOpened();
});
};
function handleClosed() { function handleClosed() {
isClosed.value = true; isClosed.value = true;
props.modalApi?.onClosed(); props.modalApi?.onClosed();
@ -253,8 +259,8 @@ function handleClosed() {
@escape-key-down="escapeKeyDown" @escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside" @focus-outside="handleFocusOutside"
@interact-outside="interactOutside" @interact-outside="interactOutside"
@open-auto-focus="handerOpenAutoFocus" @open-auto-focus="handleOpenAutoFocus"
@opened="() => modalApi?.onOpened()" @opened="handleOpened"
@pointer-down-outside="pointerDownOutside" @pointer-down-outside="pointerDownOutside"
> >
<DialogHeader <DialogHeader

View File

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

View File

@ -21,7 +21,10 @@ isFullscreen.value = !!(
); );
</script> </script>
<template> <template>
<VbenIconButton @click="toggle"> <VbenIconButton
class="hover:animate-[shrink_0.3s_ease-in-out]"
@click="toggle"
>
<Minimize v-if="isFullscreen" class="text-foreground size-4" /> <Minimize v-if="isFullscreen" class="text-foreground size-4" />
<Maximize v-else class="text-foreground size-4" /> <Maximize v-else class="text-foreground size-4" />
</VbenIconButton> </VbenIconButton>

View File

@ -59,9 +59,9 @@ function handleComplete(e: string[]) {
async function handleSend(e: Event) { async function handleSend(e: Event) {
try { try {
e?.preventDefault(); e?.preventDefault();
await handleSendCode();
countdown.value = maxTime; countdown.value = maxTime;
startCountdown(); startCountdown();
await handleSendCode();
} catch (error) { } catch (error) {
console.error('Failed to send code:', error); console.error('Failed to send code:', error);
// Consider emitting an error event or showing a notification // Consider emitting an error event or showing a notification

View File

@ -35,16 +35,24 @@ const tabsIndicatorStyle = computed(() => {
width: `${(100 / props.tabs.length).toFixed(0)}%`, width: `${(100 / props.tabs.length).toFixed(0)}%`,
}; };
}); });
function activeClass(tab: string): string[] {
return tab === activeTab.value ? ['!font-bold', 'text-primary'] : [];
}
</script> </script>
<template> <template>
<Tabs v-model="activeTab" :default-value="getDefaultValue"> <Tabs v-model="activeTab" :default-value="getDefaultValue">
<TabsList :style="tabsStyle" class="bg-accent relative grid w-full"> <TabsList
:style="tabsStyle"
class="bg-accent !outline-heavy relative grid w-full !outline !outline-2"
>
<TabsIndicator :style="tabsIndicatorStyle" /> <TabsIndicator :style="tabsIndicatorStyle" />
<template v-for="tab in tabs" :key="tab.value"> <template v-for="tab in tabs" :key="tab.value">
<TabsTrigger <TabsTrigger
:value="tab.value" :value="tab.value"
class="z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50" :class="activeClass(tab.value)"
class="hover:text-primary z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
> >
{{ tab.label }} {{ tab.label }}
</TabsTrigger> </TabsTrigger>

View File

@ -23,7 +23,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps" v-bind="forwardedProps"
:class=" :class="
cn( cn(
'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-1 transition-[width,transform] duration-300', 'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-0.5 transition-[width,transform] duration-300',
props.class, props.class,
) )
" "

View File

@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( cn(
'focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground border-border peer h-4 w-4 shrink-0 rounded-sm border transition focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50', 'focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground border-border hover:border-primary peer h-4 w-4 shrink-0 rounded-sm border transition focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
props.class, props.class,
) )
" "

View File

@ -8,12 +8,7 @@ import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils'; import { cn } from '@vben-core/shared/utils';
import { X } from 'lucide-vue-next'; import { X } from 'lucide-vue-next';
import { import { DialogClose, DialogContent, useForwardPropsEmits } from 'radix-vue';
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue';
import DialogOverlay from './DialogOverlay.vue'; import DialogOverlay from './DialogOverlay.vue';
@ -87,7 +82,7 @@ defineExpose({
</script> </script>
<template> <template>
<DialogPortal :to="appendTo"> <Teleport defer :to="appendTo">
<Transition name="fade"> <Transition name="fade">
<DialogOverlay <DialogOverlay
v-if="open && modal" v-if="open && modal"
@ -132,5 +127,5 @@ defineExpose({
<X class="h-4 w-4" /> <X class="h-4 w-4" />
</DialogClose> </DialogClose>
</DialogContent> </DialogContent>
</DialogPortal> </Teleport>
</template> </template>

View File

@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
v-model="modelValue" v-model="modelValue"
:class=" :class="
cn( cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50', 'border-input bg-background ring-offset-background placeholder:text-muted-foreground/50 focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
props.class, props.class,
) )
" "

View File

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

View File

@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
v-bind="delegatedProps" v-bind="delegatedProps"
:class=" :class="
cn( cn(
'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1', 'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-md p-1',
props.class, props.class,
) )
" "

View File

@ -1,2 +1,4 @@
export { default as VbenTree } from './tree.vue'; export { default as VbenTree } from './tree.vue';
export type { TreeProps } from './types';
export { treePropsDefaults } from './types';
export type { FlattenedItem } from 'radix-vue'; export type { FlattenedItem } from 'radix-vue';

View File

@ -14,25 +14,9 @@ import { cn, get } from '@vben-core/shared/utils';
import { TreeItem, TreeRoot } from 'radix-vue'; import { TreeItem, TreeRoot } from 'radix-vue';
import { Checkbox } from '../checkbox'; import { Checkbox } from '../checkbox';
import { treePropsDefaults } from './types';
const props = withDefaults(defineProps<TreeProps>(), { const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
allowClear: false,
autoCheckParent: true,
bordered: false,
checkStrictly: false,
defaultExpandedKeys: () => [],
defaultExpandedLevel: 0,
disabled: false,
disabledField: 'disabled',
expanded: () => [],
iconField: 'icon',
labelField: 'label',
multiple: false,
showIcon: true,
transition: true,
valueField: 'value',
childrenField: 'children',
});
const emits = defineEmits<{ const emits = defineEmits<{
expand: [value: FlattenedItem<Recordable<any>>]; expand: [value: FlattenedItem<Recordable<any>>];
@ -41,7 +25,9 @@ const emits = defineEmits<{
interface InnerFlattenItem<T = Recordable<any>, P = number | string> { interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
hasChildren: boolean; hasChildren: boolean;
id: P;
level: number; level: number;
parentId: null | P;
parents: P[]; parents: P[];
value: T; value: T;
} }
@ -50,24 +36,25 @@ function flatten<T = Recordable<any>, P = number | string>(
items: T[], items: T[],
childrenField: string = 'children', childrenField: string = 'children',
level = 0, level = 0,
parentId: null | P = null,
parents: P[] = [], parents: P[] = [],
): InnerFlattenItem<T, P>[] { ): InnerFlattenItem<T, P>[] {
const result: InnerFlattenItem<T, P>[] = []; const result: InnerFlattenItem<T, P>[] = [];
items.forEach((item) => { items.forEach((item) => {
const children = get(item, childrenField) as Array<T>; const children = get(item, childrenField) as Array<T>;
const val = { const id = get(item, props.valueField) as P;
const val: InnerFlattenItem<T, P> = {
hasChildren: Array.isArray(children) && children.length > 0, hasChildren: Array.isArray(children) && children.length > 0,
id,
level, level,
parentId,
parents: [...parents], parents: [...parents],
value: item, value: item,
}; };
result.push(val); result.push(val);
if (val.hasChildren) if (val.hasChildren)
result.push( result.push(
...flatten(children, childrenField, level + 1, [ ...flatten(children, childrenField, level + 1, id, [...parents, id]),
...parents,
get(item, props.valueField),
]),
); );
}); });
return result; return result;
@ -171,6 +158,24 @@ function collapseAll() {
expanded.value = []; expanded.value = [];
} }
function checkAll() {
if (!props.multiple) return;
modelValue.value = [
...new Set(
flattenData.value
.filter((item) => !get(item.value, props.disabledField))
.map((item) => get(item.value, props.valueField)),
),
];
updateTreeValue();
}
function unCheckAll() {
if (!props.multiple) return;
modelValue.value = [];
updateTreeValue();
}
function isNodeDisabled(item: FlattenedItem<Recordable<any>>) { function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
return props.disabled || get(item.value, props.disabledField); return props.disabled || get(item.value, props.disabledField);
} }
@ -195,12 +200,51 @@ function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
get(i.value, props.valueField) === get(item.value, props.valueField) get(i.value, props.valueField) === get(item.value, props.valueField)
); );
}) })
?.parents?.forEach((p) => { ?.parents?.filter((item) => !get(item, props.disabledField))
?.forEach((p) => {
if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) { if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) {
modelValue.value.push(p); modelValue.value.push(p);
} }
}); });
} }
if (
!props.checkStrictly &&
props.multiple &&
props.autoCheckParent &&
!isSelected
) {
flattenData.value
.find((i) => {
return (
get(i.value, props.valueField) === get(item.value, props.valueField)
);
})
?.parents?.filter((item) => !get(item, props.disabledField))
?.reverse()
.forEach((p) => {
const children = flattenData.value.filter((i) => {
return (
i.parents.length > 0 &&
i.parents.includes(p) &&
i.id !== item._id &&
i.parentId === p
);
});
if (Array.isArray(modelValue.value)) {
const hasSelectedChild = children.some((child) =>
(modelValue.value as unknown[]).includes(
get(child.value, props.valueField),
),
);
if (!hasSelectedChild) {
const index = modelValue.value.indexOf(p);
if (index !== -1) {
modelValue.value.splice(index, 1);
}
}
}
});
}
updateTreeValue(); updateTreeValue();
emits('select', item); emits('select', item);
} }
@ -210,6 +254,8 @@ defineExpose({
collapseNodes, collapseNodes,
expandAll, expandAll,
expandNodes, expandNodes,
checkAll,
unCheckAll,
expandToLevel, expandToLevel,
getItemByValue, getItemByValue,
}); });
@ -230,15 +276,41 @@ defineExpose({
v-slot="{ flattenItems }" v-slot="{ flattenItems }"
:class=" :class="
cn( cn(
'text-blackA11 container select-none list-none rounded-lg p-2 text-sm font-medium', 'text-blackA11 container select-none list-none rounded-lg text-sm font-medium',
$attrs.class as unknown as ClassType, $attrs.class as unknown as ClassType,
bordered ? 'border' : '', bordered ? 'border' : '',
) )
" "
> >
<div class="w-full" v-if="$slots.header"> <div
:class="
cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')
"
v-if="$slots.header"
>
<slot name="header"> </slot> <slot name="header"> </slot>
</div> </div>
<div
:class="
cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')
"
v-if="treeData.length > 0"
>
<div
class="flex size-5 flex-1 cursor-pointer items-center"
@click="() => (expanded?.length > 0 ? collapseAll() : expandAll())"
>
<ChevronRight
:class="{ 'rotate-90': expanded?.length > 0 }"
class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
/>
<Checkbox
v-if="multiple"
@click.stop
@update:checked="(checked) => (checked ? checkAll() : unCheckAll())"
/>
</div>
</div>
<TransitionGroup :name="transition ? 'fade' : ''"> <TransitionGroup :name="transition ? 'fade' : ''">
<TreeItem <TreeItem
v-for="item in flattenItems" v-for="item in flattenItems"
@ -250,11 +322,11 @@ defineExpose({
handleToggle, handleToggle,
}" }"
:key="item._id" :key="item._id"
:style="{ 'padding-left': `${item.level - 0.5}rem` }" :style="{ 'margin-left': `${item.level - 1}rem` }"
:class=" :class="
cn('cursor-pointer', getNodeClass?.(item), { cn('cursor-pointer', getNodeClass?.(item), {
'data-[selected]:bg-accent': !multiple, 'data-[selected]:bg-accent': !multiple,
'cursor-not-allowed': isNodeDisabled(item), 'text-foreground/50 cursor-not-allowed': isNodeDisabled(item),
}) })
" "
v-bind=" v-bind="
@ -284,7 +356,7 @@ defineExpose({
!isNodeDisabled(item) && onToggle(item); !isNodeDisabled(item) && onToggle(item);
} }
" "
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2" class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded p-1 outline-none focus:ring-2"
> >
<ChevronRight <ChevronRight
v-if=" v-if="
@ -292,7 +364,7 @@ defineExpose({
Array.isArray(item.value[childrenField]) && Array.isArray(item.value[childrenField]) &&
item.value[childrenField].length > 0 item.value[childrenField].length > 0
" "
class="size-4 cursor-pointer transition" class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
:class="{ 'rotate-90': isExpanded }" :class="{ 'rotate-90': isExpanded }"
@click.stop=" @click.stop="
() => { () => {
@ -301,9 +373,8 @@ defineExpose({
} }
" "
/> />
<div v-else class="h-4 w-4"> <div v-else class="h-4 w-4"></div>
<!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> --> <div class="flex items-center gap-1">
</div>
<Checkbox <Checkbox
v-if="multiple" v-if="multiple"
:checked="isSelected && !isNodeDisabled(item)" :checked="isSelected && !isNodeDisabled(item)"
@ -321,7 +392,7 @@ defineExpose({
" "
/> />
<div <div
class="flex items-center gap-1 pl-2" class="flex items-center gap-1"
@click=" @click="
(event: MouseEvent) => { (event: MouseEvent) => {
if (isNodeDisabled(item)) { if (isNodeDisabled(item)) {
@ -329,8 +400,6 @@ defineExpose({
event.stopPropagation(); event.stopPropagation();
return; return;
} }
event.stopPropagation();
event.preventDefault();
handleSelect(); handleSelect();
} }
" "
@ -344,9 +413,16 @@ defineExpose({
{{ get(item.value, labelField) }} {{ get(item.value, labelField) }}
</slot> </slot>
</div> </div>
</div>
<div class="h-4 w-4"></div>
</TreeItem> </TreeItem>
</TransitionGroup> </TransitionGroup>
<div class="w-full" v-if="$slots.footer"> <div
:class="
cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-t' : '')
"
v-if="$slots.footer"
>
<slot name="footer"> </slot> <slot name="footer"> </slot>
</div> </div>
</TreeRoot> </TreeRoot>

View File

@ -40,3 +40,23 @@ export interface TreeProps {
/** 值字段 */ /** 值字段 */
valueField?: string; valueField?: string;
} }
export function treePropsDefaults() {
return {
allowClear: false,
autoCheckParent: true,
bordered: false,
checkStrictly: false,
defaultExpandedKeys: () => [],
defaultExpandedLevel: 0,
disabled: false,
disabledField: 'disabled',
iconField: 'icon',
labelField: 'label',
multiple: false,
showIcon: true,
transition: true,
valueField: 'value',
childrenField: 'children',
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ export * from './loading';
export * from './page'; export * from './page';
export * from './resize'; export * from './resize';
export * from './tippy'; export * from './tippy';
export * from './tree';
export * from '@vben-core/form-ui'; export * from '@vben-core/form-ui';
export * from '@vben-core/popup-ui'; export * from '@vben-core/popup-ui';
@ -27,7 +28,6 @@ export {
VbenPinInput, VbenPinInput,
VbenSelect, VbenSelect,
VbenSpinner, VbenSpinner,
VbenTree,
} from '@vben-core/shadcn-ui'; } from '@vben-core/shadcn-ui';
export type { FlattenedItem } from '@vben-core/shadcn-ui'; export type { FlattenedItem } from '@vben-core/shadcn-ui';

View File

@ -25,7 +25,7 @@ const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
const contentStyle = computed<StyleValue>(() => { const contentStyle = computed<StyleValue>(() => {
if (autoContentHeight) { if (autoContentHeight) {
return { return {
height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`, height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${footerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,
overflowY: shouldAutoHeight.value ? 'auto' : 'unset', overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
}; };
} }
@ -50,7 +50,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="relative"> <div class="relative flex min-h-full flex-col">
<div <div
v-if=" v-if="
description || description ||
@ -89,16 +89,10 @@ onMounted(() => {
<div :class="cn('h-full p-4', contentClass)" :style="contentStyle"> <div :class="cn('h-full p-4', contentClass)" :style="contentStyle">
<slot></slot> <slot></slot>
</div> </div>
<div <div
v-if="$slots.footer" v-if="$slots.footer"
ref="footerRef" ref="footerRef"
:class=" :class="cn('bg-card align-center flex px-6 py-4', footerClass)"
cn(
'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4',
footerClass,
)
"
> >
<slot name="footer"></slot> <slot name="footer"></slot>
</div> </div>

View File

@ -0,0 +1 @@
export { default as Tree } from './tree.vue';

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { TreeProps } from '@vben-core/shadcn-ui';
import { Inbox } from '@vben/icons';
import { $t } from '@vben/locales';
import { treePropsDefaults, VbenTree } from '@vben-core/shadcn-ui';
const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
</script>
<template>
<VbenTree v-if="props.treeData?.length > 0" v-bind="props">
<template v-for="(_, key) in $slots" :key="key" #[key]="slotProps">
<slot :name="key" v-bind="slotProps"> </slot>
</template>
</VbenTree>
<div
v-else
class="flex-col-center text-muted-foreground cursor-pointer rounded-lg border p-10 text-sm font-medium"
>
<Inbox class="size-10" />
<div class="mt-1">{{ $t('common.noData') }}</div>
</div>
</template>

View File

@ -35,6 +35,10 @@ interface Props {
* @zh_CN 按钮文本 * @zh_CN 按钮文本
*/ */
submitButtonText?: string; submitButtonText?: string;
/**
* @zh_CN 是否显示返回按钮
*/
showBack?: boolean;
} }
defineOptions({ defineOptions({
@ -43,6 +47,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
loading: false, loading: false,
showBack: true,
loginPath: '/auth/login', loginPath: '/auth/login',
submitButtonText: '', submitButtonText: '',
subTitle: '', subTitle: '',
@ -110,7 +115,12 @@ defineExpose({
{{ submitButtonText || $t('common.login') }} {{ submitButtonText || $t('common.login') }}
</slot> </slot>
</VbenButton> </VbenButton>
<VbenButton class="mt-4 w-full" variant="outline" @click="goToLogin()"> <VbenButton
v-if="showBack"
class="mt-4 w-full"
variant="outline"
@click="goToLogin()"
>
{{ $t('common.back') }} {{ $t('common.back') }}
</VbenButton> </VbenButton>
</div> </div>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { RiDingding } from '@vben/icons'; import { SvgDingDingIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { alert, useVbenModal } from '@vben-core/popup-ui'; import { alert, useVbenModal } from '@vben-core/popup-ui';
@ -96,7 +96,7 @@ const handleLogin = () => {
:tooltip="$t('authentication.dingdingLogin')" :tooltip="$t('authentication.dingdingLogin')"
tooltip-side="top" tooltip-side="top"
> >
<RiDingding /> <SvgDingDingIcon />
</VbenIconButton> </VbenIconButton>
<Modal> <Modal>
<div id="dingding_qrcode_login_element"></div> <div id="dingding_qrcode_login_element"></div>

View File

@ -35,6 +35,10 @@ interface Props {
* @zh_CN 描述 * @zh_CN 描述
*/ */
description?: string; description?: string;
/**
* @zh_CN 是否显示返回按钮
*/
showBack?: boolean;
} }
defineOptions({ defineOptions({
@ -44,6 +48,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
description: '', description: '',
loading: false, loading: false,
showBack: true,
loginPath: '/auth/login', loginPath: '/auth/login',
submitButtonText: '', submitButtonText: '',
subTitle: '', subTitle: '',
@ -88,7 +93,12 @@ function goToLogin() {
</p> </p>
</div> </div>
<VbenButton class="mt-4 w-full" variant="outline" @click="goToLogin()"> <VbenButton
v-if="showBack"
class="mt-4 w-full"
variant="outline"
@click="goToLogin()"
>
{{ $t('common.back') }} {{ $t('common.back') }}
</VbenButton> </VbenButton>
</div> </div>

View File

@ -1,6 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben/icons'; import {
SvgGithubIcon,
SvgGoogleIcon,
SvgQQChatIcon,
SvgWeChatIcon,
} from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { VbenIconButton } from '@vben-core/shadcn-ui'; import { VbenIconButton } from '@vben-core/shadcn-ui';
@ -32,28 +37,28 @@ const {
tooltip-side="top" tooltip-side="top"
class="mb-3" class="mb-3"
> >
<MdiWechat /> <SvgWeChatIcon />
</VbenIconButton> </VbenIconButton>
<VbenIconButton <VbenIconButton
:tooltip="$t('authentication.qqLogin')" :tooltip="$t('authentication.qqLogin')"
tooltip-side="top" tooltip-side="top"
class="mb-3" class="mb-3"
> >
<MdiQqchat /> <SvgQQChatIcon />
</VbenIconButton> </VbenIconButton>
<VbenIconButton <VbenIconButton
:tooltip="$t('authentication.githubLogin')" :tooltip="$t('authentication.githubLogin')"
tooltip-side="top" tooltip-side="top"
class="mb-3" class="mb-3"
> >
<MdiGithub /> <SvgGithubIcon />
</VbenIconButton> </VbenIconButton>
<VbenIconButton <VbenIconButton
:tooltip="$t('authentication.googleLogin')" :tooltip="$t('authentication.googleLogin')"
tooltip-side="top" tooltip-side="top"
class="mb-3" class="mb-3"
> >
<MdiGoogle /> <SvgGoogleIcon />
</VbenIconButton> </VbenIconButton>
<DingdingLogin <DingdingLogin
v-if="dingdingAuthConfig" v-if="dingdingAuthConfig"

View File

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

View File

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

View File

@ -50,7 +50,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<AuthenticationFormView <AuthenticationFormView
v-if="authPanelLeft" v-if="authPanelLeft"
class="min-h-full w-2/5 flex-1" class="min-h-full w-2/5 flex-1"
transition-name="slide-left" data-side="left"
> >
<template v-if="copyright" #copyright> <template v-if="copyright" #copyright>
<slot name="copyright"> <slot name="copyright">
@ -86,7 +86,14 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
class="bg-background-deep absolute inset-0 h-full w-full dark:bg-[#070709]" class="bg-background-deep absolute inset-0 h-full w-full dark:bg-[#070709]"
> >
<div class="login-background absolute left-0 top-0 size-full"></div> <div class="login-background absolute left-0 top-0 size-full"></div>
<div class="flex-col-center -enter-x mr-20 h-full"> <div
:key="authPanelLeft ? 'left' : authPanelRight ? 'right' : 'center'"
class="flex-col-center mr-20 h-full"
:class="{
'enter-x': authPanelLeft,
'-enter-x': authPanelRight,
}"
>
<template v-if="sloganImage"> <template v-if="sloganImage">
<img <img
:alt="appName" :alt="appName"
@ -110,6 +117,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<div class="login-background absolute left-0 top-0 size-full"></div> <div class="login-background absolute left-0 top-0 size-full"></div>
<AuthenticationFormView <AuthenticationFormView
class="md:bg-background shadow-primary/5 shadow-float w-full rounded-3xl pb-20 md:w-2/3 lg:w-1/2 xl:w-[36%]" class="md:bg-background shadow-primary/5 shadow-float w-full rounded-3xl pb-20 md:w-2/3 lg:w-1/2 xl:w-[36%]"
data-side="bottom"
> >
<template v-if="copyright" #copyright> <template v-if="copyright" #copyright>
<slot name="copyright"> <slot name="copyright">
@ -125,7 +133,8 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<!-- 右侧认证面板 --> <!-- 右侧认证面板 -->
<AuthenticationFormView <AuthenticationFormView
v-if="authPanelRight" v-if="authPanelRight"
class="min-h-full w-[34%] flex-1" class="min-h-full w-2/5 flex-1"
data-side="right"
> >
<template v-if="copyright" #copyright> <template v-if="copyright" #copyright>
<slot name="copyright"> <slot name="copyright">

View File

@ -2,6 +2,10 @@
defineOptions({ defineOptions({
name: 'AuthenticationFormView', name: 'AuthenticationFormView',
}); });
defineProps<{
dataSide?: 'bottom' | 'left' | 'right' | 'top';
}>();
</script> </script>
<template> <template>
@ -16,7 +20,8 @@ defineOptions({
<component <component
:is="Component" :is="Component"
:key="route.fullPath" :key="route.fullPath"
class="enter-x mt-6 w-full sm:mx-auto md:max-w-md" class="side-content mt-6 w-full sm:mx-auto md:max-w-md"
:data-side="dataSide"
/> />
</KeepAlive> </KeepAlive>
</Transition> </Transition>

View File

@ -158,7 +158,9 @@ function clickLogo() {
function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) { function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {
// //
if ( if (
preferences.app.layout === 'sidebar-mixed-nav' && ['header-mixed-nav', 'sidebar-mixed-nav'].includes(
preferences.app.layout,
) &&
route.meta && route.meta &&
route.meta.hideInMenu route.meta.hideInMenu
) { ) {

View File

@ -29,7 +29,8 @@ function useNavigation() {
return true; return true;
} }
const route = routeMetaMap.get(path); const route = routeMetaMap.get(path);
return route?.meta?.openInNewWindow ?? false; // 如果有外链或者设置了在新窗口打开,返回 true
return !!(route?.meta?.link || route?.meta?.openInNewWindow);
}; };
const resolveHref = (path: string): string => { const resolveHref = (path: string): string => {
@ -39,7 +40,13 @@ function useNavigation() {
const navigation = async (path: string) => { const navigation = async (path: string) => {
try { try {
const route = routeMetaMap.get(path); 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)) { if (isHttpUrl(path)) {
openWindow(path, { target: '_blank' }); openWindow(path, { target: '_blank' });

View File

@ -31,7 +31,7 @@ async function handleUpdate(value: string | undefined) {
:model-value="preferences.app.locale" :model-value="preferences.app.locale"
@update:model-value="handleUpdate" @update:model-value="handleUpdate"
> >
<VbenIconButton> <VbenIconButton class="hover:animate-[shrink_0.3s_ease-in-out]">
<Languages class="text-foreground size-4" /> <Languages class="text-foreground size-4" />
</VbenIconButton> </VbenIconButton>
</VbenDropdownRadioMenu> </VbenDropdownRadioMenu>

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