mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 00:26:20 +08:00
Compare commits
83 Commits
alova
...
electron-v
Author | SHA1 | Date | |
---|---|---|---|
![]() |
67071326a1 | ||
![]() |
30387b94a2 | ||
![]() |
643f25347b | ||
![]() |
49a739ffc6 | ||
![]() |
128e749702 | ||
![]() |
c91fd146b8 | ||
![]() |
74874a88a6 | ||
![]() |
f0f890c583 | ||
![]() |
ea48a01d8f | ||
![]() |
9311c658a1 | ||
![]() |
12151d9742 | ||
![]() |
de655af23b | ||
![]() |
6fe909bfba | ||
![]() |
899a99535f | ||
![]() |
29bdf2d13b | ||
![]() |
4f633c1a9a | ||
![]() |
b2951b2975 | ||
![]() |
d9f8722c27 | ||
![]() |
1539fc738c | ||
![]() |
200859fc7e | ||
![]() |
36fb6b2222 | ||
![]() |
3304d54546 | ||
![]() |
ca57d223b4 | ||
![]() |
0fa587c246 | ||
![]() |
4fd2fee782 | ||
![]() |
c4a4ca5232 | ||
![]() |
73db7f3ef5 | ||
![]() |
aef2134b60 | ||
![]() |
f920895b8c | ||
![]() |
10257e3611 | ||
![]() |
a88a2c8afa | ||
![]() |
d92697e7f7 | ||
![]() |
e34285a51e | ||
![]() |
721b5f3d3e | ||
![]() |
5e694b3194 | ||
![]() |
bf5e697c42 | ||
![]() |
9437e97ce4 | ||
![]() |
77c09b4ddf | ||
![]() |
70d07c683a | ||
![]() |
e840905934 | ||
![]() |
8b192d05fd | ||
![]() |
9f910c3486 | ||
![]() |
25b0822afd | ||
![]() |
8292024fc7 | ||
![]() |
9a0e813143 | ||
![]() |
1a73329691 | ||
![]() |
ab161444fc | ||
![]() |
0243e68fbe | ||
![]() |
54c0d4ed83 | ||
![]() |
e320311128 | ||
![]() |
1faa52335c | ||
![]() |
9ff600657b | ||
![]() |
d14527a292 | ||
![]() |
eb70c48d14 | ||
![]() |
302de1deaf | ||
![]() |
ea0c2ad58b | ||
![]() |
678646b4ba | ||
![]() |
a4da6dedf8 | ||
![]() |
332a8be29c | ||
![]() |
357ae0e565 | ||
![]() |
fe2310300c | ||
![]() |
7e99c62abf | ||
![]() |
a3b2d5c2e1 | ||
![]() |
39f9c6a3f4 | ||
![]() |
48c616ea81 | ||
![]() |
06e279c917 | ||
![]() |
ff65b061ef | ||
![]() |
b8f145aa7f | ||
![]() |
091596eda8 | ||
![]() |
17b0111424 | ||
![]() |
6e5ddb6d25 | ||
![]() |
46a10959c8 | ||
![]() |
382df99988 | ||
![]() |
d00b6bb83e | ||
![]() |
82a05e3b3a | ||
![]() |
d3d1e1616c | ||
![]() |
a4682a7116 | ||
![]() |
a8935aaf8c | ||
![]() |
a6b480fb8f | ||
![]() |
a87fa3ffbe | ||
![]() |
d450df946c | ||
![]() |
20f902ed2c | ||
![]() |
7536be5e49 |
2
.github/contributing.md
vendored
2
.github/contributing.md
vendored
@@ -19,9 +19,11 @@ Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
|
||||
|
||||
- If adding a new feature:
|
||||
|
||||
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
|
||||
|
||||
- If fixing bug:
|
||||
|
||||
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
||||
|
||||
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,7 @@ dist-ssr
|
||||
dist.zip
|
||||
dist.tar
|
||||
dist.war
|
||||
dist-electron
|
||||
.nitro
|
||||
.output
|
||||
*-dist.zip
|
||||
@@ -49,4 +50,3 @@ vite.config.ts.*
|
||||
*.sln
|
||||
*.sw?
|
||||
.history
|
||||
.cursor
|
||||
|
3
.npmrc
3
.npmrc
@@ -1,5 +1,8 @@
|
||||
registry = "https://registry.npmmirror.com"
|
||||
ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
|
||||
public-hoist-pattern[]=lefthook
|
||||
ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
|
||||
public-hoist-pattern[]=husky
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||
|
@@ -140,12 +140,8 @@ 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://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -140,12 +140,8 @@ 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://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -140,12 +140,8 @@ 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://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_CODES } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
@@ -1,15 +1,9 @@
|
||||
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_USERS } from '~/utils/mock-data';
|
||||
import {
|
||||
forbiddenResponse,
|
||||
useResponseError,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
import { forbiddenResponse } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { password, username } = await readBody(event);
|
||||
|
@@ -1,9 +1,7 @@
|
||||
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);
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
getRefreshTokenFromCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_USERS } from '~/utils/mock-data';
|
||||
import { forbiddenResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { verifyRefreshToken } from '~/utils/jwt-utils';
|
||||
import { forbiddenResponse } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const refreshToken = getRefreshTokenFromCookie(event);
|
||||
@@ -30,7 +28,6 @@ export default defineEventHandler(async (event) => {
|
||||
const accessToken = generateAccessToken(findUser);
|
||||
|
||||
setRefreshTokenCookie(event, refreshToken);
|
||||
return useResponseSuccess({
|
||||
accessToken,
|
||||
});
|
||||
|
||||
return accessToken;
|
||||
});
|
||||
|
@@ -1,32 +0,0 @@
|
||||
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) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
const data = `
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 123456789012345678901234567890123456789012345678901234567890,
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"email": "john-doe@demo.com"
|
||||
},
|
||||
{
|
||||
"id": 987654321098765432109876543210987654321098765432109876543210,
|
||||
"name": "Jane Smith",
|
||||
"age": 25,
|
||||
"email": "jane@demo.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
setHeader(event, 'Content-Type', 'application/json');
|
||||
return data;
|
||||
});
|
@@ -1,7 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENUS } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
@@ -1,6 +1,3 @@
|
||||
import { eventHandler, getQuery, setResponseStatus } from 'h3';
|
||||
import { useResponseError } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const { status } = getQuery(event);
|
||||
setResponseStatus(event, Number(status));
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { eventHandler, getQuery } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const namesMap: Record<string, any> = {};
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { eventHandler, getQuery } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const pathMap: Record<string, any> = { '/': 0 };
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
|
@@ -1,11 +1,6 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { eventHandler, getQuery } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
usePageResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
@@ -49,69 +44,30 @@ 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);
|
||||
|
||||
// 规范化 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';
|
||||
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
|
||||
listData.sort((a, b) => {
|
||||
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;
|
||||
if (sortOrder === 'asc') {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(a[sortBy as string]) -
|
||||
Number.parseFloat(b[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
result = aValue ? 1 : -1;
|
||||
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
} else {
|
||||
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',
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return isDesc ? -result : result;
|
||||
});
|
||||
}
|
||||
|
||||
return usePageResponseSuccess(
|
||||
String(pageNumber),
|
||||
String(pageSizeNumber),
|
||||
listData,
|
||||
);
|
||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||
});
|
||||
|
@@ -1,3 +1 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
|
||||
export default defineEventHandler(() => 'Test get handler');
|
||||
|
@@ -1,3 +1 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
|
||||
export default defineEventHandler(() => 'Test post handler');
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
import { forbiddenResponse, sleep } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
return `
|
||||
<h1>Hello Vben Admin</h1>
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
import { deleteCookie, getCookie, setCookie } from 'h3';
|
||||
|
||||
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
|
||||
deleteCookie(event, 'jwt', {
|
||||
httpOnly: true,
|
||||
|
@@ -1,11 +1,8 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
import type { UserInfo } from './mock-data';
|
||||
|
||||
import { getHeader } from 'h3';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { MOCK_USERS } from './mock-data';
|
||||
import { UserInfo } from './mock-data';
|
||||
|
||||
// TODO: Replace with your own secret key
|
||||
const ACCESS_TOKEN_SECRET = 'access_token_secret';
|
||||
@@ -34,22 +31,12 @@ export function verifyAccessToken(
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenParts = authHeader.split(' ');
|
||||
if (tokenParts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
const token = tokenParts[1] as string;
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(
|
||||
token,
|
||||
ACCESS_TOKEN_SECRET,
|
||||
) as unknown as UserPayload;
|
||||
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) 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 {
|
||||
@@ -63,12 +50,7 @@ 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,
|
||||
) as UserInfo;
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const user = MOCK_USERS.find((item) => item.username === username);
|
||||
const { password: _pwd, ...userinfo } = user;
|
||||
return userinfo;
|
||||
} catch {
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
import { setResponseStatus } from 'h3';
|
||||
|
||||
export function useResponseSuccess<T = any>(data: T) {
|
||||
return {
|
||||
code: 0,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -8,7 +8,13 @@ import type { Component } from 'vue';
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -76,15 +82,16 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
@@ -8,42 +8,40 @@ import type { ComponentType } from './component';
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
export { useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
@@ -35,7 +33,7 @@ setupVbenVxeTable({
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
|
@@ -12,7 +12,6 @@ import { useTitle } from '@vueuse/core';
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
@@ -20,9 +19,6 @@ async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -8,7 +8,13 @@ import type { Component } from 'vue';
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -133,15 +139,16 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
@@ -8,34 +8,32 @@ import type { ComponentType } from './component';
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
modelPropNameMap: {
|
||||
Upload: 'fileList',
|
||||
CheckboxGroup: 'model-value',
|
||||
},
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
modelPropNameMap: {
|
||||
Upload: 'fileList',
|
||||
CheckboxGroup: 'model-value',
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
export { useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
@@ -35,7 +33,7 @@ setupVbenVxeTable({
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
|
@@ -13,17 +13,12 @@ import { ElLoading } from 'element-plus';
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -8,7 +8,13 @@ import type { Component } from 'vue';
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -79,15 +85,16 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
@@ -8,38 +8,36 @@ import type { ComponentType } from './component';
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
baseModelPropName: 'value',
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
baseModelPropName: 'value',
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
export { useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
@@ -35,7 +33,7 @@ setupVbenVxeTable({
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
|
@@ -12,16 +12,12 @@ import { useTitle } from '@vueuse/core';
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
initComponentAdapter();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
|
@@ -1,13 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NCard, useMessage } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
import modalDemo from './modal.vue';
|
||||
|
||||
const message = useMessage();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
@@ -145,10 +143,6 @@ function setFormValues() {
|
||||
date: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: modalDemo,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
@@ -158,12 +152,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
<NCard title="基础表单">
|
||||
<template #header-extra>
|
||||
<NButton type="primary" @click="setFormValues">设置表单值</NButton>
|
||||
<NButton type="primary" @click="modalApi.open()" class="ml-2">
|
||||
打开弹窗
|
||||
</NButton>
|
||||
</template>
|
||||
<Form />
|
||||
</NCard>
|
||||
<Modal />
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -1,71 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field1',
|
||||
label: '字段1',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field2',
|
||||
label: '字段2',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
],
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field3',
|
||||
label: '字段3',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
await formApi.validateAndSubmitForm();
|
||||
// modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
formApi.setValues(values);
|
||||
}
|
||||
}
|
||||
},
|
||||
title: '内嵌表单示例',
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal>
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/docs",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vitepress build",
|
||||
|
@@ -22,7 +22,7 @@ outline: deep
|
||||
|
||||
## 基础用法
|
||||
|
||||
使用 `useVbenDrawer` 创建最基础的抽屉。
|
||||
使用 `useVbenDrawer` 创建最基础的模态框。
|
||||
|
||||
<DemoPreview dir="demos/vben-drawer/basic" />
|
||||
|
||||
@@ -52,7 +52,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
|
||||
|
||||
::: info 注意
|
||||
|
||||
- `VbenDrawer` 组件对于参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
||||
- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
||||
@@ -77,7 +77,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| connectedComponent | 连接另一个Drawer组件 | `Component` | - |
|
||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
@@ -96,7 +96,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
||||
| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
|
||||
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
|
||||
| class | modal的class,宽度通过这个配置 | `string` | - |
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
|
@@ -26,12 +26,6 @@ outline: deep
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
|
||||
|
||||
## 自动显示 tooltip
|
||||
|
||||
通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/auto-display" />
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
@@ -43,8 +37,6 @@ outline: deep
|
||||
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
|
||||
| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
|
||||
| tooltip | 启用文本提示 | `boolean` | `true` |
|
||||
| tooltipWhenEllipsis | 内容超出,自动启用文本提示 | `boolean` | `false` |
|
||||
| ellipsisThreshold | 设置 tooltipWhenEllipsis 后才生效,文本截断检测的像素差异阈值,越大则判断越严格,如果碰见异常情况可以自己设置阈值 | `number` | `3` |
|
||||
| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
|
||||
| tooltipColor | 提示文本的颜色 | `string` | - |
|
||||
| tooltipFontSize | 提示文本的大小 | `string` | - |
|
||||
|
@@ -90,52 +90,30 @@ import { h } from 'vue';
|
||||
import { globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
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'));
|
||||
|
||||
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 withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
@@ -326,12 +304,10 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
|
||||
| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` |
|
||||
| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
|
||||
| wrapperClass | 表单的布局,基于tailwindcss | `any` | - |
|
||||
| actionWrapperClass | 表单操作区域class | `any` | - |
|
||||
| actionLayout | 表单操作按钮位置 | `'newLine' \| 'rowEnd' \| 'inline'` | `rowEnd` |
|
||||
| actionPosition | 表单操作按钮对齐方式 | `'left' \| 'center' \| 'right'` | `right` |
|
||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
|
||||
@@ -348,7 +324,6 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
||||
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
|
||||
| scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | `boolean` | false |
|
||||
|
||||
::: tip handleValuesChange
|
||||
|
||||
@@ -419,7 +394,7 @@ export interface FormCommonConfig {
|
||||
* 所有表单项的栅格布局
|
||||
* @default ""
|
||||
*/
|
||||
formItemClass?: (() => string) | string;
|
||||
formItemClass?: string;
|
||||
/**
|
||||
* 隐藏所有表单项label
|
||||
* @default false
|
||||
|
@@ -56,15 +56,6 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/shared-data" />
|
||||
|
||||
## 动画类型
|
||||
|
||||
通过 `animationType` 属性可以控制弹窗的动画效果:
|
||||
|
||||
- `slide`(默认):从顶部向下滑动进入/退出
|
||||
- `scale`:缩放淡入/淡出效果
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/animation-type" />
|
||||
|
||||
::: info 注意
|
||||
|
||||
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
@@ -121,7 +112,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
| bordered | 是否显示border | `boolean` | `false` |
|
||||
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
|
||||
| overlayBlur | 遮罩模糊度 | `number` | - |
|
||||
| animationType | 动画类型 | `'slide' \| 'scale'` | `'slide'` |
|
||||
| submitting | 标记为提交中,锁定弹窗当前状态 | `boolean` | `false` |
|
||||
|
||||
::: info appendToMain
|
||||
|
@@ -1,16 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :line="2" :tooltip-when-ellipsis="true">
|
||||
{{ text }}
|
||||
</EllipsisText>
|
||||
|
||||
<EllipsisText :line="3" :tooltip-when-ellipsis="true">
|
||||
{{ text }}
|
||||
</EllipsisText>
|
||||
</template>
|
@@ -15,7 +15,6 @@ const [Form] = useVbenForm({
|
||||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
scrollToFirstError: true,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
|
@@ -1,36 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const [SlideModal, slideModalApi] = useVbenModal({
|
||||
animationType: 'slide',
|
||||
});
|
||||
|
||||
const [ScaleModal, scaleModalApi] = useVbenModal({
|
||||
animationType: 'scale',
|
||||
});
|
||||
|
||||
function openSlideModal() {
|
||||
slideModalApi.open();
|
||||
}
|
||||
|
||||
function openScaleModal() {
|
||||
scaleModalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<VbenButton @click="openSlideModal">滑动动画</VbenButton>
|
||||
<VbenButton @click="openScaleModal">缩放动画</VbenButton>
|
||||
</div>
|
||||
|
||||
<SlideModal title="滑动动画示例" class="w-[500px]">
|
||||
<p>这是使用滑动动画的弹窗,从顶部向下滑动进入。</p>
|
||||
</SlideModal>
|
||||
|
||||
<ScaleModal title="缩放动画示例" class="w-[500px]">
|
||||
<p>这是使用缩放动画的弹窗,以缩放淡入淡出的方式显示。</p>
|
||||
</ScaleModal>
|
||||
</div>
|
||||
</template>
|
@@ -150,8 +150,8 @@ export async function saveUserApi(user: UserInfo) {
|
||||
```ts
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export async function deleteUserApi(userId: number) {
|
||||
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||
export async function deleteUserApi(user: UserInfo) {
|
||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
|
||||
console.log(import.meta.env.VITE_PROT);
|
||||
```
|
||||
|
||||
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
|
||||
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. :::
|
||||
|
||||
:::
|
||||
|
||||
@@ -138,27 +138,6 @@ To add a new dynamically modifiable configuration item, simply follow the steps
|
||||
}
|
||||
```
|
||||
|
||||
- In `packages/effects/hooks/src/use-app-config.ts`, add the corresponding configuration item, such as:
|
||||
|
||||
```ts
|
||||
export function useAppConfig(
|
||||
env: Record<string, any>,
|
||||
isProduction: boolean,
|
||||
): ApplicationConfig {
|
||||
// In production environment, directly use the window._VBEN_ADMIN_PRO_APP_CONF_ global variable
|
||||
const config = isProduction
|
||||
? window._VBEN_ADMIN_PRO_APP_CONF_
|
||||
: (env as VbenAdminProAppConfigRaw);
|
||||
|
||||
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
|
||||
|
||||
return {
|
||||
apiURL: VITE_GLOB_API_URL,
|
||||
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item.
|
||||
|
||||
```ts
|
||||
@@ -207,12 +186,6 @@ const defaultPreferences: Preferences = {
|
||||
colorWeakMode: false,
|
||||
compact: false,
|
||||
contentCompact: 'wide',
|
||||
contentCompactWidth: 1200,
|
||||
contentPadding: 0,
|
||||
contentPaddingBottom: 0,
|
||||
contentPaddingLeft: 0,
|
||||
contentPaddingRight: 0,
|
||||
contentPaddingTop: 0,
|
||||
defaultAvatar:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||
defaultHomePath: '/analytics',
|
||||
@@ -227,7 +200,6 @@ const defaultPreferences: Preferences = {
|
||||
name: 'Vben Admin',
|
||||
preferencesButtonPosition: 'auto',
|
||||
watermark: false,
|
||||
zIndex: 200,
|
||||
},
|
||||
breadcrumb: {
|
||||
enable: true,
|
||||
@@ -248,18 +220,15 @@ const defaultPreferences: Preferences = {
|
||||
footer: {
|
||||
enable: false,
|
||||
fixed: false,
|
||||
height: 32,
|
||||
},
|
||||
header: {
|
||||
enable: true,
|
||||
height: 50,
|
||||
hidden: false,
|
||||
menuAlign: 'start',
|
||||
mode: 'fixed',
|
||||
},
|
||||
logo: {
|
||||
enable: true,
|
||||
fit: 'contain',
|
||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
},
|
||||
navigation: {
|
||||
@@ -279,14 +248,11 @@ const defaultPreferences: Preferences = {
|
||||
collapsed: false,
|
||||
collapsedButton: true,
|
||||
collapsedShowTitle: false,
|
||||
collapseWidth: 60,
|
||||
enable: true,
|
||||
expandOnHover: true,
|
||||
extraCollapse: false,
|
||||
extraCollapsedWidth: 60,
|
||||
fixedButton: true,
|
||||
hidden: false,
|
||||
mixedWidth: 80,
|
||||
width: 224,
|
||||
},
|
||||
tabbar: {
|
||||
@@ -353,18 +319,6 @@ interface AppPreferences {
|
||||
compact: boolean;
|
||||
/** Whether to enable content compact mode */
|
||||
contentCompact: ContentCompactType;
|
||||
/** Content compact width */
|
||||
contentCompactWidth: number;
|
||||
/** Content padding */
|
||||
contentPadding: number;
|
||||
/** Content bottom padding */
|
||||
contentPaddingBottom: number;
|
||||
/** Content left padding */
|
||||
contentPaddingLeft: number;
|
||||
/** Content right padding */
|
||||
contentPaddingRight: number;
|
||||
/** Content top padding */
|
||||
contentPaddingTop: number;
|
||||
// /** Default application avatar */
|
||||
defaultAvatar: string;
|
||||
/** Default homepage path */
|
||||
@@ -395,8 +349,6 @@ interface AppPreferences {
|
||||
* @zh_CN Whether to enable watermark
|
||||
*/
|
||||
watermark: boolean;
|
||||
/** z-index */
|
||||
zIndex: number;
|
||||
}
|
||||
interface BreadcrumbPreferences {
|
||||
/** Whether breadcrumbs are enabled */
|
||||
@@ -433,15 +385,11 @@ interface FooterPreferences {
|
||||
enable: boolean;
|
||||
/** Whether the footer is fixed */
|
||||
fixed: boolean;
|
||||
/** Footer height */
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface HeaderPreferences {
|
||||
/** Whether the header is enabled */
|
||||
enable: boolean;
|
||||
/** Header height */
|
||||
height: number;
|
||||
/** Whether the header is hidden, css-hidden */
|
||||
hidden: boolean;
|
||||
/** Header menu alignment */
|
||||
@@ -453,8 +401,6 @@ interface HeaderPreferences {
|
||||
interface LogoPreferences {
|
||||
/** Whether the logo is visible */
|
||||
enable: boolean;
|
||||
/** Logo image fitting method */
|
||||
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
/** Logo URL */
|
||||
source: string;
|
||||
}
|
||||
@@ -476,22 +422,16 @@ interface SidebarPreferences {
|
||||
collapsedButton: boolean;
|
||||
/** Whether to show title when sidebar is collapsed */
|
||||
collapsedShowTitle: boolean;
|
||||
/** Sidebar collapse width */
|
||||
collapseWidth: number;
|
||||
/** Whether the sidebar is visible */
|
||||
enable: boolean;
|
||||
/** Menu auto-expand state */
|
||||
expandOnHover: boolean;
|
||||
/** Whether the sidebar extension area is collapsed */
|
||||
extraCollapse: boolean;
|
||||
/** Sidebar extension area collapse width */
|
||||
extraCollapsedWidth: number;
|
||||
/** Whether the sidebar fixed button is visible */
|
||||
fixedButton: boolean;
|
||||
/** Whether the sidebar is hidden - css */
|
||||
hidden: boolean;
|
||||
/** Mixed sidebar width */
|
||||
mixedWidth: number;
|
||||
/** Sidebar width */
|
||||
width: number;
|
||||
}
|
||||
|
@@ -4,11 +4,10 @@ outline: deep
|
||||
|
||||
# Access Control
|
||||
|
||||
The framework has built-in three types of access control methods:
|
||||
The framework has built-in two types of access control methods:
|
||||
|
||||
- Determining whether a menu or button can be accessed based on user roles
|
||||
- Determining whether a menu or button can be accessed through an API
|
||||
- Mixed mode: Using both frontend and backend access control simultaneously
|
||||
|
||||
## Frontend Access Control
|
||||
|
||||
@@ -152,43 +151,6 @@ const dashboardMenus = [
|
||||
|
||||
At this point, the configuration is complete. You need to ensure that after logging in, the format of the menu returned by the interface is correct; otherwise, access will not be possible.
|
||||
|
||||
## Mixed Access Control
|
||||
|
||||
**Implementation Principle**: Mixed mode combines both frontend access control and backend access control methods. The system processes frontend fixed route permissions and backend dynamic menu data in parallel, ultimately merging both parts of routes to provide a more flexible access control solution.
|
||||
|
||||
**Advantages**: Combines the performance advantages of frontend control with the flexibility of backend control, suitable for complex business scenarios requiring permission management.
|
||||
|
||||
### Steps
|
||||
|
||||
- Ensure the current mode is set to mixed access control
|
||||
|
||||
Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='mixed'`.
|
||||
|
||||
```ts
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
accessMode: 'mixed',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- Configure frontend route permissions
|
||||
|
||||
Same as the route permission configuration method in [Frontend Access Control](#frontend-access-control) mode.
|
||||
|
||||
- Configure backend menu interface
|
||||
|
||||
Same as the interface configuration method in [Backend Access Control](#backend-access-control) mode.
|
||||
|
||||
- Ensure roles and permissions match
|
||||
|
||||
Must satisfy both frontend route permission configuration and backend menu data return requirements, ensuring user roles match the permission configurations of both modes.
|
||||
|
||||
At this point, the configuration is complete. Mixed mode will automatically merge frontend and backend routes, providing complete access control functionality.
|
||||
|
||||
## Fine-grained Control of Buttons
|
||||
|
||||
In some cases, we need to control the display of buttons with fine granularity. We can control the display of buttons through interfaces or roles.
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
- If you want to contribute code to the project, please ensure your code complies with the project's coding standards.
|
||||
- If you are using `vscode`, you need to install the following plugins:
|
||||
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code checking
|
||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting
|
||||
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Word syntax checking
|
||||
@@ -156,6 +157,7 @@ The most effective solution is to perform Lint checks locally before committing.
|
||||
The project defines corresponding hooks inside `lefthook.yml`:
|
||||
|
||||
- `pre-commit`: Runs before commit, used for code formatting and checking
|
||||
|
||||
- `code-workspace`: Updates VSCode workspace configuration
|
||||
- `lint-md`: Formats Markdown files
|
||||
- `lint-vue`: Formats and checks Vue files
|
||||
@@ -165,6 +167,7 @@ The project defines corresponding hooks inside `lefthook.yml`:
|
||||
- `lint-json`: Formats other JSON files
|
||||
|
||||
- `post-merge`: Runs after merge, used for automatic dependency installation
|
||||
|
||||
- `install`: Runs `pnpm install` to install new dependencies
|
||||
|
||||
- `commit-msg`: Runs during commit, used for checking commit message format
|
||||
|
@@ -18,6 +18,7 @@
|
||||
### 友情链接
|
||||
|
||||
- 在您的网站上添加我们的友情链接,链接如下:
|
||||
|
||||
- 名称:Vben Admin
|
||||
- 链接:https://www.vben.pro
|
||||
- 描述:Vben Admin 企业级开箱即用的中后台前端解决方案
|
||||
|
@@ -214,7 +214,7 @@ server {
|
||||
|
||||
使用 nginx 处理项目部署后的跨域问题
|
||||
|
||||
1. 配置前端项目接口地址,在项目目录下的`.env.production`文件中配置:
|
||||
1. 配置前端项目接口地址,在项目目录下的``.env.production`文件中配置:
|
||||
|
||||
```bash
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
@@ -180,8 +180,8 @@ export async function saveUserApi(user: UserInfo) {
|
||||
```ts
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export async function deleteUserApi(userId: number) {
|
||||
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||
export async function deleteUserApi(user: UserInfo) {
|
||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
console.log(import.meta.env.VITE_PROT);
|
||||
```
|
||||
|
||||
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中.
|
||||
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中. :::
|
||||
|
||||
:::
|
||||
|
||||
@@ -137,27 +137,6 @@ const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
}
|
||||
```
|
||||
|
||||
- 在 `packages/effects/hooks/src/use-app-config.ts` 中,新增对应的配置项,如:
|
||||
|
||||
```ts
|
||||
export function useAppConfig(
|
||||
env: Record<string, any>,
|
||||
isProduction: boolean,
|
||||
): ApplicationConfig {
|
||||
// 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量
|
||||
const config = isProduction
|
||||
? window._VBEN_ADMIN_PRO_APP_CONF_
|
||||
: (env as VbenAdminProAppConfigRaw);
|
||||
|
||||
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
|
||||
|
||||
return {
|
||||
apiURL: VITE_GLOB_API_URL,
|
||||
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
到这里,就可以在项目内使用 `useAppConfig`方法获取到新增的配置项了。
|
||||
|
||||
```ts
|
||||
@@ -206,12 +185,6 @@ const defaultPreferences: Preferences = {
|
||||
colorWeakMode: false,
|
||||
compact: false,
|
||||
contentCompact: 'wide',
|
||||
contentCompactWidth: 1200,
|
||||
contentPadding: 0,
|
||||
contentPaddingBottom: 0,
|
||||
contentPaddingLeft: 0,
|
||||
contentPaddingRight: 0,
|
||||
contentPaddingTop: 0,
|
||||
defaultAvatar:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||
defaultHomePath: '/analytics',
|
||||
@@ -226,7 +199,6 @@ const defaultPreferences: Preferences = {
|
||||
name: 'Vben Admin',
|
||||
preferencesButtonPosition: 'auto',
|
||||
watermark: false,
|
||||
zIndex: 200,
|
||||
},
|
||||
breadcrumb: {
|
||||
enable: true,
|
||||
@@ -247,18 +219,15 @@ const defaultPreferences: Preferences = {
|
||||
footer: {
|
||||
enable: false,
|
||||
fixed: false,
|
||||
height: 32,
|
||||
},
|
||||
header: {
|
||||
enable: true,
|
||||
height: 50,
|
||||
hidden: false,
|
||||
menuAlign: 'start',
|
||||
mode: 'fixed',
|
||||
},
|
||||
logo: {
|
||||
enable: true,
|
||||
fit: 'contain',
|
||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
},
|
||||
navigation: {
|
||||
@@ -278,14 +247,11 @@ const defaultPreferences: Preferences = {
|
||||
collapsed: false,
|
||||
collapsedButton: true,
|
||||
collapsedShowTitle: false,
|
||||
collapseWidth: 60,
|
||||
enable: true,
|
||||
expandOnHover: true,
|
||||
extraCollapse: false,
|
||||
extraCollapsedWidth: 60,
|
||||
fixedButton: true,
|
||||
hidden: false,
|
||||
mixedWidth: 80,
|
||||
width: 224,
|
||||
},
|
||||
tabbar: {
|
||||
@@ -352,18 +318,6 @@ interface AppPreferences {
|
||||
compact: boolean;
|
||||
/** 是否开启内容紧凑模式 */
|
||||
contentCompact: ContentCompactType;
|
||||
/** 内容紧凑宽度 */
|
||||
contentCompactWidth: number;
|
||||
/** 内容内边距 */
|
||||
contentPadding: number;
|
||||
/** 内容底部内边距 */
|
||||
contentPaddingBottom: number;
|
||||
/** 内容左侧内边距 */
|
||||
contentPaddingLeft: number;
|
||||
/** 内容右侧内边距 */
|
||||
contentPaddingRight: number;
|
||||
/** 内容顶部内边距 */
|
||||
contentPaddingTop: number;
|
||||
// /** 应用默认头像 */
|
||||
defaultAvatar: string;
|
||||
/** 默认首页地址 */
|
||||
@@ -394,8 +348,6 @@ interface AppPreferences {
|
||||
* @zh_CN 是否开启水印
|
||||
*/
|
||||
watermark: boolean;
|
||||
/** z-index */
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
interface BreadcrumbPreferences {
|
||||
@@ -433,15 +385,11 @@ interface FooterPreferences {
|
||||
enable: boolean;
|
||||
/** 底栏是否固定 */
|
||||
fixed: boolean;
|
||||
/** 底栏高度 */
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface HeaderPreferences {
|
||||
/** 顶栏是否启用 */
|
||||
enable: boolean;
|
||||
/** 顶栏高度 */
|
||||
height: number;
|
||||
/** 顶栏是否隐藏,css-隐藏 */
|
||||
hidden: boolean;
|
||||
/** 顶栏菜单位置 */
|
||||
@@ -453,8 +401,6 @@ interface HeaderPreferences {
|
||||
interface LogoPreferences {
|
||||
/** logo是否可见 */
|
||||
enable: boolean;
|
||||
/** logo图片适应方式 */
|
||||
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
/** logo地址 */
|
||||
source: string;
|
||||
}
|
||||
@@ -477,22 +423,16 @@ interface SidebarPreferences {
|
||||
collapsedButton: boolean;
|
||||
/** 侧边栏折叠时,是否显示title */
|
||||
collapsedShowTitle: boolean;
|
||||
/** 侧边栏折叠宽度 */
|
||||
collapseWidth: number;
|
||||
/** 侧边栏是否可见 */
|
||||
enable: boolean;
|
||||
/** 菜单自动展开状态 */
|
||||
expandOnHover: boolean;
|
||||
/** 侧边栏扩展区域是否折叠 */
|
||||
extraCollapse: boolean;
|
||||
/** 侧边栏扩展区域折叠宽度 */
|
||||
extraCollapsedWidth: number;
|
||||
/** 侧边栏固定按钮是否可见 */
|
||||
fixedButton: boolean;
|
||||
/** 侧边栏是否隐藏 - css */
|
||||
hidden: boolean;
|
||||
/** 混合侧边栏宽度 */
|
||||
mixedWidth: number;
|
||||
/** 侧边栏宽度 */
|
||||
width: number;
|
||||
}
|
||||
|
@@ -4,11 +4,10 @@ outline: deep
|
||||
|
||||
# 权限
|
||||
|
||||
框架内置了三种权限控制方式:
|
||||
框架内置了两种权限控制方式:
|
||||
|
||||
- 通过用户角色来判断菜单或者按钮是否可以访问
|
||||
- 通过接口来判断菜单或者按钮是否可以访问
|
||||
- 混合模式:同时使用前端和后端权限控制
|
||||
|
||||
## 前端访问控制
|
||||
|
||||
@@ -160,43 +159,6 @@ const dashboardMenus = [
|
||||
|
||||
到这里,就已经配置完成,你需要确保登录后,接口返回的菜单格式正确,否则无法访问。
|
||||
|
||||
## 混合访问控制
|
||||
|
||||
**实现原理**: 混合模式同时结合了前端访问控制和后端访问控制两种方式。系统会并行处理前端固定路由权限和后端动态菜单数据,最终将两部分路由合并,提供更灵活的权限控制方案。
|
||||
|
||||
**优点**: 兼具前端控制的性能优势和后端控制的灵活性,适合复杂业务场景下的权限管理。
|
||||
|
||||
### 步骤
|
||||
|
||||
- 确保当前模式为混合访问控制模式
|
||||
|
||||
调整对应应用目录下的`preferences.ts`,确保`accessMode='mixed'`。
|
||||
|
||||
```ts
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
accessMode: 'mixed',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- 配置前端路由权限
|
||||
|
||||
同[前端访问控制](#前端访问控制)模式的路由权限配置方式。
|
||||
|
||||
- 配置后端菜单接口
|
||||
|
||||
同[后端访问控制](#后端访问控制)模式的接口配置方式。
|
||||
|
||||
- 确保角色和权限匹配
|
||||
|
||||
需要同时满足前端路由权限配置和后端菜单数据返回的要求,确保用户角色与两种模式的权限配置都匹配。
|
||||
|
||||
到这里,就已经配置完成,混合模式会自动合并前端和后端的路由,提供完整的权限控制功能。
|
||||
|
||||
## 按钮细粒度控制
|
||||
|
||||
在某些情况下,我们需要对按钮进行细粒度的控制,我们可以借助接口或者角色来控制按钮的显示。
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
- 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。
|
||||
- 如果你使用的是 `vscode`,需要安装以下插件:
|
||||
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查
|
||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化
|
||||
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查
|
||||
@@ -156,6 +157,7 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风
|
||||
项目在 `lefthook.yml` 内部定义了相应的 hooks:
|
||||
|
||||
- `pre-commit`: 在提交前运行,用于代码格式化和检查
|
||||
|
||||
- `code-workspace`: 更新 VSCode 工作区配置
|
||||
- `lint-md`: 格式化 Markdown 文件
|
||||
- `lint-vue`: 格式化并检查 Vue 文件
|
||||
@@ -165,6 +167,7 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风
|
||||
- `lint-json`: 格式化其他 JSON 文件
|
||||
|
||||
- `post-merge`: 在合并后运行,用于自动安装依赖
|
||||
|
||||
- `install`: 运行 `pnpm install` 安装新依赖
|
||||
|
||||
- `commit-msg`: 在提交时运行,用于检查提交信息格式
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/commitlint-config",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/stylelint-config",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/node-utils",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tailwind-config",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tsconfig",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/vite-config",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
@@ -47,12 +47,15 @@
|
||||
"@vitejs/plugin-vue-jsx": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"electron": "catalog:",
|
||||
"rollup": "catalog:",
|
||||
"rollup-plugin-visualizer": "catalog:",
|
||||
"sass": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-compression": "catalog:",
|
||||
"vite-plugin-dts": "catalog:",
|
||||
"vite-plugin-electron": "catalog:",
|
||||
"vite-plugin-electron-renderer": "catalog:",
|
||||
"vite-plugin-html": "catalog:",
|
||||
"vite-plugin-lazy-import": "catalog:"
|
||||
}
|
||||
|
40
internal/vite-config/src/plugins/electron.ts
Normal file
40
internal/vite-config/src/plugins/electron.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
import type { CommonPluginOptions } from '../typing';
|
||||
|
||||
import fs from 'node:fs';
|
||||
|
||||
import electron from 'vite-plugin-electron/simple';
|
||||
|
||||
export const viteElectronPlugin = (
|
||||
options: CommonPluginOptions,
|
||||
): PluginOption => {
|
||||
fs.rmSync('dist-electron', { force: true, recursive: true });
|
||||
|
||||
const isServe = !options.isBuild;
|
||||
const isBuild = options.isBuild;
|
||||
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
|
||||
return electron({
|
||||
main: {
|
||||
entry: 'electron/main.ts',
|
||||
vite: {
|
||||
build: {
|
||||
minify: isBuild,
|
||||
outDir: 'dist-electron/main',
|
||||
sourcemap,
|
||||
},
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
input: 'electron/preload.ts',
|
||||
vite: {
|
||||
build: {
|
||||
minify: isBuild,
|
||||
outDir: 'dist-electron/preload',
|
||||
sourcemap: sourcemap ? 'inline' : undefined, // #332
|
||||
},
|
||||
},
|
||||
},
|
||||
renderer: {},
|
||||
});
|
||||
};
|
@@ -18,6 +18,7 @@ import { VitePWA } from 'vite-plugin-pwa';
|
||||
import viteVueDevTools from 'vite-plugin-vue-devtools';
|
||||
|
||||
import { viteArchiverPlugin } from './archiver';
|
||||
import { viteElectronPlugin } from './electron';
|
||||
import { viteExtraAppConfigPlugin } from './extra-app-config';
|
||||
import { viteImportMapPlugin } from './importmap';
|
||||
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
|
||||
@@ -97,6 +98,7 @@ async function loadApplicationPlugins(
|
||||
archiverPluginOptions,
|
||||
compress,
|
||||
compressTypes,
|
||||
electron,
|
||||
extraAppConfig,
|
||||
html,
|
||||
i18n,
|
||||
@@ -213,6 +215,10 @@ async function loadApplicationPlugins(
|
||||
return [await viteArchiverPlugin(archiverPluginOptions)];
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: electron,
|
||||
plugins: () => [viteElectronPlugin(commonOptions)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -3,23 +3,6 @@ import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
|
||||
import type { PluginOptions } from 'vite-plugin-dts';
|
||||
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
|
||||
|
||||
/**
|
||||
* ImportMap 配置接口
|
||||
* @description 用于配置模块导入映射,支持自定义导入路径和范围
|
||||
* @example
|
||||
* ```typescript
|
||||
* {
|
||||
* imports: {
|
||||
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
|
||||
* },
|
||||
* scopes: {
|
||||
* 'https://site.com/': {
|
||||
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
interface IImportMap {
|
||||
/** 模块导入映射 */
|
||||
imports?: Record<string, string>;
|
||||
@@ -202,6 +185,8 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
|
||||
* @description 可选的压缩类型
|
||||
*/
|
||||
compressTypes?: ('brotli' | 'gzip')[];
|
||||
/** 启用electron */
|
||||
electron?: boolean;
|
||||
/**
|
||||
* 是否抽离配置文件
|
||||
* @default false
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vben-admin-monorepo",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"monorepo",
|
||||
@@ -98,7 +98,7 @@
|
||||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/design",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/icons",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -45,6 +45,7 @@ export {
|
||||
Menu,
|
||||
Minimize,
|
||||
Minimize2,
|
||||
Minus,
|
||||
MoonStar,
|
||||
Palette,
|
||||
PanelLeft,
|
||||
@@ -58,6 +59,8 @@ export {
|
||||
Settings,
|
||||
Shrink,
|
||||
Square,
|
||||
SquareArrowDownLeft,
|
||||
SquareArrowUpRight,
|
||||
SquareCheckBig,
|
||||
SquareMinus,
|
||||
Sun,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/shared",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -98,6 +98,8 @@
|
||||
"@types/lodash.get": "catalog:",
|
||||
"@types/lodash.isequal": "catalog:",
|
||||
"@types/lodash.set": "catalog:",
|
||||
"@types/nprogress": "catalog:"
|
||||
"@types/nprogress": "catalog:",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"electron": "catalog:"
|
||||
}
|
||||
}
|
||||
|
@@ -1,82 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { loadScript } from '../resources';
|
||||
|
||||
const testJsPath =
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js';
|
||||
|
||||
describe('loadScript', () => {
|
||||
beforeEach(() => {
|
||||
// 每个测试前清空 head,保证环境干净
|
||||
document.head.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should resolve when the script loads successfully', async () => {
|
||||
const promise = loadScript(testJsPath);
|
||||
|
||||
// 此时脚本元素已被创建并插入
|
||||
const script = document.querySelector(
|
||||
`script[src="${testJsPath}"]`,
|
||||
) as HTMLScriptElement;
|
||||
expect(script).toBeTruthy();
|
||||
|
||||
// 模拟加载成功
|
||||
script.dispatchEvent(new Event('load'));
|
||||
|
||||
// 等待 promise resolve
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not insert duplicate script and resolve immediately if already loaded', async () => {
|
||||
// 先手动插入一个相同 src 的 script
|
||||
const existing = document.createElement('script');
|
||||
existing.src = 'bar.js';
|
||||
document.head.append(existing);
|
||||
|
||||
// 再次调用
|
||||
const promise = loadScript('bar.js');
|
||||
|
||||
// 立即 resolve
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
|
||||
// head 中只保留一个
|
||||
const scripts = document.head.querySelectorAll('script[src="bar.js"]');
|
||||
expect(scripts).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should reject when the script fails to load', async () => {
|
||||
const promise = loadScript('error.js');
|
||||
|
||||
const script = document.querySelector(
|
||||
'script[src="error.js"]',
|
||||
) as HTMLScriptElement;
|
||||
expect(script).toBeTruthy();
|
||||
|
||||
// 模拟加载失败
|
||||
script.dispatchEvent(new Event('error'));
|
||||
|
||||
await expect(promise).rejects.toThrow('Failed to load script: error.js');
|
||||
});
|
||||
|
||||
it('should handle multiple concurrent calls and only insert one script tag', async () => {
|
||||
const p1 = loadScript(testJsPath);
|
||||
const p2 = loadScript(testJsPath);
|
||||
|
||||
const script = document.querySelector(
|
||||
`script[src="${testJsPath}"]`,
|
||||
) as HTMLScriptElement;
|
||||
expect(script).toBeTruthy();
|
||||
|
||||
// 触发一次 load,两个 promise 都应该 resolve
|
||||
script.dispatchEvent(new Event('load'));
|
||||
|
||||
await expect(p1).resolves.toBeUndefined();
|
||||
await expect(p2).resolves.toBeUndefined();
|
||||
|
||||
// 只插入一次
|
||||
const scripts = document.head.querySelectorAll(
|
||||
`script[src="${testJsPath}"]`,
|
||||
);
|
||||
expect(scripts).toHaveLength(1);
|
||||
});
|
||||
});
|
@@ -7,7 +7,6 @@ export * from './inference';
|
||||
export * from './letter';
|
||||
export * from './merge';
|
||||
export * from './nprogress';
|
||||
export * from './resources';
|
||||
export * from './state-handler';
|
||||
export * from './to';
|
||||
export * from './tree';
|
||||
|
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* 加载js文件
|
||||
* @param src js文件地址
|
||||
*/
|
||||
function loadScript(src: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (document.querySelector(`script[src="${src}"]`)) {
|
||||
// 如果已经加载过,直接 resolve
|
||||
return resolve();
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.addEventListener('load', () => resolve());
|
||||
script.addEventListener('error', () =>
|
||||
reject(new Error(`Failed to load script: ${src}`)),
|
||||
);
|
||||
document.head.append(script);
|
||||
});
|
||||
}
|
||||
|
||||
export { loadScript };
|
@@ -28,10 +28,11 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
|
||||
* @param path
|
||||
*/
|
||||
function openRouteInNewWindow(path: string) {
|
||||
const { hash, origin } = location;
|
||||
// const { hash, origin } = location;
|
||||
const fullPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
|
||||
openWindow(url, { target: '_blank' });
|
||||
// const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
|
||||
// openWindow(url, { target: '_blank' });
|
||||
window.ipcRenderer.invoke('open-win', fullPath);
|
||||
}
|
||||
|
||||
export { openRouteInNewWindow, openWindow };
|
||||
|
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@vben-core/typings/electron"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
27
packages/@core/base/typings/electron.d.ts
vendored
Normal file
27
packages/@core/base/typings/electron.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { IpcRendererEvent } from 'electron';
|
||||
|
||||
export type IpcRendererInvoke =
|
||||
| 'app-close'
|
||||
| 'app-maximize'
|
||||
| 'app-minimize'
|
||||
| 'is-maximized'
|
||||
| 'open-win';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ipcRenderer: {
|
||||
invoke: (channel: IpcRendererInvoke, ...args: any[]) => Promise<any>;
|
||||
off: (
|
||||
channel: string,
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void,
|
||||
) => void;
|
||||
on: (
|
||||
channel: string,
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void,
|
||||
) => void;
|
||||
send: (channel: string, data: any) => Promise<any>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/typings",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -27,6 +27,9 @@
|
||||
},
|
||||
"./vue-router": {
|
||||
"types": "./vue-router.d.ts"
|
||||
},
|
||||
"./electron": {
|
||||
"types": "./electron.d.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
|
3
packages/@core/base/typings/src/app.d.ts
vendored
3
packages/@core/base/typings/src/app.d.ts
vendored
@@ -60,9 +60,8 @@ type BreadcrumbStyleType = 'background' | 'normal';
|
||||
* 权限模式
|
||||
* backend 后端权限模式
|
||||
* frontend 前端权限模式
|
||||
* mixed 混合权限模式
|
||||
*/
|
||||
type AccessModeType = 'backend' | 'frontend' | 'mixed';
|
||||
type AccessModeType = 'backend' | 'frontend';
|
||||
|
||||
/**
|
||||
* 导航风格
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/composables",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -10,12 +10,6 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"colorWeakMode": false,
|
||||
"compact": false,
|
||||
"contentCompact": "wide",
|
||||
"contentCompactWidth": 1200,
|
||||
"contentPadding": 0,
|
||||
"contentPaddingBottom": 0,
|
||||
"contentPaddingLeft": 0,
|
||||
"contentPaddingRight": 0,
|
||||
"contentPaddingTop": 0,
|
||||
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
||||
"defaultHomePath": "/analytics",
|
||||
"dynamicTitle": true,
|
||||
@@ -29,7 +23,6 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"name": "Vben Admin",
|
||||
"preferencesButtonPosition": "auto",
|
||||
"watermark": false,
|
||||
"zIndex": 200,
|
||||
},
|
||||
"breadcrumb": {
|
||||
"enable": true,
|
||||
@@ -50,18 +43,15 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"footer": {
|
||||
"enable": false,
|
||||
"fixed": false,
|
||||
"height": 32,
|
||||
},
|
||||
"header": {
|
||||
"enable": true,
|
||||
"height": 50,
|
||||
"hidden": false,
|
||||
"menuAlign": "start",
|
||||
"mode": "fixed",
|
||||
},
|
||||
"logo": {
|
||||
"enable": true,
|
||||
"fit": "contain",
|
||||
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
|
||||
},
|
||||
"navigation": {
|
||||
@@ -78,17 +68,14 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
},
|
||||
"sidebar": {
|
||||
"autoActivateChild": false,
|
||||
"collapseWidth": 60,
|
||||
"collapsed": false,
|
||||
"collapsedButton": true,
|
||||
"collapsedShowTitle": false,
|
||||
"enable": true,
|
||||
"expandOnHover": true,
|
||||
"extraCollapse": false,
|
||||
"extraCollapsedWidth": 60,
|
||||
"fixedButton": true,
|
||||
"hidden": false,
|
||||
"mixedWidth": 80,
|
||||
"width": 224,
|
||||
},
|
||||
"tabbar": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/preferences",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -9,12 +9,6 @@ const defaultPreferences: Preferences = {
|
||||
colorWeakMode: false,
|
||||
compact: false,
|
||||
contentCompact: 'wide',
|
||||
contentCompactWidth: 1200,
|
||||
contentPadding: 0,
|
||||
contentPaddingBottom: 0,
|
||||
contentPaddingLeft: 0,
|
||||
contentPaddingRight: 0,
|
||||
contentPaddingTop: 0,
|
||||
defaultAvatar:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||
defaultHomePath: '/analytics',
|
||||
@@ -29,7 +23,6 @@ const defaultPreferences: Preferences = {
|
||||
name: 'Vben Admin',
|
||||
preferencesButtonPosition: 'auto',
|
||||
watermark: false,
|
||||
zIndex: 200,
|
||||
},
|
||||
breadcrumb: {
|
||||
enable: true,
|
||||
@@ -50,19 +43,15 @@ const defaultPreferences: Preferences = {
|
||||
footer: {
|
||||
enable: false,
|
||||
fixed: false,
|
||||
height: 32,
|
||||
},
|
||||
header: {
|
||||
enable: true,
|
||||
height: 50,
|
||||
hidden: false,
|
||||
menuAlign: 'start',
|
||||
mode: 'fixed',
|
||||
},
|
||||
|
||||
logo: {
|
||||
enable: true,
|
||||
fit: 'contain',
|
||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
},
|
||||
navigation: {
|
||||
@@ -82,14 +71,11 @@ const defaultPreferences: Preferences = {
|
||||
collapsed: false,
|
||||
collapsedButton: true,
|
||||
collapsedShowTitle: false,
|
||||
collapseWidth: 60,
|
||||
enable: true,
|
||||
expandOnHover: true,
|
||||
extraCollapse: false,
|
||||
extraCollapsedWidth: 60,
|
||||
fixedButton: true,
|
||||
hidden: false,
|
||||
mixedWidth: 80,
|
||||
width: 224,
|
||||
},
|
||||
tabbar: {
|
||||
|
@@ -33,18 +33,6 @@ interface AppPreferences {
|
||||
compact: boolean;
|
||||
/** 是否开启内容紧凑模式 */
|
||||
contentCompact: ContentCompactType;
|
||||
/** 内容紧凑宽度 */
|
||||
contentCompactWidth: number;
|
||||
/** 内容内边距 */
|
||||
contentPadding: number;
|
||||
/** 内容底部内边距 */
|
||||
contentPaddingBottom: number;
|
||||
/** 内容左侧内边距 */
|
||||
contentPaddingLeft: number;
|
||||
/** 内容右侧内边距 */
|
||||
contentPaddingRight: number;
|
||||
/** 内容顶部内边距 */
|
||||
contentPaddingTop: number;
|
||||
// /** 应用默认头像 */
|
||||
defaultAvatar: string;
|
||||
/** 默认首页地址 */
|
||||
@@ -75,8 +63,6 @@ interface AppPreferences {
|
||||
* @zh_CN 是否开启水印
|
||||
*/
|
||||
watermark: boolean;
|
||||
/** z-index */
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
interface BreadcrumbPreferences {
|
||||
@@ -114,15 +100,11 @@ interface FooterPreferences {
|
||||
enable: boolean;
|
||||
/** 底栏是否固定 */
|
||||
fixed: boolean;
|
||||
/** 底栏高度 */
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface HeaderPreferences {
|
||||
/** 顶栏是否启用 */
|
||||
enable: boolean;
|
||||
/** 顶栏高度 */
|
||||
height: number;
|
||||
/** 顶栏是否隐藏,css-隐藏 */
|
||||
hidden: boolean;
|
||||
/** 顶栏菜单位置 */
|
||||
@@ -134,8 +116,6 @@ interface HeaderPreferences {
|
||||
interface LogoPreferences {
|
||||
/** logo是否可见 */
|
||||
enable: boolean;
|
||||
/** logo图片适应方式 */
|
||||
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
/** logo地址 */
|
||||
source: string;
|
||||
}
|
||||
@@ -158,22 +138,16 @@ interface SidebarPreferences {
|
||||
collapsedButton: boolean;
|
||||
/** 侧边栏折叠时,是否显示title */
|
||||
collapsedShowTitle: boolean;
|
||||
/** 侧边栏折叠宽度 */
|
||||
collapseWidth: number;
|
||||
/** 侧边栏是否可见 */
|
||||
enable: boolean;
|
||||
/** 菜单自动展开状态 */
|
||||
expandOnHover: boolean;
|
||||
/** 侧边栏扩展区域是否折叠 */
|
||||
extraCollapse: boolean;
|
||||
/** 侧边栏扩展区域折叠宽度 */
|
||||
extraCollapsedWidth: number;
|
||||
/** 侧边栏固定按钮是否可见 */
|
||||
fixedButton: boolean;
|
||||
/** 侧边栏是否隐藏 - css */
|
||||
hidden: boolean;
|
||||
/** 混合侧边栏宽度 */
|
||||
mixedWidth: number;
|
||||
/** 侧边栏宽度 */
|
||||
width: number;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/form-ui",
|
||||
"version": "5.5.9",
|
||||
"version": "5.5.6",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -34,21 +34,27 @@ const submitButtonOptions = computed(() => {
|
||||
// return !!unref(rootProps).showCollapseButton;
|
||||
// });
|
||||
|
||||
const queryFormStyle = computed(() => {
|
||||
if (!unref(rootProps).actionWrapperClass) {
|
||||
return {
|
||||
'grid-column': `-2 / -1`,
|
||||
marginLeft: 'auto',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
const props = unref(rootProps);
|
||||
if (!props.formApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { valid } = await props.formApi.validate();
|
||||
const { valid } = await form.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = toRaw(await props.formApi.getValues());
|
||||
await props.handleSubmit?.(values);
|
||||
const values = toRaw(await unref(rootProps).formApi?.getValues());
|
||||
await unref(rootProps).handleSubmit?.(values);
|
||||
}
|
||||
|
||||
async function handleReset(e: Event) {
|
||||
@@ -75,59 +81,22 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
const actionWrapperClass = computed(() => {
|
||||
const props = unref(rootProps);
|
||||
const actionLayout = props.actionLayout || 'rowEnd';
|
||||
const actionPosition = props.actionPosition || 'right';
|
||||
|
||||
const cls = [
|
||||
'flex',
|
||||
'items-center',
|
||||
'gap-3',
|
||||
props.compact ? 'pb-2' : 'pb-4',
|
||||
props.layout === 'vertical' ? 'self-end' : 'self-center',
|
||||
props.layout === 'inline' ? '' : 'w-full',
|
||||
props.actionWrapperClass,
|
||||
];
|
||||
|
||||
switch (actionLayout) {
|
||||
case 'newLine': {
|
||||
cls.push('col-span-full');
|
||||
break;
|
||||
}
|
||||
case 'rowEnd': {
|
||||
cls.push('col-[-2/-1]');
|
||||
break;
|
||||
}
|
||||
// 'inline' 不需要额外类名,保持默认
|
||||
}
|
||||
|
||||
switch (actionPosition) {
|
||||
case 'center': {
|
||||
cls.push('justify-center');
|
||||
break;
|
||||
}
|
||||
case 'left': {
|
||||
cls.push('justify-start');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// case 'right': 默认右对齐
|
||||
cls.push('justify-end');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cls.join(' ');
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
handleReset,
|
||||
handleSubmit,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cn(actionWrapperClass)">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'col-span-full w-full text-right',
|
||||
rootProps.compact ? 'pb-2' : 'pb-6',
|
||||
rootProps.actionWrapperClass,
|
||||
)
|
||||
"
|
||||
:style="queryFormStyle"
|
||||
>
|
||||
<template v-if="rootProps.actionButtonsReverse">
|
||||
<!-- 提交按钮前 -->
|
||||
<slot name="submit-before"></slot>
|
||||
@@ -135,6 +104,7 @@ defineExpose({
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
@@ -149,6 +119,7 @@ defineExpose({
|
||||
<component
|
||||
:is="COMPONENT_MAP.DefaultButton"
|
||||
v-if="resetButtonOptions.show"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleReset"
|
||||
v-bind="resetButtonOptions"
|
||||
@@ -163,6 +134,7 @@ defineExpose({
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
@@ -175,9 +147,9 @@ defineExpose({
|
||||
<slot name="expand-before"></slot>
|
||||
|
||||
<VbenExpandableArrow
|
||||
class="ml-[-0.3em]"
|
||||
v-if="rootProps.showCollapseButton"
|
||||
v-model:model-value="collapsed"
|
||||
class="ml-2"
|
||||
>
|
||||
<span>{{ collapsed ? $t('expand') : $t('collapse') }}</span>
|
||||
</VbenExpandableArrow>
|
||||
|
@@ -11,7 +11,7 @@ import type { Recordable } from '@vben-core/typings';
|
||||
|
||||
import type { FormActions, FormSchema, VbenFormProps } from './types';
|
||||
|
||||
import { isRef, toRaw } from 'vue';
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
import { Store } from '@vben-core/shared/store';
|
||||
import {
|
||||
@@ -39,7 +39,6 @@ function getDefaultState(): VbenFormProps {
|
||||
layout: 'horizontal',
|
||||
resetButtonOptions: {},
|
||||
schema: [],
|
||||
scrollToFirstError: false,
|
||||
showCollapseButton: false,
|
||||
showDefaultActions: true,
|
||||
submitButtonOptions: {},
|
||||
@@ -101,26 +100,9 @@ export class FormApi {
|
||||
getFieldComponentRef<T = ComponentPublicInstance>(
|
||||
fieldName: string,
|
||||
): T | undefined {
|
||||
let target = this.componentRefMap.has(fieldName)
|
||||
? (this.componentRefMap.get(fieldName) as ComponentPublicInstance)
|
||||
return this.componentRefMap.has(fieldName)
|
||||
? (this.componentRefMap.get(fieldName) as T)
|
||||
: undefined;
|
||||
if (
|
||||
target &&
|
||||
target.$.type.name === 'AsyncComponentWrapper' &&
|
||||
target.$.subTree.ref
|
||||
) {
|
||||
if (Array.isArray(target.$.subTree.ref)) {
|
||||
if (
|
||||
target.$.subTree.ref.length > 0 &&
|
||||
isRef(target.$.subTree.ref[0]?.r)
|
||||
) {
|
||||
target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance;
|
||||
}
|
||||
} else if (isRef(target.$.subTree.ref.r)) {
|
||||
target = target.$.subTree.ref.r.value as ComponentPublicInstance;
|
||||
}
|
||||
}
|
||||
return target as T;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,41 +236,6 @@ export class FormApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到第一个错误字段
|
||||
* @param errors 验证错误对象
|
||||
*/
|
||||
scrollToFirstError(errors: Record<string, any> | string) {
|
||||
// https://github.com/logaretm/vee-validate/discussions/3835
|
||||
const firstErrorFieldName =
|
||||
typeof errors === 'string' ? errors : Object.keys(errors)[0];
|
||||
|
||||
if (!firstErrorFieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = document.querySelector(
|
||||
`[name="${firstErrorFieldName}"]`,
|
||||
) as HTMLElement;
|
||||
|
||||
// 如果通过 name 属性找不到,尝试通过组件引用查找, 正常情况下不会走到这,怕哪天 vee-validate 改了 name 属性有个兜底的
|
||||
if (!el) {
|
||||
const componentRef = this.getFieldComponentRef(firstErrorFieldName);
|
||||
if (componentRef && componentRef.$el instanceof HTMLElement) {
|
||||
el = componentRef.$el;
|
||||
}
|
||||
}
|
||||
|
||||
if (el) {
|
||||
// 滚动到错误字段,添加一些偏移量以确保字段完全可见
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
|
||||
const form = await this.getForm();
|
||||
form.setFieldValue(field, value, shouldValidate);
|
||||
@@ -413,21 +360,14 @@ export class FormApi {
|
||||
|
||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||
console.error('validate error', validateResult?.errors);
|
||||
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(validateResult.errors);
|
||||
}
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
async validateAndSubmitForm() {
|
||||
const form = await this.getForm();
|
||||
const { valid, errors } = await form.validate();
|
||||
const { valid } = await form.validate();
|
||||
if (!valid) {
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(errors);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return await this.submitForm();
|
||||
@@ -439,10 +379,6 @@ export class FormApi {
|
||||
|
||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||
console.error('validate error', validateResult?.errors);
|
||||
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(fieldName);
|
||||
}
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
|
@@ -59,7 +59,7 @@ const values = useFormValues();
|
||||
const errors = useFieldError(fieldName);
|
||||
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
|
||||
const formApi = formRenderProps.form;
|
||||
const compact = computed(() => formRenderProps.compact);
|
||||
const compact = formRenderProps.compact;
|
||||
const isInValid = computed(() => errors.value?.length > 0);
|
||||
|
||||
const FieldComponent = computed(() => {
|
||||
@@ -295,7 +295,7 @@ onUnmounted(() => {
|
||||
'form-is-required': shouldRequired,
|
||||
'flex-col': isVertical,
|
||||
'flex-row items-center': !isVertical,
|
||||
'pb-4': !compact,
|
||||
'pb-6': !compact,
|
||||
'pb-2': compact,
|
||||
}"
|
||||
class="relative flex"
|
||||
@@ -386,7 +386,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<Transition name="slide-up" v-if="!compact">
|
||||
<FormMessage class="absolute" />
|
||||
<FormMessage class="absolute bottom-1" />
|
||||
</Transition>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
@@ -12,12 +12,7 @@ import type {
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Form } from '@vben-core/shadcn-ui';
|
||||
import {
|
||||
cn,
|
||||
isFunction,
|
||||
isString,
|
||||
mergeWithArrayOverride,
|
||||
} from '@vben-core/shared/utils';
|
||||
import { cn, isString, mergeWithArrayOverride } from '@vben-core/shared/utils';
|
||||
|
||||
import { provideFormRenderProps } from './context';
|
||||
import { useExpandable } from './expandable';
|
||||
@@ -41,16 +36,6 @@ const emits = defineEmits<{
|
||||
submit: [event: any];
|
||||
}>();
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
const cls = ['flex'];
|
||||
if (props.layout === 'inline') {
|
||||
cls.push('flex-wrap gap-x-2');
|
||||
} else {
|
||||
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
|
||||
}
|
||||
return cn(...cls, props.wrapperClass);
|
||||
});
|
||||
|
||||
provideFormRenderProps(props);
|
||||
|
||||
const { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);
|
||||
@@ -125,17 +110,6 @@ const computedSchema = computed(
|
||||
? keepIndex <= index
|
||||
: false;
|
||||
|
||||
// 处理函数形式的formItemClass
|
||||
let resolvedSchemaFormItemClass = schema.formItemClass;
|
||||
if (isFunction(schema.formItemClass)) {
|
||||
try {
|
||||
resolvedSchemaFormItemClass = schema.formItemClass();
|
||||
} catch (error) {
|
||||
console.error('Error calling formItemClass function:', error);
|
||||
resolvedSchemaFormItemClass = '';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
colon,
|
||||
disabled,
|
||||
@@ -159,7 +133,7 @@ const computedSchema = computed(
|
||||
'flex-shrink-0',
|
||||
{ hidden },
|
||||
formItemClass,
|
||||
resolvedSchemaFormItemClass,
|
||||
schema.formItemClass,
|
||||
),
|
||||
labelClass: cn(labelClass, schema.labelClass),
|
||||
};
|
||||
@@ -170,7 +144,7 @@ const computedSchema = computed(
|
||||
|
||||
<template>
|
||||
<component :is="formComponent" v-bind="formComponentProps">
|
||||
<div ref="wrapperRef" :class="wrapperClass">
|
||||
<div ref="wrapperRef" :class="wrapperClass" class="grid">
|
||||
<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>
|
||||
|
@@ -8,7 +8,7 @@ import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
|
||||
|
||||
import type { FormApi } from './form-api';
|
||||
|
||||
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
|
||||
export type FormLayout = 'horizontal' | 'vertical';
|
||||
|
||||
export type BaseFormComponentType =
|
||||
| 'DefaultButton'
|
||||
@@ -174,10 +174,10 @@ export interface FormCommonConfig {
|
||||
*/
|
||||
formFieldProps?: FormFieldOptions;
|
||||
/**
|
||||
* 所有表单项的栅格布局,支持函数形式
|
||||
* 所有表单项的栅格布局
|
||||
* @default ""
|
||||
*/
|
||||
formItemClass?: (() => string) | string;
|
||||
formItemClass?: string;
|
||||
/**
|
||||
* 隐藏所有表单项label
|
||||
* @default false
|
||||
@@ -354,15 +354,6 @@ export interface VbenFormProps<
|
||||
* 操作按钮是否反转(提交按钮前置)
|
||||
*/
|
||||
actionButtonsReverse?: boolean;
|
||||
/**
|
||||
* 操作按钮组的样式
|
||||
* newLine: 在新行显示。rowEnd: 在行内显示,靠右对齐(默认)。inline: 使用grid默认样式
|
||||
*/
|
||||
actionLayout?: 'inline' | 'newLine' | 'rowEnd';
|
||||
/**
|
||||
* 操作按钮组显示位置,默认靠右显示
|
||||
*/
|
||||
actionPosition?: 'center' | 'left' | 'right';
|
||||
/**
|
||||
* 表单操作区域class
|
||||
*/
|
||||
@@ -396,12 +387,6 @@ export interface VbenFormProps<
|
||||
*/
|
||||
resetButtonOptions?: ActionButtonOptions;
|
||||
|
||||
/**
|
||||
* 验证失败时是否自动滚动到第一个错误字段
|
||||
* @default false
|
||||
*/
|
||||
scrollToFirstError?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示默认操作按钮
|
||||
* @default true
|
||||
|
@@ -10,7 +10,7 @@ import { createContext } from '@vben-core/shadcn-ui';
|
||||
import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils';
|
||||
|
||||
import { useForm } from 'vee-validate';
|
||||
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
|
||||
import { object } from 'zod';
|
||||
import { getDefaultsForSchema } from 'zod-defaults';
|
||||
|
||||
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
|
||||
@@ -52,12 +52,7 @@ export function useFormInitial(
|
||||
if (Reflect.has(item, 'defaultValue')) {
|
||||
set(initialValues, item.fieldName, item.defaultValue);
|
||||
} else if (item.rules && !isString(item.rules)) {
|
||||
// 检查规则是否适合提取默认值
|
||||
const customDefaultValue = getCustomDefaultValue(item.rules);
|
||||
zodObject[item.fieldName] = item.rules;
|
||||
if (customDefaultValue !== undefined) {
|
||||
initialValues[item.fieldName] = customDefaultValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,38 +64,6 @@ export function useFormInitial(
|
||||
}
|
||||
return mergeWithArrayOverride(initialValues, zodDefaults);
|
||||
}
|
||||
// 自定义默认值提取逻辑
|
||||
function getCustomDefaultValue(rule: any): any {
|
||||
if (rule instanceof ZodString) {
|
||||
return ''; // 默认为空字符串
|
||||
} else if (rule instanceof ZodNumber) {
|
||||
return null; // 默认为 null(避免显示 0)
|
||||
} else if (rule instanceof ZodObject) {
|
||||
// 递归提取嵌套对象的默认值
|
||||
const defaultValues: Record<string, any> = {};
|
||||
for (const [key, valueSchema] of Object.entries(rule.shape)) {
|
||||
defaultValues[key] = getCustomDefaultValue(valueSchema);
|
||||
}
|
||||
return defaultValues;
|
||||
} else if (rule instanceof ZodIntersection) {
|
||||
// 对于交集类型,从schema 提取默认值
|
||||
const leftDefaultValue = getCustomDefaultValue(rule._def.left);
|
||||
const rightDefaultValue = getCustomDefaultValue(rule._def.right);
|
||||
|
||||
// 如果左右两边都能提取默认值,合并它们
|
||||
if (
|
||||
typeof leftDefaultValue === 'object' &&
|
||||
typeof rightDefaultValue === 'object'
|
||||
) {
|
||||
return { ...leftDefaultValue, ...rightDefaultValue };
|
||||
}
|
||||
|
||||
// 否则优先使用左边的默认值
|
||||
return leftDefaultValue ?? rightDefaultValue;
|
||||
} else {
|
||||
return undefined; // 其他类型不提供默认值
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
delegatedSlots,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user