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
*.sw?
.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">
<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

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

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

View File

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

View File

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

View File

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

View File

@ -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) => {

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) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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> = {};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +326,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` |
| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
| wrapperClass | 表单的布局基于tailwindcss | `any` | - |
| actionWrapperClass | 表单操作区域class | `any` | - |
@ -451,6 +473,8 @@ export interface FormSchema<
fieldName: string;
/** 帮助信息 */
help?: CustomRenderType;
/** 是否隐藏表单项 */
hide?: boolean;
/** 表单的标签如果是一个string会用于默认必选规则的消息提示 */
label?: CustomRenderType;
/** 自定义组件内部渲染 */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "vben-admin-monorepo",
"version": "5.5.8",
"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": {

View File

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

View File

@ -1,5 +1,5 @@
.side-content {
animation-duration: 0.2s;
animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
@ -37,7 +37,7 @@
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-10px);
transform: translateY(50px);
}
to {
@ -49,7 +49,7 @@
@keyframes slide-left {
from {
opacity: 0;
transform: translateX(-10px);
transform: translateX(-50px);
}
to {
@ -61,7 +61,7 @@
@keyframes slide-right {
from {
opacity: 0;
transform: translateX(-10px);
transform: translateX(50px);
}
to {
@ -73,7 +73,7 @@
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(10px);
transform: translateY(-50px);
}
to {
@ -85,3 +85,17 @@
.z-popup {
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",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

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

View File

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

View File

@ -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' });
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -342,13 +342,12 @@ export class FormApi {
isObject(obj[key]) &&
!isDayjsObject(obj[key]) &&
!isDate(obj[key])
? fieldMergeFn(obj[key], value)
? fieldMergeFn(value, obj[key])
: value;
}
return true;
});
const filteredFields = fieldMergeFn(fields, form.values);
this.handleStringToArrayFields(filteredFields);
form.setValues(filteredFields, shouldValidate);
}
@ -358,7 +357,6 @@ export class FormApi {
const form = await this.getForm();
await form.submitForm();
const rawValues = toRaw(await this.getValues());
this.handleArrayToStringFields(rawValues);
await this.state?.handleSubmit?.(rawValues);
return rawValues;
@ -458,16 +456,31 @@ export class FormApi {
return this.form;
}
private handleArrayToStringFields = (originValues: Record<string, any>) => {
private handleMultiFields = (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) =>
Array.isArray(value) ? value.join(sep) : value,
);
this.processFields(fields, separator, originValues, (value, sep) => {
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']
@ -503,8 +516,7 @@ export class FormApi {
const values = { ...originValues };
const fieldMappingTime = this.state?.fieldMappingTime;
this.handleStringToArrayFields(values);
this.handleMultiFields(values);
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values;
}
@ -550,65 +562,6 @@ export class FormApi {
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 = (
fields: string[],
separator: string,

View File

@ -41,6 +41,7 @@ const {
emptyStateValue,
fieldName,
formFieldProps,
hide,
label,
labelClass,
labelWidth,
@ -59,7 +60,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(() => {
@ -95,7 +96,7 @@ const currentRules = computed(() => {
});
const visible = computed(() => {
return isIf.value && isShow.value;
return !hide && isIf.value && isShow.value;
});
const shouldRequired = computed(() => {
@ -283,7 +284,7 @@ onUnmounted(() => {
<template>
<FormField
v-if="isIf"
v-if="!hide && isIf"
v-bind="fieldProps"
v-slot="slotProps"
:name="fieldName"

View File

@ -42,11 +42,11 @@ const emits = defineEmits<{
}>();
const wrapperClass = computed(() => {
const cls = ['flex flex-col'];
if (props.layout === 'vertical') {
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4');
const cls = ['flex'];
if (props.layout === 'inline') {
cls.push('flex-wrap gap-x-2');
} else {
cls.push('gap-2');
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
}
return cn(...cls, props.wrapperClass);
});
@ -170,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>

View File

@ -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'
@ -255,6 +255,8 @@ export interface FormSchema<
fieldName: string;
/** 帮助信息 */
help?: CustomRenderType;
/** 是否隐藏表单项 */
hide?: boolean;
/** 表单项 */
label?: CustomRenderType;
// 自定义组件内部渲染
@ -277,7 +279,8 @@ export interface FormRenderProps<
*/
arrayToStringFields?: ArrayToStringFields;
/**
* showCollapseButton=true下生效
* showCollapseButton=true下生效
* true: false:
*/
collapsed?: boolean;
/**

View File

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

View File

@ -10,7 +10,7 @@ import {
useLayoutFooterStyle,
useLayoutHeaderStyle,
} from '@vben-core/composables';
import { Menu } from '@vben-core/icons';
import { IconifyIcon } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
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"
@click="handleHeaderToggle"
>
<Menu class="size-4" />
<IconifyIcon v-if="showSidebar" icon="ep:fold" />
<IconifyIcon v-else icon="ep:expand" />
</VbenIconButton>
</template>
<slot name="header"></slot>

View File

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

View File

@ -107,7 +107,6 @@ export class ModalApi {
this.store.setState((prev) => ({
...prev,
isOpen: false,
submitting: false,
}));
}
}
@ -162,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) {

View File

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

View File

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

View File

@ -21,7 +21,10 @@ isFullscreen.value = !!(
);
</script>
<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" />
<Maximize v-else class="text-foreground size-4" />
</VbenIconButton>

View File

@ -59,9 +59,9 @@ function handleComplete(e: string[]) {
async function handleSend(e: Event) {
try {
e?.preventDefault();
await handleSendCode();
countdown.value = maxTime;
startCountdown();
await handleSendCode();
} catch (error) {
console.error('Failed to send code:', error);
// 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)}%`,
};
});
function activeClass(tab: string): string[] {
return tab === activeTab.value ? ['!font-bold', 'text-primary'] : [];
}
</script>
<template>
<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" />
<template v-for="tab in tabs" :key="tab.value">
<TabsTrigger
: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 }}
</TabsTrigger>

View File

@ -23,7 +23,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
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,
)
"

View File

@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
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,
)
"

View File

@ -8,12 +8,7 @@ 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';
@ -87,7 +82,7 @@ defineExpose({
</script>
<template>
<DialogPortal :to="appendTo">
<Teleport defer :to="appendTo">
<Transition name="fade">
<DialogOverlay
v-if="open && modal"
@ -132,5 +127,5 @@ defineExpose({
<X class="h-4 w-4" />
</DialogClose>
</DialogContent>
</DialogPortal>
</Teleport>
</template>

View File

@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
v-model="modelValue"
:class="
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,
)
"

View File

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

View File

@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
v-bind="delegatedProps"
:class="
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,
)
"

View File

@ -1,2 +1,4 @@
export { default as VbenTree } from './tree.vue';
export type { TreeProps } from './types';
export { treePropsDefaults } from './types';
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 { Checkbox } from '../checkbox';
import { treePropsDefaults } from './types';
const props = withDefaults(defineProps<TreeProps>(), {
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 props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
const emits = defineEmits<{
expand: [value: FlattenedItem<Recordable<any>>];
@ -41,7 +25,9 @@ const emits = defineEmits<{
interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
hasChildren: boolean;
id: P;
level: number;
parentId: null | P;
parents: P[];
value: T;
}
@ -50,24 +36,25 @@ function flatten<T = Recordable<any>, P = number | string>(
items: T[],
childrenField: string = 'children',
level = 0,
parentId: null | P = null,
parents: P[] = [],
): InnerFlattenItem<T, P>[] {
const result: InnerFlattenItem<T, P>[] = [];
items.forEach((item) => {
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,
id,
level,
parentId,
parents: [...parents],
value: item,
};
result.push(val);
if (val.hasChildren)
result.push(
...flatten(children, childrenField, level + 1, [
...parents,
get(item, props.valueField),
]),
...flatten(children, childrenField, level + 1, id, [...parents, id]),
);
});
return result;
@ -171,6 +158,24 @@ function collapseAll() {
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>>) {
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)
);
})
?.parents?.forEach((p) => {
?.parents?.filter((item) => !get(item, props.disabledField))
?.forEach((p) => {
if (Array.isArray(modelValue.value) && !modelValue.value.includes(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();
emits('select', item);
}
@ -210,6 +254,8 @@ defineExpose({
collapseNodes,
expandAll,
expandNodes,
checkAll,
unCheckAll,
expandToLevel,
getItemByValue,
});
@ -230,15 +276,41 @@ defineExpose({
v-slot="{ flattenItems }"
:class="
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,
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>
</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' : ''">
<TreeItem
v-for="item in flattenItems"
@ -250,11 +322,11 @@ defineExpose({
handleToggle,
}"
:key="item._id"
:style="{ 'padding-left': `${item.level - 0.5}rem` }"
:style="{ 'margin-left': `${item.level - 1}rem` }"
:class="
cn('cursor-pointer', getNodeClass?.(item), {
'data-[selected]:bg-accent': !multiple,
'cursor-not-allowed': isNodeDisabled(item),
'text-foreground/50 cursor-not-allowed': isNodeDisabled(item),
})
"
v-bind="
@ -284,7 +356,7 @@ defineExpose({
!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
v-if="
@ -292,7 +364,7 @@ defineExpose({
Array.isArray(item.value[childrenField]) &&
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 }"
@click.stop="
() => {
@ -301,52 +373,56 @@ defineExpose({
}
"
/>
<div v-else class="h-4 w-4">
<!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> -->
</div>
<Checkbox
v-if="multiple"
:checked="isSelected && !isNodeDisabled(item)"
:disabled="isNodeDisabled(item)"
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
<div v-else class="h-4 w-4"></div>
<div class="flex items-center gap-1">
<Checkbox
v-if="multiple"
:checked="isSelected && !isNodeDisabled(item)"
:disabled="isNodeDisabled(item)"
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
}
handleSelect();
}
handleSelect();
}
"
/>
<div
class="flex items-center gap-1 pl-2"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
"
/>
<div
class="flex items-center gap-1"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
}
handleSelect();
}
event.stopPropagation();
event.preventDefault();
handleSelect();
}
"
>
<slot name="node" v-bind="item">
<IconifyIcon
class="size-4"
v-if="showIcon && get(item.value, iconField)"
:icon="get(item.value, iconField)"
/>
{{ get(item.value, labelField) }}
</slot>
"
>
<slot name="node" v-bind="item">
<IconifyIcon
class="size-4"
v-if="showIcon && get(item.value, iconField)"
:icon="get(item.value, iconField)"
/>
{{ get(item.value, labelField) }}
</slot>
</div>
</div>
<div class="h-4 w-4"></div>
</TreeItem>
</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>
</div>
</TreeRoot>

View File

@ -40,3 +40,23 @@ export interface TreeProps {
/** 值字段 */
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",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ export * from './loading';
export * from './page';
export * from './resize';
export * from './tippy';
export * from './tree';
export * from '@vben-core/form-ui';
export * from '@vben-core/popup-ui';
@ -27,7 +28,6 @@ export {
VbenPinInput,
VbenSelect,
VbenSpinner,
VbenTree,
} 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>(() => {
if (autoContentHeight) {
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',
};
}
@ -50,7 +50,7 @@ onMounted(() => {
</script>
<template>
<div class="relative">
<div class="relative flex min-h-full flex-col">
<div
v-if="
description ||
@ -89,16 +89,10 @@ onMounted(() => {
<div :class="cn('h-full p-4', contentClass)" :style="contentStyle">
<slot></slot>
</div>
<div
v-if="$slots.footer"
ref="footerRef"
:class="
cn(
'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4',
footerClass,
)
"
:class="cn('bg-card align-center flex px-6 py-4', footerClass)"
>
<slot name="footer"></slot>
</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 按钮文本
*/
submitButtonText?: string;
/**
* @zh_CN 是否显示返回按钮
*/
showBack?: boolean;
}
defineOptions({
@ -43,6 +47,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
loading: false,
showBack: true,
loginPath: '/auth/login',
submitButtonText: '',
subTitle: '',
@ -110,7 +115,12 @@ defineExpose({
{{ submitButtonText || $t('common.login') }}
</slot>
</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') }}
</VbenButton>
</div>

View File

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

View File

@ -35,6 +35,10 @@ interface Props {
* @zh_CN 描述
*/
description?: string;
/**
* @zh_CN 是否显示返回按钮
*/
showBack?: boolean;
}
defineOptions({
@ -44,6 +48,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
description: '',
loading: false,
showBack: true,
loginPath: '/auth/login',
submitButtonText: '',
subTitle: '',
@ -88,7 +93,12 @@ function goToLogin() {
</p>
</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') }}
</VbenButton>
</div>

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<AuthenticationFormView
v-if="authPanelLeft"
class="min-h-full w-2/5 flex-1"
transition-name="slide-left"
data-side="left"
>
<template v-if="copyright" #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]"
>
<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">
<img
:alt="appName"
@ -110,6 +117,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<div class="login-background absolute left-0 top-0 size-full"></div>
<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%]"
data-side="bottom"
>
<template v-if="copyright" #copyright>
<slot name="copyright">
@ -125,7 +133,8 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<!-- 右侧认证面板 -->
<AuthenticationFormView
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>
<slot name="copyright">

View File

@ -2,6 +2,10 @@
defineOptions({
name: 'AuthenticationFormView',
});
defineProps<{
dataSide?: 'bottom' | 'left' | 'right' | 'top';
}>();
</script>
<template>
@ -16,7 +20,8 @@ defineOptions({
<component
:is="Component"
: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>
</Transition>

View File

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

View File

@ -29,7 +29,8 @@ 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 => {
@ -39,7 +40,13 @@ function useNavigation() {
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' });

View File

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

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