Compare commits
30 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
4173264805 | |
|
|
46540a7329 | |
|
|
13fd0ea16c | |
|
|
f7016466ee | |
|
|
0cd865e211 | |
|
|
64428b9b11 | |
|
|
aed31a5a4e | |
|
|
b3e196f001 | |
|
|
b2c117f544 | |
|
|
01391ee5a1 | |
|
|
3572ce1538 | |
|
|
d1e1256202 | |
|
|
b7776c5148 | |
|
|
2d1519eca7 | |
|
|
93b5618b52 | |
|
|
639d2e1525 | |
|
|
26646d42f7 | |
|
|
17fa8eb93b | |
|
|
8250894a50 | |
|
|
a72b8acaf9 | |
|
|
a46c85d77d | |
|
|
fdc5b02c30 | |
|
|
476aa068d7 | |
|
|
bb6057cac3 | |
|
|
abbbbfb955 | |
|
|
79c87c9f46 | |
|
|
f815dcf3ae | |
|
|
1197efea26 | |
|
|
2a83f1d666 | |
|
|
4b3d2d21ed |
|
|
@ -19,6 +19,7 @@ permissions:
|
|||
|
||||
jobs:
|
||||
post-update:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
# if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ env:
|
|||
|
||||
jobs:
|
||||
version:
|
||||
if: (github.event.pull_request.merged || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: (github.event.pull_request.merged || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
# if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ env:
|
|||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
@ -55,6 +56,7 @@ jobs:
|
|||
|
||||
lint:
|
||||
name: Lint
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
@ -77,6 +79,7 @@ jobs:
|
|||
|
||||
check:
|
||||
name: Check
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
|
|
@ -106,6 +109,7 @@ jobs:
|
|||
|
||||
ci-ok:
|
||||
name: CI OK
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, check, lint]
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ on:
|
|||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ on:
|
|||
jobs:
|
||||
deploy-playground-ftp:
|
||||
name: Deploy Push Playground Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
|
||||
deploy-docs-ftp:
|
||||
name: Deploy Push Docs Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -63,7 +63,7 @@ jobs:
|
|||
|
||||
deploy-antd-ftp:
|
||||
name: Deploy Push Antd Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -94,7 +94,7 @@ jobs:
|
|||
|
||||
deploy-ele-ftp:
|
||||
name: Deploy Push Element Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -125,7 +125,7 @@ jobs:
|
|||
|
||||
deploy-naive-ftp:
|
||||
name: Deploy Push Naive Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ jobs:
|
|||
# write permission is required for autolabeler
|
||||
# otherwise, read permission is required at least
|
||||
pull-requests: write
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
|
|
|
|||
|
|
@ -3,23 +3,29 @@ name: Issue Close Require
|
|||
|
||||
# 触发条件:每天零点
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 步骤1:关闭未活动的 Issues
|
||||
# 关闭未活动的 Issues
|
||||
- name: Close Inactive Issues
|
||||
uses: actions-cool/issues-helper@v3
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
actions: 'close-issues' # 执行动作:关闭 Issues
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # GitHub Token,用于认证
|
||||
labels: 'needs reproduction' # 目标标签
|
||||
inactive-day: 3 # 未活动天数阈值
|
||||
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
|
||||
stale-issue-label: needs-reproduction # Label that flags an issue as stale.
|
||||
only-labels: needs-reproduction # Only process these issues
|
||||
days-before-issue-close: 3
|
||||
ignore-updates: true
|
||||
remove-stale-when-updated: false
|
||||
close-issue-message: This issue was closed because it was open for 3 days without a valid reproduction.
|
||||
close-issue-label: closed-by-action
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ permissions:
|
|||
|
||||
jobs:
|
||||
reply-labeled:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: remove enhancement pending
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ permissions:
|
|||
|
||||
jobs:
|
||||
action:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-inactive-days: '30'
|
||||
issue-inactive-days: '14'
|
||||
issue-lock-reason: ''
|
||||
pr-inactive-days: '30'
|
||||
pr-lock-reason: ''
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ permissions:
|
|||
jobs:
|
||||
build:
|
||||
name: Create Release
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ on:
|
|||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
name: Semantic Pull Request
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate PR title
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ on:
|
|||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ ports:
|
|||
onOpen: open-preview
|
||||
tasks:
|
||||
- init: corepack enable && pnpm install
|
||||
command: pnpm run dev
|
||||
command: pnpm run dev:play
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ pnpm build
|
|||
|
||||
[@Vben](https://github.com/anncwb)
|
||||
|
||||
## スター歴史
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## 寄付
|
||||
|
||||
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ Support modern browsers, not IE
|
|||
|
||||
[@Vben](https://github.com/anncwb)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## Donate
|
||||
|
||||
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ pnpm dev
|
|||
pnpm build
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||
|
||||
## 如何贡献
|
||||
|
||||
非常欢迎你的加入 或者提交一个 Pull Request。
|
||||
|
|
@ -120,6 +124,10 @@ pnpm build
|
|||
|
||||
[@Vben](https://github.com/anncwb)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## 捐赠
|
||||
|
||||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
|
||||
|
|
@ -128,10 +136,6 @@ pnpm build
|
|||
|
||||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||
|
||||
## 更新日志
|
||||
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||
|
||||
## Contributor
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dataItem = {
|
||||
id: faker.string.uuid(),
|
||||
imageUrl: faker.image.avatar(),
|
||||
imageUrl2: faker.image.avatar(),
|
||||
open: faker.datatype.boolean(),
|
||||
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
|
||||
productName: faker.commerce.productName(),
|
||||
price: faker.commerce.price(),
|
||||
currency: faker.finance.currencyCode(),
|
||||
quantity: faker.number.int({ min: 1, max: 100 }),
|
||||
available: faker.datatype.boolean(),
|
||||
category: faker.commerce.department(),
|
||||
releaseDate: faker.date.past(),
|
||||
rating: faker.number.float({ min: 1, max: 5 }),
|
||||
description: faker.commerce.productDescription(),
|
||||
weight: faker.number.float({ min: 0.1, max: 10 }),
|
||||
color: faker.color.human(),
|
||||
inProduction: faker.datatype.boolean(),
|
||||
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
|
||||
};
|
||||
|
||||
dataList.push(dataItem);
|
||||
}
|
||||
|
||||
return dataList;
|
||||
}
|
||||
|
||||
const mockData = generateMockDataList(100);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
await sleep(600);
|
||||
|
||||
const { page, pageSize } = getQuery(event);
|
||||
return usePageResponseSuccess(page as string, pageSize as string, mockData);
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"start": "nitro dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "catalog:",
|
||||
"jsonwebtoken": "catalog:",
|
||||
"nitropack": "catalog:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,6 +9,27 @@ export function useResponseSuccess<T = any>(data: T) {
|
|||
};
|
||||
}
|
||||
|
||||
export function usePageResponseSuccess<T = any>(
|
||||
page: number | string,
|
||||
pageSize: number | string,
|
||||
list: T[],
|
||||
{ message = 'ok' } = {},
|
||||
) {
|
||||
const pageData = pagination(
|
||||
Number.parseInt(`${page}`),
|
||||
Number.parseInt(`${pageSize}`),
|
||||
list,
|
||||
);
|
||||
|
||||
return {
|
||||
...useResponseSuccess({
|
||||
items: pageData,
|
||||
total: list.length,
|
||||
}),
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function useResponseError(message: string, error: any = null) {
|
||||
return {
|
||||
code: -1,
|
||||
|
|
@ -27,3 +48,18 @@ export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
|
|||
setResponseStatus(event, 401);
|
||||
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
|
||||
}
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function pagination<T = any>(
|
||||
pageNo: number,
|
||||
pageSize: number,
|
||||
array: T[],
|
||||
): T[] {
|
||||
const offset = (pageNo - 1) * Number(pageSize);
|
||||
return offset + Number(pageSize) >= array.length
|
||||
? array.slice(offset)
|
||||
: array.slice(offset, offset + Number(pageSize));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
|
|
@ -57,6 +58,16 @@ export type FormComponentType =
|
|||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
) => {
|
||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||
const placeholder = props?.placeholder || $t(`placeholder.${type}`);
|
||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化表单组件,并注册到form组件内部
|
||||
setupVbenForm<FormComponentType>({
|
||||
components: {
|
||||
|
|
@ -73,20 +84,20 @@ setupVbenForm<FormComponentType>({
|
|||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
},
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './form';
|
||||
export * from './vxe-table';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: true,
|
||||
minHeight: 180,
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
|
|
@ -10,10 +10,6 @@ export namespace AuthApi {
|
|||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
desc: string;
|
||||
realName: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
|
|
@ -68,7 +70,7 @@ function createRequestClient(baseURL: string) {
|
|||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor({
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
|
|
@ -93,7 +95,10 @@ function createRequestClient(baseURL: string) {
|
|||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
|
||||
errorMessageResponseInterceptor((msg: string, _error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
message.error(msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
|
|
@ -42,6 +43,16 @@ export type FormComponentType =
|
|||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
) => {
|
||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||
const placeholder = props?.placeholder || $t(`placeholder.${type}`);
|
||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化表单组件,并注册到form组件内部
|
||||
setupVbenForm<FormComponentType>({
|
||||
components: {
|
||||
|
|
@ -56,14 +67,14 @@ setupVbenForm<FormComponentType>({
|
|||
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: ElDivider,
|
||||
Input: ElInput,
|
||||
InputNumber: ElInputNumber,
|
||||
Input: withDefaultPlaceholder(ElInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
|
||||
RadioGroup: ElRadioGroup,
|
||||
Select: ElSelect,
|
||||
Select: withDefaultPlaceholder(ElSelect, 'select'),
|
||||
Space: ElSpace,
|
||||
Switch: ElSwitch,
|
||||
TimePicker: ElTimePicker,
|
||||
TreeSelect: ElTreeSelect,
|
||||
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
|
||||
Upload: ElUpload,
|
||||
},
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './form';
|
||||
export * from './vxe-table';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { ElButton, ElImage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: true,
|
||||
minHeight: 180,
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
const src = row[column.field];
|
||||
return h(ElImage, { src, previewSrcList: [src] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
ElButton,
|
||||
{ size: 'small', link: true },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
|
|
@ -10,10 +10,6 @@ export namespace AuthApi {
|
|||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
desc: string;
|
||||
realName: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
|
|
@ -68,7 +70,7 @@ function createRequestClient(baseURL: string) {
|
|||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor({
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
|
|
@ -93,7 +95,10 @@ function createRequestClient(baseURL: string) {
|
|||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string) => ElMessage.error(msg)),
|
||||
errorMessageResponseInterceptor((msg: string, _error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
ElMessage.error(msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
ElMessage,
|
||||
ElNotification,
|
||||
ElSpace,
|
||||
ElTable,
|
||||
} from 'element-plus';
|
||||
|
||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
|
@ -38,6 +39,14 @@ function notify(type: NotificationType) {
|
|||
type,
|
||||
});
|
||||
}
|
||||
const tableData = [
|
||||
{ prop1: '1', prop2: 'A' },
|
||||
{ prop1: '2', prop2: 'B' },
|
||||
{ prop1: '3', prop2: 'C' },
|
||||
{ prop1: '4', prop2: 'D' },
|
||||
{ prop1: '5', prop2: 'E' },
|
||||
{ prop1: '6', prop2: 'F' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -74,5 +83,11 @@ function notify(type: NotificationType) {
|
|||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5">
|
||||
<ElTable :data="tableData" stripe>
|
||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
|
|
@ -43,6 +44,16 @@ export type FormComponentType =
|
|||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
) => {
|
||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||
const placeholder = props?.placeholder || $t(`placeholder.${type}`);
|
||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化表单组件,并注册到form组件内部
|
||||
setupVbenForm<FormComponentType>({
|
||||
components: {
|
||||
|
|
@ -62,17 +73,18 @@ setupVbenForm<FormComponentType>({
|
|||
);
|
||||
},
|
||||
Divider: NDivider,
|
||||
Input: NInput,
|
||||
InputNumber: NInputNumber,
|
||||
Input: withDefaultPlaceholder(NInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
|
||||
RadioGroup: NRadioGroup,
|
||||
Select: NSelect,
|
||||
Select: withDefaultPlaceholder(NSelect, 'select'),
|
||||
Space: NSpace,
|
||||
Switch: NSwitch,
|
||||
TimePicker: NTimePicker,
|
||||
TreeSelect: NTreeSelect,
|
||||
TreeSelect: withDefaultPlaceholder(NTreeSelect, 'select'),
|
||||
Upload: NUpload,
|
||||
},
|
||||
config: {
|
||||
disabledOnChangeListener: true,
|
||||
baseModelPropName: 'value',
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './form';
|
||||
export * from './naive';
|
||||
export * from './vxe-table';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { NButton, NImage } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: true,
|
||||
minHeight: 180,
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
return h(NImage, { src: row[column.field] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
NButton,
|
||||
{ size: 'small', type: 'primary', quaternary: true },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
|
|
@ -10,10 +10,6 @@ export namespace AuthApi {
|
|||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
desc: string;
|
||||
realName: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
|
|
@ -67,7 +69,7 @@ function createRequestClient(baseURL: string) {
|
|||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor({
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
|
|
@ -92,7 +94,10 @@ function createRequestClient(baseURL: string) {
|
|||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
|
||||
errorMessageResponseInterceptor((msg: string, _error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
message.error(msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:shield-key-outline',
|
||||
title: $t('page.demos.table'),
|
||||
},
|
||||
name: 'Table',
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"intlify",
|
||||
"mkdist",
|
||||
"mockjs",
|
||||
"vitejs",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"nprogress",
|
||||
|
|
|
|||
|
|
@ -133,12 +133,19 @@ function sidebarCommercial(): DefaultTheme.SidebarItem[] {
|
|||
function nav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
activeMatch: '^/en/(guide|components)/',
|
||||
text: 'Doc',
|
||||
items: [
|
||||
{
|
||||
activeMatch: '^/en/guide/',
|
||||
link: '/en/guide/introduction/vben',
|
||||
text: 'Guide',
|
||||
},
|
||||
// {
|
||||
// activeMatch: '^/en/components/',
|
||||
// link: '/en/components/introduction',
|
||||
// text: 'Components',
|
||||
// },
|
||||
{
|
||||
text: 'Historical Versions',
|
||||
items: [
|
||||
|
|
|
|||
|
|
@ -176,13 +176,16 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
|||
function nav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
activeMatch: '^/(guide|components)/',
|
||||
text: '文档',
|
||||
items: [
|
||||
{
|
||||
activeMatch: '^/guide/',
|
||||
link: '/guide/introduction/vben',
|
||||
text: '指南',
|
||||
},
|
||||
{
|
||||
activeMatch: '^/components/',
|
||||
link: '/components/introduction',
|
||||
text: '组件',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,3 +9,14 @@ html.dark {
|
|||
.form-valid-error p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 顶部导航栏选中项样式 */
|
||||
.VPNavBarMenuLink,
|
||||
.VPNavBarMenuGroup {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active,
|
||||
.VPNavBarMenuGroup.active {
|
||||
border-bottom-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/docs",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vitepress build",
|
||||
|
|
@ -13,9 +13,7 @@
|
|||
"dependencies": {
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/hooks": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/preferences": "workspace:*",
|
||||
"@vben/styles": "workspace:*",
|
||||
"ant-design-vue": "catalog:",
|
||||
"lucide-vue-next": "catalog:",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import type {
|
|||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
|
|
@ -84,6 +85,16 @@ export type FormComponentType =
|
|||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
) => {
|
||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||
const placeholder = props?.placeholder || $t(`placeholder.${type}`);
|
||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化表单组件,并注册到form组件内部
|
||||
setupVbenForm<FormComponentType>({
|
||||
components: {
|
||||
|
|
@ -100,26 +111,27 @@ setupVbenForm<FormComponentType>({
|
|||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
},
|
||||
config: {
|
||||
// 是否禁用onChange事件监听,naive ui组件库默认不需要监听onChange事件,否则会在控制台报错
|
||||
disabledOnChangeListener: true,
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
|
|
@ -229,7 +241,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
|||
| --- | --- | --- |
|
||||
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` |
|
||||
| resetForm | 重置表单 | `()=>Promise<void>` |
|
||||
| setValues | 设置表单值 | `()=>Promise<Record<string,any>>` |
|
||||
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
|
||||
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
|
||||
| validate | 表单校验 | `()=>Promise<void>` |
|
||||
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
|
||||
|
|
@ -255,6 +267,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
|||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
|
||||
| collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
|
||||
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
|
||||
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
|
||||
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
|
||||
| schema | 表单项的每一项配置 | `FormSchema` | - |
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
|
|||
```json
|
||||
{
|
||||
"scripts": {
|
||||
// Install dependencies
|
||||
"bootstrap": "pnpm install",
|
||||
// Build the project
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
|
||||
// Build the project with analysis
|
||||
|
|
@ -77,7 +75,7 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
|
|||
// Check types
|
||||
"check:type": "turbo run typecheck",
|
||||
// Clean the project (delete node_modules, dist, .turbo, etc.)
|
||||
"clean": "vsh clean",
|
||||
"clean": "node ./scripts/clean.mjs",
|
||||
// Commit code
|
||||
"commit": "czg",
|
||||
// Start the project (by default, the dev scripts of all packages in the entire repository will run)
|
||||
|
|
@ -97,7 +95,7 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
|
|||
// Lint code
|
||||
"lint": "vsh lint",
|
||||
// After installing dependencies, execute the stub script for all packages
|
||||
"postinstall": "turbo run stub",
|
||||
"postinstall": "pnpm -r run stub --if-present",
|
||||
// Only allow using pnpm
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
// Install husky
|
||||
|
|
@ -107,9 +105,9 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
|
|||
// Package specification check
|
||||
"publint": "vsh publint",
|
||||
// Delete all node_modules, yarn.lock, package.lock.json, and reinstall dependencies
|
||||
"reinstall": "pnpm clean --del-lock && pnpm bootstrap",
|
||||
"reinstall": "pnpm clean --del-lock && pnpm install",
|
||||
// Run vitest unit tests
|
||||
"test:unit": "vitest",
|
||||
"test:unit": "vitest run --dom",
|
||||
// Update project dependencies
|
||||
"update:deps": " pnpm update --latest --recursive",
|
||||
// Changeset generation and versioning
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ The `src/api/request.ts` within the application can be configured according to t
|
|||
/**
|
||||
* This file can be adjusted according to business logic
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
|
|
@ -227,7 +229,7 @@ function createRequestClient(baseURL: string) {
|
|||
});
|
||||
|
||||
// Deal Response Data
|
||||
client.addResponseInterceptor({
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
|
|
@ -253,7 +255,10 @@ function createRequestClient(baseURL: string) {
|
|||
|
||||
// Generic error handling; if none of the above error handling logic is triggered, it will fall back to this.
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
|
||||
errorMessageResponseInterceptor((msg: string, _error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
message.error(msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ If you want to adjust the content of the login form, you can configure the `Auth
|
|||
```vue
|
||||
<AuthenticationLogin
|
||||
:loading="authStore.loginLoading"
|
||||
password-placeholder="123456"
|
||||
username-placeholder="vben"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -42,23 +42,6 @@ Check the dependency situation of the entire project and output `unused dependen
|
|||
pnpm vsh check-dep
|
||||
```
|
||||
|
||||
### vsh clean
|
||||
|
||||
Delete the project's `node_modules`, `dist`, `.turbo` directories, etc., to clean the project.
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
pnpm vsh clean
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `-r,--recursive` | Recursively delete the entire project, default `true` |
|
||||
| `--del-lock` | Whether to delete the `pnpm-lock.yaml` file, default `true` |
|
||||
|
||||
### vsh lint
|
||||
|
||||
Lint checks the project to see if the code in the project conforms to standards.
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
|
|||
```json
|
||||
{
|
||||
"scripts": {
|
||||
// 安装依赖
|
||||
"bootstrap": "pnpm install",
|
||||
// 构建项目
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
|
||||
// 构建项目并分析
|
||||
|
|
@ -77,7 +75,7 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
|
|||
// 检查类型
|
||||
"check:type": "turbo run typecheck",
|
||||
// 清理项目(删除node_modules、dist、.turbo)等目录
|
||||
"clean": "vsh clean",
|
||||
"clean": "node ./scripts/clean.mjs",
|
||||
// 提交代码
|
||||
"commit": "czg",
|
||||
// 启动项目(默认会运行整个仓库所有包的dev脚本)
|
||||
|
|
@ -97,7 +95,7 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
|
|||
// lint 代码
|
||||
"lint": "vsh lint",
|
||||
// 依赖安装完成之后,执行所有包的stub脚本
|
||||
"postinstall": "turbo run stub",
|
||||
"postinstall": "pnpm -r run stub --if-present",
|
||||
// 只允许使用pnpm
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
// husky的安装
|
||||
|
|
@ -107,9 +105,9 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
|
|||
// 包规范检查
|
||||
"publint": "vsh publint",
|
||||
// 删除所有的node_modules、yarn.lock、package.lock.json,重新安装依赖
|
||||
"reinstall": "pnpm clean --del-lock && pnpm bootstrap",
|
||||
"reinstall": "pnpm clean --del-lock && pnpm install",
|
||||
// 运行 vitest 单元测试
|
||||
"test:unit": "vitest",
|
||||
"test:unit": "vitest run --dom",
|
||||
// 更新项目依赖
|
||||
"update:deps": " pnpm update --latest --recursive",
|
||||
// changeset生成提交集
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ export async function deleteUserApi(user: UserInfo) {
|
|||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
|
|
@ -230,7 +232,7 @@ function createRequestClient(baseURL: string) {
|
|||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor({
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
|
|
@ -256,7 +258,10 @@ function createRequestClient(baseURL: string) {
|
|||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
|
||||
errorMessageResponseInterceptor((msg: string, _error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
message.error(msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 登录
|
||||
|
||||
本文介绍如何去改造自己的应用程序登录页。
|
||||
本文介绍如何去改造自己的应用程序登录页以及如何快速的对接登录页面接口。
|
||||
|
||||
## 登录页面调整
|
||||
|
||||
如果你想调整登录页面的标题、描述和图标以及工具栏,你可以通过配置 `AuthPageLayout` 组件的 `props` 参数来实现。
|
||||
如果你想调整登录页面的标题、描述和图标以及工具栏,你可以通过配置 `AuthPageLayout` 组件的参数来实现。
|
||||
|
||||

|
||||
|
||||
|
|
@ -30,8 +34,6 @@
|
|||
```vue
|
||||
<AuthenticationLogin
|
||||
:loading="authStore.loginLoading"
|
||||
password-placeholder="123456"
|
||||
username-placeholder="vben"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
```
|
||||
|
|
@ -108,8 +110,111 @@
|
|||
|
||||
:::
|
||||
|
||||
::: tip
|
||||
::: tip Note
|
||||
|
||||
如果这些配置不能满足你的需求,你可以自行实现登录表单及相关登录逻辑。
|
||||
如果这些配置不能满足你的需求,你可以自行实现登录表单及相关登录逻辑或者给我们提交 `PR`。
|
||||
|
||||
:::
|
||||
|
||||
## 接口对接流程
|
||||
|
||||
这里将会快速的介绍如何快速对接自己的后端。
|
||||
|
||||
### 前置条件
|
||||
|
||||
- 首先文档用的后端服务,接口返回的格式统一如下:
|
||||
|
||||
```ts
|
||||
interface HttpResponse<T = any> {
|
||||
/**
|
||||
* 0 表示成功 其他表示失败
|
||||
* 0 means success, others means fail
|
||||
*/
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
如果你不符合这个格式,你需要先阅读 [服务端交互](../essentials/server.md) 文档,改造你的`request.ts`配置。
|
||||
|
||||
- 其次你需要在先将本地代理地址改为你的真实后端地址,你可以在应用下的 `vite.config.mts` 内配置:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig(async () => {
|
||||
return {
|
||||
vite: {
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// 这里改为你的真实接口地址
|
||||
target: 'http://localhost:5320/api',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 登录接口
|
||||
|
||||
为了能正常登录,你的后端最少需要提供 `2-3` 个接口:
|
||||
|
||||
- 登录接口
|
||||
|
||||
接口地址可在应用下的 `src/api/core/auth` 内修改,以下为默认接口地址:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||
}
|
||||
|
||||
/** 只需要保证登录接口返回值有 `accessToken` 字段即可 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
}
|
||||
```
|
||||
|
||||
- 获取用户信息接口
|
||||
|
||||
接口地址可在应用下的 `src/api/core/user` 内修改,以下为默认接口地址:
|
||||
|
||||
```ts
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/user/info');
|
||||
}
|
||||
|
||||
/** 只需要保证登录接口返回值有以下字段即可,多的字段可以自行使用 */
|
||||
export interface UserInfo {
|
||||
roles: string[];
|
||||
realName: string;
|
||||
}
|
||||
```
|
||||
|
||||
- 获取权限码 (可选)
|
||||
|
||||
这个接口用于获取用户的权限码,权限码是用于控制用户的权限的,接口地址可在应用下的 `src/api/core/auth` 内修改,以下为默认接口地址:
|
||||
|
||||
```ts
|
||||
export async function getAccessCodesApi() {
|
||||
return requestClient.get<string[]>('/auth/codes');
|
||||
}
|
||||
```
|
||||
|
||||
如果你不需要这个权限,你只需要把代码改为返回一个空数组即可。
|
||||
|
||||
```ts {2}
|
||||
export async function getAccessCodesApi() {
|
||||
// 这里返回一个空数组即可
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 精简版本
|
||||
|
||||
从 `5.0` 版本开始,我们不再提供精简的仓库或者分支。我们的目标是提供一个更加一致的开发体验,同时减少维护成本。在这里,我们将如何介绍自己的项目,如何去精简以及移除不需要的功能。
|
||||
|
|
@ -74,3 +78,17 @@ pnpm install
|
|||
- `.github` 文件夹用于存放 GitHub 的配置文件
|
||||
- `.vscode` 文件夹用于存放 VSCode 的配置文件,如果你使用其他编辑器,可以删除
|
||||
- `./scripts/deploy` 文件夹用于存放部署脚本,如果你不需要docker部署,可以删除
|
||||
|
||||
## 应用精简
|
||||
|
||||
当你确定了某个应用,你还可以进一步精简:
|
||||
|
||||
### 删除不需要的路由及页面
|
||||
|
||||
- 在应用的 `src/router/routes` 文件中,你可以删除不需要的路由。其中 `core` 文件夹内,如果只需要登录和忘记密码,你可以删除其他路由,如忘记密码、注册等。路由删除后,你可以删除对应的页面文件,在 `src/views/_core` 文件夹中。
|
||||
|
||||
- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以删除对应的页面文件,在 `src/views` 文件夹中。
|
||||
|
||||
### 删除不需要的组件
|
||||
|
||||
- 在应用的 `packages/effects/common-ui/src/ui` 文件夹中,你可以删除不需要的组件,如`about`、`dashboard` 目录等。删除之前请先确保你的路由中没有引用到这些组件。
|
||||
|
|
|
|||
|
|
@ -42,16 +42,6 @@ pnpm vsh check-circular
|
|||
pnpm vsh check-dep
|
||||
```
|
||||
|
||||
### vsh clean
|
||||
|
||||
删除项目的`node_modules`、`dist`、`.turbo`等目录,清理项目。
|
||||
|
||||
#### 用法
|
||||
|
||||
```bash
|
||||
pnpm vsh clean
|
||||
```
|
||||
|
||||
#### 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/commitlint-config",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export async function node(): Promise<Linter.Config[]> {
|
|||
'vite',
|
||||
'@vue/test-utils',
|
||||
'@vben/tailwind-config',
|
||||
'@playwright/test',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -15,10 +15,17 @@ const customConfig: Linter.Config[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/effects/**/**', 'packages/types/**/**'],
|
||||
files: [
|
||||
'apps/**/**',
|
||||
'packages/effects/**/**',
|
||||
'packages/utils/**/**',
|
||||
'packages/types/**/**',
|
||||
'packages/locales/**/**',
|
||||
],
|
||||
ignores: restrictedImportIgnores,
|
||||
rules: {
|
||||
'perfectionist/sort-interfaces': 'off',
|
||||
'perfectionist/sort-objects': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -135,7 +142,15 @@ const customConfig: Linter.Config[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
files: ['internal/**/**'],
|
||||
files: ['**/**/playwright.config.ts'],
|
||||
rules: {
|
||||
'n/prefer-global/buffer': 'off',
|
||||
'n/prefer-global/process': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['internal/**/**', 'scripts/**/**'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/stylelint-config",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"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.3.0",
|
||||
"version": "5.3.2",
|
||||
"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.3.0",
|
||||
"version": "5.3.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const shadcnUiColors = {
|
|||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
hover: 'hsl(var(--accent-hover))',
|
||||
lighter: 'has(val(--accent-lighter))',
|
||||
},
|
||||
background: {
|
||||
deep: 'hsl(var(--background-deep))',
|
||||
|
|
@ -90,7 +91,10 @@ const customColors = {
|
|||
main: {
|
||||
DEFAULT: 'hsl(var(--main))',
|
||||
},
|
||||
overlay: 'hsl(var(--overlay))',
|
||||
overlay: {
|
||||
content: 'hsl(var(--overlay-content))',
|
||||
DEFAULT: 'hsl(var(--overlay))',
|
||||
},
|
||||
red: {
|
||||
...createColorsPalette('red'),
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/tsconfig",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"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.3.0",
|
||||
"version": "5.3.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
|
@ -53,6 +53,7 @@
|
|||
"vite": "catalog:",
|
||||
"vite-plugin-compression": "catalog:",
|
||||
"vite-plugin-dts": "catalog:",
|
||||
"vite-plugin-html": "catalog:"
|
||||
"vite-plugin-html": "catalog:",
|
||||
"vite-plugin-lazy-import": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
|
|||
},
|
||||
pwa: true,
|
||||
pwaOptions: getDefaultPwaOptions(appTitle),
|
||||
vxeTableLazyImport: true,
|
||||
...envConfig,
|
||||
...application,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { viteMetadataPlugin } from './inject-metadata';
|
|||
import { viteLicensePlugin } from './license';
|
||||
import { viteNitroMockPlugin } from './nitro-mock';
|
||||
import { vitePrintPlugin } from './print';
|
||||
import { viteVxeTableImportsPlugin } from './vxe-table';
|
||||
|
||||
/**
|
||||
* 获取条件成立的 vite 插件
|
||||
|
|
@ -110,6 +111,7 @@ async function loadApplicationPlugins(
|
|||
printInfoMap,
|
||||
pwa,
|
||||
pwaOptions,
|
||||
vxeTableLazyImport,
|
||||
...commonOptions
|
||||
} = options;
|
||||
|
||||
|
|
@ -135,6 +137,12 @@ async function loadApplicationPlugins(
|
|||
return [await vitePrintPlugin({ infoMap: printInfoMap })];
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: vxeTableLazyImport,
|
||||
plugins: async () => {
|
||||
return [await viteVxeTableImportsPlugin()];
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: nitroMock,
|
||||
plugins: async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import type { PluginOption } from 'vite';
|
||||
|
||||
import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import';
|
||||
|
||||
async function viteVxeTableImportsPlugin(): Promise<PluginOption> {
|
||||
return [
|
||||
lazyImport({
|
||||
resolvers: [
|
||||
VxeResolver({
|
||||
libraryName: 'vxe-table',
|
||||
}),
|
||||
VxeResolver({
|
||||
libraryName: 'vxe-pc-ui',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export { viteVxeTableImportsPlugin };
|
||||
|
|
@ -123,6 +123,8 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
|
|||
pwa?: boolean;
|
||||
/** pwa 插件配置 */
|
||||
pwaOptions?: Partial<PwaPluginOptions>;
|
||||
/** 是否开启vxe-table懒加载 */
|
||||
vxeTableLazyImport?: boolean;
|
||||
}
|
||||
|
||||
interface LibraryPluginOptions extends CommonPluginOptions {
|
||||
|
|
|
|||
26
package.json
26
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vben-admin-pro",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"monorepo",
|
||||
|
|
@ -25,11 +25,10 @@
|
|||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"bootstrap": "pnpm install",
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
|
||||
"build:analyze": "turbo build:analyze",
|
||||
"build:docker": "./build-local-docker-image.sh",
|
||||
"build:antd": "pnpm run build --filter=@vben/web-antd",
|
||||
"build:docker": "./build-local-docker-image.sh",
|
||||
"build:docs": "pnpm run build --filter=@vben/docs",
|
||||
"build:ele": "pnpm run build --filter=@vben/web-ele",
|
||||
"build:naive": "pnpm run build --filter=@vben/web-naive",
|
||||
|
|
@ -40,7 +39,7 @@
|
|||
"check:cspell": "cspell lint **/*.ts **/README.md .changeset/*.md --no-progress",
|
||||
"check:dep": "vsh check-dep",
|
||||
"check:type": "turbo run typecheck",
|
||||
"clean": "vsh clean",
|
||||
"clean": "node ./scripts/clean.mjs",
|
||||
"commit": "czg",
|
||||
"dev": "turbo-run dev",
|
||||
"dev:antd": "pnpm -F @vben/web-antd run dev",
|
||||
|
|
@ -50,20 +49,21 @@
|
|||
"dev:play": "pnpm -F @vben/playground run dev",
|
||||
"format": "vsh lint --format",
|
||||
"lint": "vsh lint",
|
||||
"postinstall": "turbo run stub",
|
||||
"postinstall": "pnpm -r run stub --if-present",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prepare": "is-ci || husky",
|
||||
"preview": "turbo-run preview",
|
||||
"publint": "vsh publint",
|
||||
"reinstall": "pnpm clean --del-lock && pnpm bootstrap",
|
||||
"test:unit": "vitest",
|
||||
"reinstall": "pnpm clean --del-lock && pnpm install",
|
||||
"test:unit": "vitest run --dom",
|
||||
"test:e2e": "turbo run test:e2e",
|
||||
"update:deps": "pnpm update --latest --recursive",
|
||||
"version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "catalog:",
|
||||
"@changesets/cli": "catalog:",
|
||||
"@types/jsdom": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@vben/commitlint-config": "workspace:*",
|
||||
"@vben/eslint-config": "workspace:*",
|
||||
|
|
@ -80,10 +80,11 @@
|
|||
"autoprefixer": "catalog:",
|
||||
"cross-env": "catalog:",
|
||||
"cspell": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"husky": "catalog:",
|
||||
"is-ci": "catalog:",
|
||||
"jsdom": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"playwright": "catalog:",
|
||||
"rimraf": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"turbo": "catalog:",
|
||||
|
|
@ -98,7 +99,7 @@
|
|||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.5.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.11.0",
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
|
|
@ -109,12 +110,11 @@
|
|||
"@ctrl/tinycolor": "4.1.0",
|
||||
"clsx": "2.1.1",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "3.5.7"
|
||||
"vue": "3.5.10"
|
||||
},
|
||||
"neverBuiltDependencies": [
|
||||
"canvas",
|
||||
"node-gyp",
|
||||
"playwright"
|
||||
"node-gyp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben-core/design",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -82,11 +82,11 @@
|
|||
@apply opacity-100;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
/* input:-webkit-autofill {
|
||||
@apply border-none;
|
||||
|
||||
box-shadow: 0 0 0 1000px transparent inset;
|
||||
}
|
||||
} */
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
|
||||
--accent: 216 5% 19%;
|
||||
--accent-lighter: 216 5% 11%;
|
||||
--accent-hover: 216 5% 24%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
|
|
@ -78,6 +79,7 @@
|
|||
|
||||
/* 遮罩颜色 */
|
||||
--overlay: 0deg 0% 0% / 40%;
|
||||
--overlay-content: 0deg 0% 0% / 40%;
|
||||
|
||||
/* 基本文字大小 */
|
||||
--font-size-base: 16px;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
|
||||
--accent: 240 5% 96%;
|
||||
--accent-lighter: 240 0% 98%;
|
||||
--accent-hover: 200deg 10% 90%;
|
||||
--accent-foreground: 240 6% 10%;
|
||||
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
|
||||
/* 遮罩颜色 */
|
||||
--overlay: 0 0% 0% / 45%;
|
||||
--overlay-light: 0 0% 95% / 45%;
|
||||
--overlay-content: 0 0% 95% / 45%;
|
||||
|
||||
/* 基本文字大小 */
|
||||
--font-size-base: 16px;
|
||||
|
|
|
|||
|
|
@ -3,5 +3,19 @@ import { defineBuildConfig } from 'unbuild';
|
|||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
entries: [
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
loaders: ['vue'],
|
||||
pattern: ['**/*.vue'],
|
||||
},
|
||||
{
|
||||
builder: 'mkdist',
|
||||
format: 'esm',
|
||||
input: './src',
|
||||
loaders: ['js'],
|
||||
pattern: ['**/*.ts'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben-core/icons",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<svg
|
||||
height="41"
|
||||
viewBox="0 0 64 41"
|
||||
width="64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g fill="none" fill-rule="evenodd" transform="translate(0 1)">
|
||||
<ellipse
|
||||
cx="32"
|
||||
cy="33"
|
||||
fill="hsl(var(--background-deep))"
|
||||
rx="32"
|
||||
ry="7"
|
||||
/>
|
||||
<g fill-rule="nonzero" stroke="hsl(var(--heavy))">
|
||||
<path
|
||||
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
|
||||
/>
|
||||
<path
|
||||
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
|
||||
fill="hsl(var(--accent))"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export { default as EmptyIcon } from './components/empty.vue';
|
||||
export * from './create-icon';
|
||||
export * from './lucide';
|
||||
|
||||
export * from './lucide';
|
||||
export * from '@iconify/vue';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben-core/shared",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
/**
|
||||
* @zh_CN 布局内容高度 css变量
|
||||
* @en_US Layout content height
|
||||
*/
|
||||
/** layout content 组件的高度 */
|
||||
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
|
||||
/** layout content 组件的宽度 */
|
||||
export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
|
||||
/** layout header 组件的高度 */
|
||||
export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
|
||||
/** layout footer 组件的高度 */
|
||||
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
|
||||
|
||||
/**
|
||||
* @zh_CN 默认命名空间
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { bindMethods } from '../util';
|
||||
import { bindMethods, getNestedValue } from '../util';
|
||||
|
||||
class TestClass {
|
||||
public value: string;
|
||||
|
|
@ -78,3 +78,79 @@ describe('bindMethods', () => {
|
|||
expect(value).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNestedValue', () => {
|
||||
interface UserProfile {
|
||||
age: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface UserSettings {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
user: {
|
||||
profile: UserProfile;
|
||||
settings: UserSettings;
|
||||
};
|
||||
}
|
||||
|
||||
const data: Data = {
|
||||
user: {
|
||||
profile: {
|
||||
age: 25,
|
||||
name: 'Alice',
|
||||
},
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should get a nested value when the path is valid', () => {
|
||||
const result = getNestedValue(data, 'user.profile.name');
|
||||
expect(result).toBe('Alice');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent property', () => {
|
||||
const result = getNestedValue(data, 'user.profile.gender');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when accessing a non-existent deep path', () => {
|
||||
const result = getNestedValue(data, 'user.nonexistent.field');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if a middle level is undefined', () => {
|
||||
const result = getNestedValue({ user: undefined }, 'user.profile.name');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the correct value for a nested setting', () => {
|
||||
const result = getNestedValue(data, 'user.settings.theme');
|
||||
expect(result).toBe('dark');
|
||||
});
|
||||
|
||||
it('should work for a single-level path', () => {
|
||||
const result = getNestedValue({ a: 1, b: 2 }, 'b');
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it('should return the entire object if path is empty', () => {
|
||||
expect(() => getNestedValue(data, '')()).toThrow();
|
||||
});
|
||||
|
||||
it('should handle paths with array indexes', () => {
|
||||
const complexData = { list: [{ name: 'Item1' }, { name: 'Item2' }] };
|
||||
const result = getNestedValue(complexData, 'list.1.name');
|
||||
expect(result).toBe('Item2');
|
||||
});
|
||||
|
||||
it('should return undefined when accessing an out-of-bounds array index', () => {
|
||||
const complexData = { list: [{ name: 'Item1' }] };
|
||||
const result = getNestedValue(complexData, 'list.2.name');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,3 +85,11 @@ export function needsScrollbar() {
|
|||
// 在其他情况下,根据 scrollHeight 和 innerHeight 比较判断
|
||||
return doc.scrollHeight > window.innerHeight;
|
||||
}
|
||||
|
||||
export function triggerWindowResize(): void {
|
||||
// 创建一个新的 resize 事件
|
||||
const resizeEvent = new Event('resize');
|
||||
|
||||
// 触发 window 的 resize 事件
|
||||
window.dispatchEvent(resizeEvent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,10 @@
|
|||
import { createDefu } from 'defu';
|
||||
|
||||
export { createDefu as createMerge, defu as merge } from 'defu';
|
||||
|
||||
export const mergeWithArrayOverride = createDefu((originObj, key, updates) => {
|
||||
if (Array.isArray(originObj[key]) && Array.isArray(updates)) {
|
||||
originObj[key] = updates;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,3 +17,28 @@ export function bindMethods<T extends object>(instance: T): void {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取嵌套对象的字段值
|
||||
* @param obj - 要查找的对象
|
||||
* @param path - 用于查找字段的路径,使用小数点分隔
|
||||
* @returns 字段值,或者未找到时返回 undefined
|
||||
*/
|
||||
export function getNestedValue<T>(obj: T, path: string): any {
|
||||
if (typeof path !== 'string' || path.length === 0) {
|
||||
throw new Error('Path must be a non-empty string');
|
||||
}
|
||||
// 把路径字符串按 "." 分割成数组
|
||||
const keys = path.split('.') as (number | string)[];
|
||||
|
||||
let current: any = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
current = current[key as keyof typeof current];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben-core/typings",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"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/composables",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export * from './use-content-style';
|
||||
export * from './use-is-mobile';
|
||||
export * from './use-layout-style';
|
||||
export * from './use-namespace';
|
||||
export * from './use-priority-value';
|
||||
export * from './use-scroll-lock';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|||
import {
|
||||
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
||||
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
|
||||
CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT,
|
||||
CSS_VARIABLE_LAYOUT_HEADER_HEIGHT,
|
||||
} from '@vben-core/shared/constants';
|
||||
import {
|
||||
getElementVisibleRect,
|
||||
|
|
@ -15,7 +17,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
|
|||
/**
|
||||
* @zh_CN content style
|
||||
*/
|
||||
function useContentStyle() {
|
||||
export function useLayoutContentStyle() {
|
||||
let resizeObserver: null | ResizeObserver = null;
|
||||
const contentElement = ref<HTMLDivElement | null>(null);
|
||||
const visibleDomRect = ref<null | VisibleDomRect>(null);
|
||||
|
|
@ -40,7 +42,7 @@ function useContentStyle() {
|
|||
contentHeight.value = `${visibleDomRect.value.height}px`;
|
||||
contentWidth.value = `${visibleDomRect.value.width}px`;
|
||||
},
|
||||
100,
|
||||
16,
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -58,4 +60,28 @@ function useContentStyle() {
|
|||
return { contentElement, overlayStyle, visibleDomRect };
|
||||
}
|
||||
|
||||
export { useContentStyle };
|
||||
export function useLayoutHeaderStyle() {
|
||||
const headerHeight = useCssVar(CSS_VARIABLE_LAYOUT_HEADER_HEIGHT);
|
||||
|
||||
return {
|
||||
getLayoutHeaderHeight: () => {
|
||||
return Number.parseInt(`${headerHeight.value}`, 10);
|
||||
},
|
||||
setLayoutHeaderHeight: (height: number) => {
|
||||
headerHeight.value = `${height}px`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useLayoutFooterStyle() {
|
||||
const footerHeight = useCssVar(CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT);
|
||||
|
||||
return {
|
||||
getLayoutFooterHeight: () => {
|
||||
return Number.parseInt(`${footerHeight.value}`, 10);
|
||||
},
|
||||
setLayoutFooterHeight: (height: number) => {
|
||||
footerHeight.value = `${height}px`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ import { getScrollbarWidth, needsScrollbar } from '@vben-core/shared/utils';
|
|||
|
||||
import {
|
||||
useScrollLock as _useScrollLock,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
} from '@vueuse/core';
|
||||
|
||||
export const SCROLL_FIXED_CLASS = `_scroll__fixed_`;
|
||||
|
|
@ -12,7 +12,7 @@ export function useScrollLock() {
|
|||
const isLocked = _useScrollLock(document.body);
|
||||
const scrollbarWidth = getScrollbarWidth();
|
||||
|
||||
tryOnBeforeMount(() => {
|
||||
tryOnMounted(() => {
|
||||
if (!needsScrollbar()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||
"icpLink": "",
|
||||
},
|
||||
"footer": {
|
||||
"enable": true,
|
||||
"enable": false,
|
||||
"fixed": false,
|
||||
},
|
||||
"header": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben-core/preferences",
|
||||
"version": "5.3.0",
|
||||
"version": "5.3.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const defaultPreferences: Preferences = {
|
|||
icpLink: '',
|
||||
},
|
||||
footer: {
|
||||
enable: true,
|
||||
enable: false,
|
||||
fixed: false,
|
||||
},
|
||||
header: {
|
||||
|
|
|
|||
|
|
@ -171,8 +171,9 @@ class PreferenceManager {
|
|||
// 加载并合并当前存储的偏好设置
|
||||
const mergedPreference = merge(
|
||||
{},
|
||||
overrides,
|
||||
this.loadCachedPreferences() || defaultPreferences,
|
||||
// overrides,
|
||||
this.loadCachedPreferences() || {},
|
||||
this.initialPreferences,
|
||||
);
|
||||
|
||||
// 更新偏好设置
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ function usePreferences() {
|
|||
return isDarkTheme(preferences.theme.mode);
|
||||
});
|
||||
|
||||
const locale = computed(() => {
|
||||
return preferences.app.locale;
|
||||
});
|
||||
|
||||
const isMobile = computed(() => {
|
||||
return appPreferences.value.isMobile;
|
||||
});
|
||||
|
|
@ -218,6 +222,7 @@ function usePreferences() {
|
|||
isSideNav,
|
||||
keepAlive,
|
||||
layout,
|
||||
locale,
|
||||
preferencesButtonPosition,
|
||||
sidebarCollapsed,
|
||||
theme,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,7 @@
|
|||
// 假设这个文件为 FormApi.ts
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FormApi } from '../src/form-api';
|
||||
|
||||
vi.mock('@vben-core/shared/utils', () => ({
|
||||
bindMethods: vi.fn(),
|
||||
createMerge: vi.fn((mergeFn) => {
|
||||
return (stateOrFn: any, prev: any) => {
|
||||
mergeFn(prev, 'key', stateOrFn);
|
||||
return { ...prev, ...stateOrFn };
|
||||
};
|
||||
}),
|
||||
isFunction: (fn: any) => typeof fn === 'function',
|
||||
StateHandler: vi.fn().mockImplementation(() => ({
|
||||
reset: vi.fn(),
|
||||
setConditionTrue: vi.fn(),
|
||||
waitForCondition: vi.fn().mockResolvedValue(true),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('formApi', () => {
|
||||
let formApi: FormApi;
|
||||
|
||||
|
|
@ -126,9 +109,8 @@ describe('formApi', () => {
|
|||
});
|
||||
|
||||
it('should unmount form and reset state', () => {
|
||||
formApi.unmounted();
|
||||
formApi.unmount();
|
||||
expect(formApi.isMounted).toBe(false);
|
||||
expect(formApi.stateHandler.reset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate form', async () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, toRaw, unref } from 'vue';
|
||||
import { computed, toRaw, unref, watch } from 'vue';
|
||||
|
||||
import { useSimpleLocale } from '@vben-core/composables';
|
||||
import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
|
||||
import { cn, isFunction } from '@vben-core/shared/utils';
|
||||
import { cn, isFunction, triggerWindowResize } from '@vben-core/shared/utils';
|
||||
|
||||
import { COMPONENT_MAP } from '../config';
|
||||
import { injectFormProps } from '../use-form-context';
|
||||
|
|
@ -65,6 +65,16 @@ async function handleReset(e: Event) {
|
|||
form.resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => collapsed.value,
|
||||
() => {
|
||||
const props = unref(rootProps);
|
||||
if (props.collapseTriggerResize) {
|
||||
triggerWindowResize();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import type { BaseFormComponentType, VbenFormAdapterOptions } from './types';
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
FormCommonConfig,
|
||||
VbenFormAdapterOptions,
|
||||
} from './types';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
|
@ -16,6 +20,8 @@ import { defineRule } from 'vee-validate';
|
|||
|
||||
const DEFAULT_MODEL_PROP_NAME = 'modelValue';
|
||||
|
||||
export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};
|
||||
|
||||
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||
DefaultResetActionButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
|
||||
DefaultSubmitActionButton: h(VbenButton, { size: 'sm', variant: 'default' }),
|
||||
|
|
@ -37,6 +43,9 @@ export function setupVbenForm<
|
|||
>(options: VbenFormAdapterOptions<T>) {
|
||||
const { components, config, defineRules } = options;
|
||||
|
||||
DEFAULT_FORM_COMMON_CONFIG.disabledOnChangeListener =
|
||||
config?.disabledOnChangeListener ?? false;
|
||||
|
||||
if (defineRules) {
|
||||
for (const key of Object.keys(defineRules)) {
|
||||
defineRule(key, defineRules[key as never]);
|
||||
|
|
|
|||
|
|
@ -12,26 +12,23 @@ import { toRaw } from 'vue';
|
|||
import { Store } from '@vben-core/shared/store';
|
||||
import {
|
||||
bindMethods,
|
||||
createMerge,
|
||||
isFunction,
|
||||
mergeWithArrayOverride,
|
||||
StateHandler,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
const merge = createMerge((originObj, key, updates) => {
|
||||
if (Array.isArray(originObj[key]) && Array.isArray(updates)) {
|
||||
originObj[key] = updates;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
import { objectPick } from '@vueuse/core';
|
||||
|
||||
function getDefaultState(): VbenFormProps {
|
||||
return {
|
||||
actionWrapperClass: '',
|
||||
collapsed: false,
|
||||
collapsedRows: 1,
|
||||
collapseTriggerResize: false,
|
||||
commonConfig: {},
|
||||
handleReset: undefined,
|
||||
handleSubmit: undefined,
|
||||
handleValuesChange: undefined,
|
||||
layout: 'horizontal',
|
||||
resetButtonOptions: {},
|
||||
schema: [],
|
||||
|
|
@ -43,11 +40,11 @@ function getDefaultState(): VbenFormProps {
|
|||
}
|
||||
|
||||
export class FormApi {
|
||||
private prevState: null | VbenFormProps = null;
|
||||
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
|
||||
public form = {} as FormActions;
|
||||
isMounted = false;
|
||||
|
||||
// private prevState!: ModalState;
|
||||
isMounted = false;
|
||||
public state: null | VbenFormProps = null;
|
||||
|
||||
stateHandler: StateHandler;
|
||||
|
|
@ -66,7 +63,9 @@ export class FormApi {
|
|||
},
|
||||
{
|
||||
onUpdate: () => {
|
||||
this.prevState = this.state;
|
||||
this.state = this.store.state;
|
||||
this.updateState();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -87,6 +86,24 @@ export class FormApi {
|
|||
return this.form;
|
||||
}
|
||||
|
||||
private updateState() {
|
||||
const currentSchema = this.state?.schema ?? [];
|
||||
const prevSchema = this.prevState?.schema ?? [];
|
||||
// 进行了删除schema操作
|
||||
if (currentSchema.length < prevSchema.length) {
|
||||
const currentFields = new Set(
|
||||
currentSchema.map((item) => item.fieldName),
|
||||
);
|
||||
const deletedSchema = prevSchema.filter(
|
||||
(item) => !currentFields.has(item.fieldName),
|
||||
);
|
||||
|
||||
for (const schema of deletedSchema) {
|
||||
this.form?.setFieldValue(schema.fieldName, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果需要多次更新状态,可以使用 batch 方法
|
||||
batchStore(cb: () => void) {
|
||||
this.store.batch(cb);
|
||||
|
|
@ -101,6 +118,47 @@ export class FormApi {
|
|||
return form.values;
|
||||
}
|
||||
|
||||
merge(formApi: FormApi) {
|
||||
const chain = [this, formApi];
|
||||
const proxy = new Proxy(formApi, {
|
||||
get(target: any, prop: any) {
|
||||
if (prop === 'merge') {
|
||||
return (nextFormApi: FormApi) => {
|
||||
chain.push(nextFormApi);
|
||||
return proxy;
|
||||
};
|
||||
}
|
||||
if (prop === 'submitAllForm') {
|
||||
return async (needMerge: boolean = true) => {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
chain.map(async (api) => {
|
||||
const form = await api.getForm();
|
||||
const validateResult = await api.validate();
|
||||
if (!validateResult.valid) {
|
||||
return;
|
||||
}
|
||||
const rawValues = toRaw(form.values || {});
|
||||
return rawValues;
|
||||
}),
|
||||
);
|
||||
if (needMerge) {
|
||||
const mergedResults = Object.assign({}, ...results);
|
||||
return mergedResults;
|
||||
}
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
return target[prop];
|
||||
},
|
||||
});
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
mount(formActions: FormActions) {
|
||||
if (!this.isMounted) {
|
||||
Object.assign(this.form, formActions);
|
||||
|
|
@ -155,19 +213,32 @@ export class FormApi {
|
|||
) {
|
||||
if (isFunction(stateOrFn)) {
|
||||
this.store.setState((prev) => {
|
||||
return merge(stateOrFn(prev), prev);
|
||||
return mergeWithArrayOverride(stateOrFn(prev), prev);
|
||||
});
|
||||
} else {
|
||||
this.store.setState((prev) => merge(stateOrFn, prev));
|
||||
this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表单值
|
||||
* @param fields record
|
||||
* @param filterFields 过滤不在schema中定义的字段 默认为true
|
||||
* @param shouldValidate
|
||||
*/
|
||||
async setValues(
|
||||
fields: Record<string, any>,
|
||||
filterFields: boolean = true,
|
||||
shouldValidate: boolean = false,
|
||||
) {
|
||||
const form = await this.getForm();
|
||||
form.setValues(fields, shouldValidate);
|
||||
if (!filterFields) {
|
||||
form.setValues(fields, shouldValidate);
|
||||
return;
|
||||
}
|
||||
const fieldNames = this.state?.schema?.map((item) => item.fieldName) ?? [];
|
||||
const filteredFields = objectPick(fields, fieldNames);
|
||||
form.setValues(filteredFields, shouldValidate);
|
||||
}
|
||||
|
||||
async submitForm(e?: Event) {
|
||||
|
|
@ -180,7 +251,7 @@ export class FormApi {
|
|||
return rawValues;
|
||||
}
|
||||
|
||||
unmounted() {
|
||||
unmount() {
|
||||
// this.state = null;
|
||||
this.isMounted = false;
|
||||
this.stateHandler.reset();
|
||||
|
|
@ -211,7 +282,10 @@ export class FormApi {
|
|||
currentSchema.forEach((schema, index) => {
|
||||
const updatedData = updatedMap[schema.fieldName];
|
||||
if (updatedData) {
|
||||
currentSchema[index] = merge(updatedData, schema) as FormSchema;
|
||||
currentSchema[index] = mergeWithArrayOverride(
|
||||
updatedData,
|
||||
schema,
|
||||
) as FormSchema;
|
||||
}
|
||||
});
|
||||
this.setState({ schema: currentSchema });
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { ZodType } from 'zod';
|
|||
|
||||
import type { FormSchema, MaybeComponentProps } from '../types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { computed, nextTick, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
|
|
@ -32,6 +32,7 @@ const {
|
|||
dependencies,
|
||||
description,
|
||||
disabled,
|
||||
disabledOnChangeListener,
|
||||
fieldName,
|
||||
formFieldProps,
|
||||
label,
|
||||
|
|
@ -49,6 +50,7 @@ const { componentBindEventMap, componentMap, isVertical } = useFormContext();
|
|||
const formRenderProps = injectRenderFormProps();
|
||||
const values = useFormValues();
|
||||
const errors = useFieldError(fieldName);
|
||||
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
|
||||
const formApi = formRenderProps.form;
|
||||
|
||||
const isInValid = computed(() => errors.value?.length > 0);
|
||||
|
|
@ -156,6 +158,18 @@ const computedProps = computed(() => {
|
|||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
() => computedProps.value?.autofocus,
|
||||
(value) => {
|
||||
if (value === true) {
|
||||
nextTick(() => {
|
||||
autofocus();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const shouldDisabled = computed(() => {
|
||||
return isDisabled.value || disabled || computedProps.value?.disabled;
|
||||
});
|
||||
|
|
@ -177,7 +191,7 @@ const fieldProps = computed(() => {
|
|||
keepValue: true,
|
||||
label,
|
||||
...(rules ? { rules } : {}),
|
||||
...formFieldProps,
|
||||
...(formFieldProps as Record<string, any>),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -200,15 +214,16 @@ function fieldBindEvent(slotProps: Record<string, any>) {
|
|||
return {
|
||||
[`onUpdate:${bindEventField}`]: handler,
|
||||
[bindEventField]: value,
|
||||
onChange: (e: Record<string, any>) => {
|
||||
const shouldUnwrap = isEventObjectLike(e);
|
||||
const onChange = slotProps?.componentField?.onChange;
|
||||
if (!shouldUnwrap) {
|
||||
return onChange?.(e);
|
||||
}
|
||||
|
||||
return onChange?.(e?.target?.[bindEventField] ?? e);
|
||||
},
|
||||
onChange: disabledOnChangeListener
|
||||
? undefined
|
||||
: (e: Record<string, any>) => {
|
||||
const shouldUnwrap = isEventObjectLike(e);
|
||||
const onChange = slotProps?.componentField?.onChange;
|
||||
if (!shouldUnwrap) {
|
||||
return onChange?.(e);
|
||||
}
|
||||
return onChange?.(e?.target?.[bindEventField] ?? e);
|
||||
},
|
||||
onInput: () => {},
|
||||
};
|
||||
}
|
||||
|
|
@ -226,6 +241,17 @@ function createComponentProps(slotProps: Record<string, any>) {
|
|||
|
||||
return binds;
|
||||
}
|
||||
|
||||
function autofocus() {
|
||||
if (
|
||||
fieldComponentRef.value &&
|
||||
isFunction(fieldComponentRef.value.focus) &&
|
||||
// 检查当前是否有元素被聚焦
|
||||
document.activeElement !== fieldComponentRef.value
|
||||
) {
|
||||
fieldComponentRef.value?.focus?.();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -275,6 +301,7 @@ function createComponentProps(slotProps: Record<string, any>) {
|
|||
>
|
||||
<component
|
||||
:is="fieldComponent"
|
||||
ref="fieldComponentRef"
|
||||
:class="{
|
||||
'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
|
||||
isInValid,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { ZodTypeAny } from 'zod';
|
||||
|
||||
import type { FormRenderProps, FormSchema, FormShape } from '../types';
|
||||
import type {
|
||||
FormCommonConfig,
|
||||
FormRenderProps,
|
||||
FormSchema,
|
||||
FormShape,
|
||||
} from '../types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Form } from '@vben-core/shadcn-ui';
|
||||
import { cn, isString } from '@vben-core/shared/utils';
|
||||
import { cn, isString, mergeWithArrayOverride } from '@vben-core/shared/utils';
|
||||
|
||||
import { type GenericObject } from 'vee-validate';
|
||||
|
||||
|
|
@ -17,12 +22,16 @@ import { getBaseRules, getDefaultValueInZodStack } from './helper';
|
|||
|
||||
interface Props extends FormRenderProps {}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapsedRows: 1,
|
||||
commonConfig: () => ({}),
|
||||
showCollapseButton: false,
|
||||
wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{ globalCommonConfig?: FormCommonConfig } & Props>(),
|
||||
{
|
||||
collapsedRows: 1,
|
||||
commonConfig: () => ({}),
|
||||
globalCommonConfig: () => ({}),
|
||||
showCollapseButton: false,
|
||||
wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
|
||||
},
|
||||
);
|
||||
|
||||
const emits = defineEmits<{
|
||||
submit: [event: any];
|
||||
|
|
@ -77,6 +86,7 @@ const computedSchema = computed(
|
|||
componentProps = {},
|
||||
controlClass = '',
|
||||
disabled,
|
||||
disabledOnChangeListener = false,
|
||||
formFieldProps = {},
|
||||
formItemClass = '',
|
||||
hideLabel = false,
|
||||
|
|
@ -84,7 +94,7 @@ const computedSchema = computed(
|
|||
labelClass = '',
|
||||
labelWidth = 100,
|
||||
wrapperClass = '',
|
||||
} = props.commonConfig;
|
||||
} = mergeWithArrayOverride(props.commonConfig, props.globalCommonConfig);
|
||||
return (props.schema || []).map((schema, index) => {
|
||||
const keepIndex = keepFormItemIndex.value;
|
||||
|
||||
|
|
@ -96,6 +106,7 @@ const computedSchema = computed(
|
|||
|
||||
return {
|
||||
disabled,
|
||||
disabledOnChangeListener,
|
||||
hideLabel,
|
||||
hideRequiredMark,
|
||||
labelWidth,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { VbenButtonProps } from '@vben-core/shadcn-ui';
|
||||
import type { Field, FormContext, GenericObject } from 'vee-validate';
|
||||
import type { FieldOptions, FormContext, GenericObject } from 'vee-validate';
|
||||
import type { ZodTypeAny } from 'zod';
|
||||
|
||||
import type { FormApi } from './form-api';
|
||||
|
|
@ -33,6 +33,15 @@ export type FormItemClassType =
|
|||
| (Record<never, never> & string)
|
||||
| WrapperClassType;
|
||||
|
||||
export type FormFieldOptions = Partial<
|
||||
{
|
||||
validateOnBlur?: boolean;
|
||||
validateOnChange?: boolean;
|
||||
validateOnInput?: boolean;
|
||||
validateOnModelUpdate?: boolean;
|
||||
} & FieldOptions
|
||||
>;
|
||||
|
||||
export interface FormShape {
|
||||
/** 默认值 */
|
||||
default?: any;
|
||||
|
|
@ -139,11 +148,16 @@ export interface FormCommonConfig {
|
|||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 是否禁用所有表单项的change事件监听
|
||||
* @default false
|
||||
*/
|
||||
disabledOnChangeListener?: boolean;
|
||||
/**
|
||||
* 所有表单项的控件样式
|
||||
* @default {}
|
||||
*/
|
||||
formFieldProps?: Partial<typeof Field>;
|
||||
formFieldProps?: FormFieldOptions;
|
||||
/**
|
||||
* 所有表单项的栅格布局
|
||||
* @default ""
|
||||
|
|
@ -230,6 +244,11 @@ export interface FormRenderProps<
|
|||
* @default 1
|
||||
*/
|
||||
collapsedRows?: number;
|
||||
/**
|
||||
* 是否触发resize事件
|
||||
* @default false
|
||||
*/
|
||||
collapseTriggerResize?: boolean;
|
||||
/**
|
||||
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
|
||||
*/
|
||||
|
|
@ -288,6 +307,10 @@ export interface VbenFormProps<
|
|||
* 表单提交回调
|
||||
*/
|
||||
handleSubmit?: HandleSubmitFn;
|
||||
/**
|
||||
* 表单值变化回调
|
||||
*/
|
||||
handleValuesChange?: (values: Record<string, any>) => void;
|
||||
/**
|
||||
* 重置按钮参数
|
||||
*/
|
||||
|
|
@ -317,6 +340,7 @@ export interface VbenFormAdapterOptions<
|
|||
components: Partial<Record<T, Component>>;
|
||||
config?: {
|
||||
baseModelPropName?: string;
|
||||
disabledOnChangeListener?: boolean;
|
||||
modelPropNameMap?: Partial<Record<T, string>>;
|
||||
};
|
||||
defineRules?: {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function useVbenForm<
|
|||
const Form = defineComponent(
|
||||
(props: VbenFormProps, { attrs, slots }) => {
|
||||
onBeforeUnmount(() => {
|
||||
api.unmounted();
|
||||
api.unmount();
|
||||
});
|
||||
return () =>
|
||||
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue