Compare commits

...

30 Commits
v5.3.0 ... main

Author SHA1 Message Date
Vben 4173264805
feat: add vxe-table component (#4563)
* chore: wip vxe-table

* feat: add table demo

* chore: follow ci recommendations to adjust details

* chore: add custom-cell demo

* feat: add custom-cell table demo

* feat: add table from demo
2024-10-04 23:05:28 +08:00
vben 46540a7329 chore: release v5.3.2 2024-10-03 15:43:15 +08:00
Vben 13fd0ea16c
fix: try to fix the error reported by the stub command in the window system (#4560) 2024-10-03 15:34:29 +08:00
Vben f7016466ee
feat: add examples of asynchronous form verification and verification time (#4559)
* feat: add examples of asynchronous form verification and verification time
2024-10-03 15:15:50 +08:00
Vben 0cd865e211
fix: fixed an error in the form onChange within Naive (#4558)
* fix: fixed an error in the form onChange within Naive

* chore: update
2024-10-03 14:22:18 +08:00
Squall2017 64428b9b11
feat: add form field autofocus configuration (#4550)
* feat: add form field autofocus configuration
2024-10-03 13:10:21 +08:00
LinaBell aed31a5a4e
perf: [antd] default placeholder for input and select components (#4551)
* chore: demo of customizing form layout using tailwind

* perf: default placeholder for input and select components

* chore: update ts type

* chore: extract public methods
2024-10-03 13:04:19 +08:00
dependabot[bot] b3e196f001
chore(deps-dev): bump the non-breaking-changes group across 1 directory with 3 updates (#4557)
* chore(deps-dev): bump the non-breaking-changes group across 1 directory with 3 updates

Bumps the non-breaking-changes group with 3 updates in the / directory: [turbo](https://github.com/vercel/turborepo), [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) and [rollup](https://github.com/rollup/rollup).


Updates `turbo` from 2.1.2 to 2.1.3
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/release.md)
- [Commits](https://github.com/vercel/turborepo/compare/v2.1.2...v2.1.3)

Updates `vitest` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.2/packages/vitest)

Updates `rollup` from 4.22.5 to 4.24.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.22.5...v4.24.0)

---
updated-dependencies:
- dependency-name: turbo
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: rollup
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-03 12:54:04 +08:00
LinaBell b2c117f544
chore: demo of customizing form layout using tailwind (#4549) 2024-09-30 09:47:16 +08:00
vben 01391ee5a1 chore: release v5.3.1 2024-09-29 22:25:20 +08:00
Vben 3572ce1538
fix: when multiple pop-ups exist at the same time, clicking will close all (#4548) 2024-09-29 22:15:43 +08:00
Vben d1e1256202
chore: disable sorting of non-core folder object fields (#4547)
* chore: disable sorting of non-core folder object fields

* chore: ci error
2024-09-29 22:03:17 +08:00
Squall2017 b7776c5148
fix: fix abnormal display of scroll bar on lock screen page (#4546)
*  fix abnormal display of scroll bar on lock screen page
2024-09-29 21:45:56 +08:00
dependabot[bot] 2d1519eca7
chore(deps): bump the non-breaking-changes group across 1 directory with 4 updates (#4537)
* chore(deps): bump the non-breaking-changes group across 1 directory with 4 updates

Bumps the non-breaking-changes group with 4 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [vue](https://github.com/vuejs/core), [rollup](https://github.com/rollup/rollup) and [@vue/shared](https://github.com/vuejs/core/tree/HEAD/packages/shared).


Updates `@types/node` from 22.7.2 to 22.7.4
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vue` from 3.5.8 to 3.5.10
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.8...v3.5.10)

Updates `rollup` from 4.22.4 to 4.22.5
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.22.4...v4.22.5)

Updates `@vue/shared` from 3.5.8 to 3.5.10
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.5.10/packages/shared)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: rollup
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: "@vue/shared"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-28 16:10:26 +08:00
jinmao88 93b5618b52
feat(@vben/stores): export defineStore to support pinia persistence within apps (#4483)
Co-authored-by: Li Kui <90845831+likui628@users.noreply.github.com>
2024-09-28 16:01:04 +08:00
Vben 639d2e1525
feat: add pagination component (#4522)
* feat: add pagination component

* chore: update
2024-09-26 23:09:17 +08:00
Vben 26646d42f7
fix: when modal and drawer exist at the same time, click Close All (#4521) 2024-09-26 22:50:37 +08:00
Vben 17fa8eb93b
fix: improve ant design button icon style (#4520) 2024-09-26 22:40:23 +08:00
Vben 8250894a50
fix: improve input browser backfilling style (#4519) 2024-09-26 22:31:20 +08:00
vince a72b8acaf9
fix: window system clean script execution problems (#4513)
* fix: fix window system clean script execution problems

* fix: lint error

* chore: remove test code
2024-09-26 11:59:19 +08:00
Vben a46c85d77d
chore: update documentation and deps (#4510)
* chore: update docs

* chore: update deps

* chore: update action

* fix: reset after preferences are refreshed

* fix: ci error
2024-09-25 23:09:48 +08:00
Squall2017 fdc5b02c30
feat(form): add merge form functionality (#4495)
* feat: captcha example

* fix: fix lint errors

* chore: event handling and methods

* chore: add accessibility features ARIA labels and roles

* refactor: refactor code structure and improve captcha demo page

* feat: add captcha internationalization

* chore: 适配时间戳国际化展示

* fix: 1. 添加点击位置边界校验,防止点击外部导致x,y误差。2. 演示页面宽度过长添加滚动条。3. 添加hooks

* feat: sync test

* feat: 添加合并表单功能

* fix: 修复上一步不展示问题

---------

Co-authored-by: vince <vince292007@gmail.com>
2024-09-25 18:11:02 +08:00
Netfan 476aa068d7
fix: stripe table style for element plus, fixed: #4501 (#4503) 2024-09-25 17:33:24 +08:00
LinaBell bb6057cac3
perf: setValues method of the form supports assigning values only to keys that exist in the schema (#4508)
* fix: hover border style same as antd style when validate error

* fix: hover border style same as antd style when validate error

* feat(@vben-core/form-ui): Default form validation rules applicable to selector components

* fix: Missing the default required label style for components such as select

* fix: the focus style and antd of the input box validation failure should be consistent

* fix: the focus style and antd of the input box validation failure should be consistent

* fix: some antd components failed to verify styles

* perf: setValues method of the form supports assigning values only to keys that exist in the schema

* docs: update form component docs

---------

Co-authored-by: vince <vince292007@gmail.com>
2024-09-25 17:09:38 +08:00
Fifteen abbbbfb955
fix(docs): fix the selected state of the top navigation bar (#4499)
* fix(@vben/docs): fix the selected state of the top navigation bar

* style(@vben/docs): navigation bar selected item style
2024-09-25 09:53:55 +08:00
handsomeFu 79c87c9f46
chore(@vben/playground): Add new slider captcha element and adjust references (#4490)
Add a new slider captcha action reference (`el6`) to support additional functionality and update related template and event handler to use this new reference.
2024-09-24 14:15:41 +08:00
Vben f815dcf3ae
fix: after deleting the form item, you will also get the form value (#4481)
* fix: after deleting the form item, you will also get the form value
2024-09-23 22:59:58 +08:00
neo.dowithless 1197efea26
fix: wrong style when breadcrumbs display background (#4472) 2024-09-23 14:15:46 +08:00
Vben 2a83f1d666
feat: add playwright e2e testing framework (#4468)
* feat: add playwright e2e testing framework
2024-09-22 21:35:40 +08:00
aonoa 4b3d2d21ed
fix: Clear the input box when closing the search (#4467)
Signed-off-by: aonoa <1991849113@qq.com>
2024-09-22 20:38:01 +08:00
227 changed files with 6184 additions and 2263 deletions

View File

@ -19,6 +19,7 @@ permissions:
jobs: jobs:
post-update: post-update:
if: github.repository == 'vbenjs/vue-vben-admin'
# if: ${{ github.actor == 'dependabot[bot]' }} # if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:

View File

@ -18,7 +18,7 @@ env:
jobs: jobs:
version: 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' # if: github.repository == 'vbenjs/vue-vben-admin'
timeout-minutes: 15 timeout-minutes: 15
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -17,6 +17,7 @@ env:
jobs: jobs:
test: test:
name: Test name: Test
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@ -55,6 +56,7 @@ jobs:
lint: lint:
name: Lint name: Lint
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@ -77,6 +79,7 @@ jobs:
check: check:
name: Check name: Check
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 20 timeout-minutes: 20
strategy: strategy:
@ -106,6 +109,7 @@ jobs:
ci-ok: ci-ok:
name: CI OK name: CI OK
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [test, check, lint] needs: [test, check, lint]
env: env:

View File

@ -22,6 +22,7 @@ on:
jobs: jobs:
analyze: analyze:
name: Analyze (${{ matrix.language }}) name: Analyze (${{ matrix.language }})
if: github.repository == 'vbenjs/vue-vben-admin'
# Runner size impacts CodeQL analysis time. To learn more, please see: # Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/supported-runners-and-hardware-resources

View File

@ -8,7 +8,7 @@ on:
jobs: jobs:
deploy-playground-ftp: deploy-playground-ftp:
name: Deploy Push 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -39,7 +39,7 @@ jobs:
deploy-docs-ftp: deploy-docs-ftp:
name: Deploy Push 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -63,7 +63,7 @@ jobs:
deploy-antd-ftp: deploy-antd-ftp:
name: Deploy Push 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -94,7 +94,7 @@ jobs:
deploy-ele-ftp: deploy-ele-ftp:
name: Deploy Push Element 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -125,7 +125,7 @@ jobs:
deploy-naive-ftp: deploy-naive-ftp:
name: Deploy Push 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code

View File

@ -17,6 +17,7 @@ jobs:
# write permission is required for autolabeler # write permission is required for autolabeler
# otherwise, read permission is required at least # otherwise, read permission is required at least
pull-requests: write pull-requests: write
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: release-drafter/release-drafter@v6 - uses: release-drafter/release-drafter@v6

View File

@ -3,23 +3,29 @@ name: Issue Close Require
# 触发条件:每天零点 # 触发条件:每天零点
on: on:
workflow_dispatch:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 0 * * *'
permissions: permissions:
pull-requests: write pull-requests: write
contents: write contents: write
issues: write
jobs: jobs:
close-issues: close-issues:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# 步骤1关闭未活动的 Issues # 关闭未活动的 Issues
- name: Close Inactive Issues - name: Close Inactive Issues
uses: actions-cool/issues-helper@v3 uses: actions/stale@v9
with: with:
actions: 'close-issues' # 执行动作:关闭 Issues days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
token: ${{ secrets.GITHUB_TOKEN }} # GitHub Token用于认证 stale-issue-label: needs-reproduction # Label that flags an issue as stale.
labels: 'needs reproduction' # 目标标签 only-labels: needs-reproduction # Only process these issues
inactive-day: 3 # 未活动天数阈值 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

View File

@ -13,6 +13,7 @@ permissions:
jobs: jobs:
reply-labeled: reply-labeled:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: remove enhancement pending - name: remove enhancement pending

View File

@ -11,12 +11,13 @@ permissions:
jobs: jobs:
action: action:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v5
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '30' issue-inactive-days: '14'
issue-lock-reason: '' issue-lock-reason: ''
pr-inactive-days: '30' pr-inactive-days: '30'
pr-lock-reason: '' pr-lock-reason: ''

View File

@ -15,6 +15,7 @@ permissions:
jobs: jobs:
build: build:
name: Create Release name: Create Release
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:

View File

@ -9,8 +9,9 @@ on:
jobs: jobs:
main: main:
runs-on: ubuntu-latest
name: Semantic Pull Request name: Semantic Pull Request
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@v5 uses: amannn/action-semantic-pull-request@v5

View File

@ -6,6 +6,7 @@ on:
jobs: jobs:
stale: stale:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v9

View File

@ -3,4 +3,4 @@ ports:
onOpen: open-preview onOpen: open-preview
tasks: tasks:
- init: corepack enable && pnpm install - init: corepack enable && pnpm install
command: pnpm run dev command: pnpm run dev:play

View File

@ -125,6 +125,10 @@ pnpm build
[@Vben](https://github.com/anncwb) [@Vben](https://github.com/anncwb)
## スター歴史
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## 寄付 ## 寄付
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます! このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!

View File

@ -124,6 +124,10 @@ Support modern browsers, not IE
[@Vben](https://github.com/anncwb) [@Vben](https://github.com/anncwb)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## Donate ## Donate
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support! If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!

View File

@ -77,6 +77,10 @@ pnpm dev
pnpm build pnpm build
``` ```
## 更新日志
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 如何贡献 ## 如何贡献
非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。 非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。
@ -120,6 +124,10 @@ pnpm build
[@Vben](https://github.com/anncwb) [@Vben](https://github.com/anncwb)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](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> <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 ## Contributor
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors"> <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">

View File

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

View File

@ -10,6 +10,7 @@
"start": "nitro dev" "start": "nitro dev"
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "catalog:",
"jsonwebtoken": "catalog:", "jsonwebtoken": "catalog:",
"nitropack": "catalog:" "nitropack": "catalog:"
}, },

View File

@ -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) { export function useResponseError(message: string, error: any = null) {
return { return {
code: -1, code: -1,
@ -27,3 +48,18 @@ export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401); setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); 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));
}

View File

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

View File

@ -4,6 +4,7 @@ import type {
VbenFormProps, VbenFormProps,
} from '@vben/common-ui'; } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
@ -57,6 +58,16 @@ export type FormComponentType =
| 'Upload' | 'Upload'
| BaseFormComponentType; | 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组件内部 // 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({ setupVbenForm<FormComponentType>({
components: { components: {
@ -73,20 +84,20 @@ setupVbenForm<FormComponentType>({
return h(Button, { ...props, attrs, type: 'primary' }, slots); return h(Button, { ...props, attrs, type: 'primary' }, slots);
}, },
Divider, Divider,
Input, Input: withDefaultPlaceholder(Input, 'input'),
InputNumber, InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword, InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions, Mentions: withDefaultPlaceholder(Mentions, 'input'),
Radio, Radio,
RadioGroup, RadioGroup,
RangePicker, RangePicker,
Rate, Rate,
Select, Select: withDefaultPlaceholder(Select, 'select'),
Space, Space,
Switch, Switch,
Textarea, Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker, TimePicker,
TreeSelect, TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload, Upload,
}, },
config: { config: {

View File

@ -1 +1,2 @@
export * from './form'; export * from './form';
export * from './vxe-table';

View File

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

View File

@ -10,10 +10,6 @@ export namespace AuthApi {
/** 登录接口返回值 */ /** 登录接口返回值 */
export interface LoginResult { export interface LoginResult {
accessToken: string; accessToken: string;
desc: string;
realName: string;
userId: string;
username: string;
} }
export interface RefreshTokenResult { export interface RefreshTokenResult {

View File

@ -1,6 +1,8 @@
/** /**
* *
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import {
@ -68,7 +70,7 @@ function createRequestClient(baseURL: string) {
}); });
// response数据解构 // response数据解构
client.addResponseInterceptor({ client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => { fulfilled: (response) => {
const { data: responseData, status } = response; const { data: responseData, status } = response;
@ -93,7 +95,10 @@ function createRequestClient(baseURL: string) {
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor( client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)), errorMessageResponseInterceptor((msg: string, _error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
message.error(msg);
}),
); );
return client; return client;

View File

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

View File

@ -4,6 +4,7 @@ import type {
VbenFormProps, VbenFormProps,
} from '@vben/common-ui'; } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
@ -42,6 +43,16 @@ export type FormComponentType =
| 'Upload' | 'Upload'
| BaseFormComponentType; | 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组件内部 // 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({ setupVbenForm<FormComponentType>({
components: { components: {
@ -56,14 +67,14 @@ setupVbenForm<FormComponentType>({
return h(ElButton, { ...props, attrs, type: 'primary' }, slots); return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
}, },
Divider: ElDivider, Divider: ElDivider,
Input: ElInput, Input: withDefaultPlaceholder(ElInput, 'input'),
InputNumber: ElInputNumber, InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
RadioGroup: ElRadioGroup, RadioGroup: ElRadioGroup,
Select: ElSelect, Select: withDefaultPlaceholder(ElSelect, 'select'),
Space: ElSpace, Space: ElSpace,
Switch: ElSwitch, Switch: ElSwitch,
TimePicker: ElTimePicker, TimePicker: ElTimePicker,
TreeSelect: ElTreeSelect, TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
Upload: ElUpload, Upload: ElUpload,
}, },
config: { config: {

View File

@ -1 +1,2 @@
export * from './form'; export * from './form';
export * from './vxe-table';

View File

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

View File

@ -10,10 +10,6 @@ export namespace AuthApi {
/** 登录接口返回值 */ /** 登录接口返回值 */
export interface LoginResult { export interface LoginResult {
accessToken: string; accessToken: string;
desc: string;
realName: string;
userId: string;
username: string;
} }
export interface RefreshTokenResult { export interface RefreshTokenResult {

View File

@ -1,6 +1,8 @@
/** /**
* *
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import {
@ -68,7 +70,7 @@ function createRequestClient(baseURL: string) {
}); });
// response数据解构 // response数据解构
client.addResponseInterceptor({ client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => { fulfilled: (response) => {
const { data: responseData, status } = response; const { data: responseData, status } = response;
@ -93,7 +95,10 @@ function createRequestClient(baseURL: string) {
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor( client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => ElMessage.error(msg)), errorMessageResponseInterceptor((msg: string, _error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
ElMessage.error(msg);
}),
); );
return client; return client;

View File

@ -7,6 +7,7 @@ import {
ElMessage, ElMessage,
ElNotification, ElNotification,
ElSpace, ElSpace,
ElTable,
} from 'element-plus'; } from 'element-plus';
type NotificationType = 'error' | 'info' | 'success' | 'warning'; type NotificationType = 'error' | 'info' | 'success' | 'warning';
@ -38,6 +39,14 @@ function notify(type: NotificationType) {
type, 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> </script>
<template> <template>
@ -74,5 +83,11 @@ function notify(type: NotificationType) {
<ElButton type="success" @click="notify('success')"> 成功 </ElButton> <ElButton type="success" @click="notify('success')"> 成功 </ElButton>
</ElSpace> </ElSpace>
</ElCard> </ElCard>
<ElCard class="mb-5">
<ElTable :data="tableData" stripe>
<ElTable.TableColumn label="测试列1" prop="prop1" />
<ElTable.TableColumn label="测试列2" prop="prop2" />
</ElTable>
</ElCard>
</Page> </Page>
</template> </template>

View File

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

View File

@ -4,6 +4,7 @@ import type {
VbenFormProps, VbenFormProps,
} from '@vben/common-ui'; } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
@ -43,6 +44,16 @@ export type FormComponentType =
| 'Upload' | 'Upload'
| BaseFormComponentType; | 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组件内部 // 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({ setupVbenForm<FormComponentType>({
components: { components: {
@ -62,17 +73,18 @@ setupVbenForm<FormComponentType>({
); );
}, },
Divider: NDivider, Divider: NDivider,
Input: NInput, Input: withDefaultPlaceholder(NInput, 'input'),
InputNumber: NInputNumber, InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
RadioGroup: NRadioGroup, RadioGroup: NRadioGroup,
Select: NSelect, Select: withDefaultPlaceholder(NSelect, 'select'),
Space: NSpace, Space: NSpace,
Switch: NSwitch, Switch: NSwitch,
TimePicker: NTimePicker, TimePicker: NTimePicker,
TreeSelect: NTreeSelect, TreeSelect: withDefaultPlaceholder(NTreeSelect, 'select'),
Upload: NUpload, Upload: NUpload,
}, },
config: { config: {
disabledOnChangeListener: true,
baseModelPropName: 'value', baseModelPropName: 'value',
modelPropNameMap: { modelPropNameMap: {
Checkbox: 'checked', Checkbox: 'checked',

View File

@ -1,2 +1,3 @@
export * from './form'; export * from './form';
export * from './naive'; export * from './naive';
export * from './vxe-table';

View File

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

View File

@ -10,10 +10,6 @@ export namespace AuthApi {
/** 登录接口返回值 */ /** 登录接口返回值 */
export interface LoginResult { export interface LoginResult {
accessToken: string; accessToken: string;
desc: string;
realName: string;
userId: string;
username: string;
} }
export interface RefreshTokenResult { export interface RefreshTokenResult {

View File

@ -1,6 +1,8 @@
/** /**
* *
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import {
@ -67,7 +69,7 @@ function createRequestClient(baseURL: string) {
}); });
// response数据解构 // response数据解构
client.addResponseInterceptor({ client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => { fulfilled: (response) => {
const { data: responseData, status } = response; const { data: responseData, status } = response;
@ -92,7 +94,10 @@ function createRequestClient(baseURL: string) {
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor( client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)), errorMessageResponseInterceptor((msg: string, _error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
message.error(msg);
}),
); );
return client; return client;

View File

@ -25,7 +25,6 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
meta: { meta: {
icon: 'mdi:shield-key-outline',
title: $t('page.demos.table'), title: $t('page.demos.table'),
}, },
name: 'Table', name: 'Table',

View File

@ -19,6 +19,7 @@
"intlify", "intlify",
"mkdist", "mkdist",
"mockjs", "mockjs",
"vitejs",
"noopener", "noopener",
"noreferrer", "noreferrer",
"nprogress", "nprogress",

View File

@ -133,12 +133,19 @@ function sidebarCommercial(): DefaultTheme.SidebarItem[] {
function nav(): DefaultTheme.NavItem[] { function nav(): DefaultTheme.NavItem[] {
return [ return [
{ {
activeMatch: '^/en/(guide|components)/',
text: 'Doc', text: 'Doc',
items: [ items: [
{ {
activeMatch: '^/en/guide/',
link: '/en/guide/introduction/vben', link: '/en/guide/introduction/vben',
text: 'Guide', text: 'Guide',
}, },
// {
// activeMatch: '^/en/components/',
// link: '/en/components/introduction',
// text: 'Components',
// },
{ {
text: 'Historical Versions', text: 'Historical Versions',
items: [ items: [

View File

@ -176,13 +176,16 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
function nav(): DefaultTheme.NavItem[] { function nav(): DefaultTheme.NavItem[] {
return [ return [
{ {
activeMatch: '^/(guide|components)/',
text: '文档', text: '文档',
items: [ items: [
{ {
activeMatch: '^/guide/',
link: '/guide/introduction/vben', link: '/guide/introduction/vben',
text: '指南', text: '指南',
}, },
{ {
activeMatch: '^/components/',
link: '/components/introduction', link: '/components/introduction',
text: '组件', text: '组件',
}, },

View File

@ -9,3 +9,14 @@ html.dark {
.form-valid-error p { .form-valid-error p {
margin: 0; margin: 0;
} }
/* 顶部导航栏选中项样式 */
.VPNavBarMenuLink,
.VPNavBarMenuGroup {
border-bottom: 1px solid transparent;
}
.VPNavBarMenuLink.active,
.VPNavBarMenuGroup.active {
border-bottom-color: var(--vp-c-brand-1);
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@vben/docs", "name": "@vben/docs",
"version": "5.3.0", "version": "5.3.2",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "vitepress build", "build": "vitepress build",
@ -13,9 +13,7 @@
"dependencies": { "dependencies": {
"@vben-core/shadcn-ui": "workspace:*", "@vben-core/shadcn-ui": "workspace:*",
"@vben/common-ui": "workspace:*", "@vben/common-ui": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/locales": "workspace:*", "@vben/locales": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"ant-design-vue": "catalog:", "ant-design-vue": "catalog:",
"lucide-vue-next": "catalog:", "lucide-vue-next": "catalog:",

View File

@ -31,6 +31,7 @@ import type {
VbenFormProps, VbenFormProps,
} from '@vben/common-ui'; } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
@ -84,6 +85,16 @@ export type FormComponentType =
| 'Upload' | 'Upload'
| BaseFormComponentType; | 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组件内部 // 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({ setupVbenForm<FormComponentType>({
components: { components: {
@ -100,26 +111,27 @@ setupVbenForm<FormComponentType>({
return h(Button, { ...props, attrs, type: 'primary' }, slots); return h(Button, { ...props, attrs, type: 'primary' }, slots);
}, },
Divider, Divider,
Input, Input: withDefaultPlaceholder(Input, 'input'),
InputNumber, InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword, InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions, Mentions: withDefaultPlaceholder(Mentions, 'input'),
Radio, Radio,
RadioGroup, RadioGroup,
RangePicker, RangePicker,
Rate, Rate,
Select, Select: withDefaultPlaceholder(Select, 'select'),
Space, Space,
Switch, Switch,
Textarea, Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker, TimePicker,
TreeSelect, TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload, Upload,
}, },
config: { config: {
// 是否禁用onChange事件监听naive ui组件库默认不需要监听onChange事件否则会在控制台报错
disabledOnChangeListener: true,
// ant design vue组件库默认都是 v-model:value // ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value', baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList // 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: { modelPropNameMap: {
Checkbox: 'checked', Checkbox: 'checked',
@ -229,7 +241,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| --- | --- | --- | | --- | --- | --- |
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` | | submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` |
| resetForm | 重置表单 | `()=>Promise<void>` | | 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>` | | getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
| validate | 表单校验 | `()=>Promise<void>` | | validate | 表单校验 | `()=>Promise<void>` |
| resetValidate | 重置表单校验 | `()=>Promise<void>` | | resetValidate | 重置表单校验 | `()=>Promise<void>` |
@ -255,6 +267,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - | | submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` | | showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
| collapsed | 是否折叠,在`是否展开在showCollapseButton=true`时生效 | `boolean` | `false` | | collapsed | 是否折叠,在`是否展开在showCollapseButton=true`时生效 | `boolean` | `false` |
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
| collapsedRows | 折叠时保持的行数 | `number` | `1` | | collapsedRows | 折叠时保持的行数 | `number` | `1` |
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - | | commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
| schema | 表单项的每一项配置 | `FormSchema` | - | | schema | 表单项的每一项配置 | `FormSchema` | - |

View File

@ -46,8 +46,6 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
```json ```json
{ {
"scripts": { "scripts": {
// Install dependencies
"bootstrap": "pnpm install",
// Build the project // Build the project
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build", "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
// Build the project with analysis // Build the project with analysis
@ -77,7 +75,7 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
// Check types // Check types
"check:type": "turbo run typecheck", "check:type": "turbo run typecheck",
// Clean the project (delete node_modules, dist, .turbo, etc.) // Clean the project (delete node_modules, dist, .turbo, etc.)
"clean": "vsh clean", "clean": "node ./scripts/clean.mjs",
// Commit code // Commit code
"commit": "czg", "commit": "czg",
// Start the project (by default, the dev scripts of all packages in the entire repository will run) // 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 code
"lint": "vsh lint", "lint": "vsh lint",
// After installing dependencies, execute the stub script for all packages // 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 // Only allow using pnpm
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
// Install husky // Install husky
@ -107,9 +105,9 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
// Package specification check // Package specification check
"publint": "vsh publint", "publint": "vsh publint",
// Delete all node_modules, yarn.lock, package.lock.json, and reinstall dependencies // 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 // Run vitest unit tests
"test:unit": "vitest", "test:unit": "vitest run --dom",
// Update project dependencies // Update project dependencies
"update:deps": " pnpm update --latest --recursive", "update:deps": " pnpm update --latest --recursive",
// Changeset generation and versioning // Changeset generation and versioning

View File

@ -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 * This file can be adjusted according to business logic
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import {
@ -227,7 +229,7 @@ function createRequestClient(baseURL: string) {
}); });
// Deal Response Data // Deal Response Data
client.addResponseInterceptor({ client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => { fulfilled: (response) => {
const { data: responseData, status } = 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. // Generic error handling; if none of the above error handling logic is triggered, it will fall back to this.
client.addResponseInterceptor( client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)), errorMessageResponseInterceptor((msg: string, _error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
message.error(msg);
}),
); );
return client; return client;

View File

@ -37,8 +37,6 @@ If you want to adjust the content of the login form, you can configure the `Auth
```vue ```vue
<AuthenticationLogin <AuthenticationLogin
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin" @submit="authStore.authLogin"
/> />
``` ```

View File

@ -42,23 +42,6 @@ Check the dependency situation of the entire project and output `unused dependen
pnpm vsh check-dep 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 ### vsh lint
Lint checks the project to see if the code in the project conforms to standards. Lint checks the project to see if the code in the project conforms to standards.

View File

@ -46,8 +46,6 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
```json ```json
{ {
"scripts": { "scripts": {
// 安装依赖
"bootstrap": "pnpm install",
// 构建项目 // 构建项目
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build", "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
// 构建项目并分析 // 构建项目并分析
@ -77,7 +75,7 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
// 检查类型 // 检查类型
"check:type": "turbo run typecheck", "check:type": "turbo run typecheck",
// 清理项目删除node_modules、dist、.turbo等目录 // 清理项目删除node_modules、dist、.turbo等目录
"clean": "vsh clean", "clean": "node ./scripts/clean.mjs",
// 提交代码 // 提交代码
"commit": "czg", "commit": "czg",
// 启动项目默认会运行整个仓库所有包的dev脚本 // 启动项目默认会运行整个仓库所有包的dev脚本
@ -97,7 +95,7 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
// lint 代码 // lint 代码
"lint": "vsh lint", "lint": "vsh lint",
// 依赖安装完成之后执行所有包的stub脚本 // 依赖安装完成之后执行所有包的stub脚本
"postinstall": "turbo run stub", "postinstall": "pnpm -r run stub --if-present",
// 只允许使用pnpm // 只允许使用pnpm
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
// husky的安装 // husky的安装
@ -107,9 +105,9 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
// 包规范检查 // 包规范检查
"publint": "vsh publint", "publint": "vsh publint",
// 删除所有的node_modules、yarn.lock、package.lock.json重新安装依赖 // 删除所有的node_modules、yarn.lock、package.lock.json重新安装依赖
"reinstall": "pnpm clean --del-lock && pnpm bootstrap", "reinstall": "pnpm clean --del-lock && pnpm install",
// 运行 vitest 单元测试 // 运行 vitest 单元测试
"test:unit": "vitest", "test:unit": "vitest run --dom",
// 更新项目依赖 // 更新项目依赖
"update:deps": " pnpm update --latest --recursive", "update:deps": " pnpm update --latest --recursive",
// changeset生成提交集 // changeset生成提交集

View File

@ -163,6 +163,8 @@ export async function deleteUserApi(user: UserInfo) {
/** /**
* 该文件可自行根据业务逻辑进行调整 * 该文件可自行根据业务逻辑进行调整
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import {
@ -230,7 +232,7 @@ function createRequestClient(baseURL: string) {
}); });
// response数据解构 // response数据解构
client.addResponseInterceptor({ client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => { fulfilled: (response) => {
const { data: responseData, status } = response; const { data: responseData, status } = response;
@ -256,7 +258,10 @@ function createRequestClient(baseURL: string) {
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor( client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)), errorMessageResponseInterceptor((msg: string, _error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
message.error(msg);
}),
); );
return client; return client;

View File

@ -1,10 +1,14 @@
---
outline: deep
---
# 登录 # 登录
本文介绍如何去改造自己的应用程序登录页。 本文介绍如何去改造自己的应用程序登录页以及如何快速的对接登录页面接口
## 登录页面调整 ## 登录页面调整
如果你想调整登录页面的标题、描述和图标以及工具栏,你可以通过配置 `AuthPageLayout` 组件的 `props` 参数来实现。 如果你想调整登录页面的标题、描述和图标以及工具栏,你可以通过配置 `AuthPageLayout` 组件的参数来实现。
![login](/guide/login.png) ![login](/guide/login.png)
@ -30,8 +34,6 @@
```vue ```vue
<AuthenticationLogin <AuthenticationLogin
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin" @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 [];
}
```

View File

@ -1,3 +1,7 @@
---
outline: deep
---
# 精简版本 # 精简版本
`5.0` 版本开始,我们不再提供精简的仓库或者分支。我们的目标是提供一个更加一致的开发体验,同时减少维护成本。在这里,我们将如何介绍自己的项目,如何去精简以及移除不需要的功能。 `5.0` 版本开始,我们不再提供精简的仓库或者分支。我们的目标是提供一个更加一致的开发体验,同时减少维护成本。在这里,我们将如何介绍自己的项目,如何去精简以及移除不需要的功能。
@ -74,3 +78,17 @@ pnpm install
- `.github` 文件夹用于存放 GitHub 的配置文件 - `.github` 文件夹用于存放 GitHub 的配置文件
- `.vscode` 文件夹用于存放 VSCode 的配置文件,如果你使用其他编辑器,可以删除 - `.vscode` 文件夹用于存放 VSCode 的配置文件,如果你使用其他编辑器,可以删除
- `./scripts/deploy` 文件夹用于存放部署脚本如果你不需要docker部署可以删除 - `./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` 目录等。删除之前请先确保你的路由中没有引用到这些组件。

View File

@ -42,16 +42,6 @@ pnpm vsh check-circular
pnpm vsh check-dep pnpm vsh check-dep
``` ```
### vsh clean
删除项目的`node_modules`、`dist`、`.turbo`等目录,清理项目。
#### 用法
```bash
pnpm vsh clean
```
#### 选项 #### 选项
| 选项 | 说明 | | 选项 | 说明 |

View File

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

View File

@ -24,6 +24,7 @@ export async function node(): Promise<Linter.Config[]> {
'vite', 'vite',
'@vue/test-utils', '@vue/test-utils',
'@vben/tailwind-config', '@vben/tailwind-config',
'@playwright/test',
], ],
}, },
], ],

View File

@ -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, ignores: restrictedImportIgnores,
rules: { rules: {
'perfectionist/sort-interfaces': 'off', '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: { rules: {
'no-console': 'off', 'no-console': 'off',
}, },

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ const shadcnUiColors = {
DEFAULT: 'hsl(var(--accent))', DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))', foreground: 'hsl(var(--accent-foreground))',
hover: 'hsl(var(--accent-hover))', hover: 'hsl(var(--accent-hover))',
lighter: 'has(val(--accent-lighter))',
}, },
background: { background: {
deep: 'hsl(var(--background-deep))', deep: 'hsl(var(--background-deep))',
@ -90,7 +91,10 @@ const customColors = {
main: { main: {
DEFAULT: 'hsl(var(--main))', DEFAULT: 'hsl(var(--main))',
}, },
overlay: 'hsl(var(--overlay))', overlay: {
content: 'hsl(var(--overlay-content))',
DEFAULT: 'hsl(var(--overlay))',
},
red: { red: {
...createColorsPalette('red'), ...createColorsPalette('red'),
foreground: 'hsl(var(--destructive-foreground))', foreground: 'hsl(var(--destructive-foreground))',

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@vben/vite-config", "name": "@vben/vite-config",
"version": "5.3.0", "version": "5.3.2",
"private": true, "private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin", "homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@ -53,6 +53,7 @@
"vite": "catalog:", "vite": "catalog:",
"vite-plugin-compression": "catalog:", "vite-plugin-compression": "catalog:",
"vite-plugin-dts": "catalog:", "vite-plugin-dts": "catalog:",
"vite-plugin-html": "catalog:" "vite-plugin-html": "catalog:",
"vite-plugin-lazy-import": "catalog:"
} }
} }

View File

@ -47,6 +47,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
}, },
pwa: true, pwa: true,
pwaOptions: getDefaultPwaOptions(appTitle), pwaOptions: getDefaultPwaOptions(appTitle),
vxeTableLazyImport: true,
...envConfig, ...envConfig,
...application, ...application,
}); });

View File

@ -26,6 +26,7 @@ import { viteMetadataPlugin } from './inject-metadata';
import { viteLicensePlugin } from './license'; import { viteLicensePlugin } from './license';
import { viteNitroMockPlugin } from './nitro-mock'; import { viteNitroMockPlugin } from './nitro-mock';
import { vitePrintPlugin } from './print'; import { vitePrintPlugin } from './print';
import { viteVxeTableImportsPlugin } from './vxe-table';
/** /**
* vite * vite
@ -110,6 +111,7 @@ async function loadApplicationPlugins(
printInfoMap, printInfoMap,
pwa, pwa,
pwaOptions, pwaOptions,
vxeTableLazyImport,
...commonOptions ...commonOptions
} = options; } = options;
@ -135,6 +137,12 @@ async function loadApplicationPlugins(
return [await vitePrintPlugin({ infoMap: printInfoMap })]; return [await vitePrintPlugin({ infoMap: printInfoMap })];
}, },
}, },
{
condition: vxeTableLazyImport,
plugins: async () => {
return [await viteVxeTableImportsPlugin()];
},
},
{ {
condition: nitroMock, condition: nitroMock,
plugins: async () => { plugins: async () => {

View File

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

View File

@ -123,6 +123,8 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
pwa?: boolean; pwa?: boolean;
/** pwa 插件配置 */ /** pwa 插件配置 */
pwaOptions?: Partial<PwaPluginOptions>; pwaOptions?: Partial<PwaPluginOptions>;
/** 是否开启vxe-table懒加载 */
vxeTableLazyImport?: boolean;
} }
interface LibraryPluginOptions extends CommonPluginOptions { interface LibraryPluginOptions extends CommonPluginOptions {

View File

@ -1,6 +1,6 @@
{ {
"name": "vben-admin-pro", "name": "vben-admin-pro",
"version": "5.3.0", "version": "5.3.2",
"private": true, "private": true,
"keywords": [ "keywords": [
"monorepo", "monorepo",
@ -25,11 +25,10 @@
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
"bootstrap": "pnpm install",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build", "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
"build:analyze": "turbo build:analyze", "build:analyze": "turbo build:analyze",
"build:docker": "./build-local-docker-image.sh",
"build:antd": "pnpm run build --filter=@vben/web-antd", "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:docs": "pnpm run build --filter=@vben/docs",
"build:ele": "pnpm run build --filter=@vben/web-ele", "build:ele": "pnpm run build --filter=@vben/web-ele",
"build:naive": "pnpm run build --filter=@vben/web-naive", "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:cspell": "cspell lint **/*.ts **/README.md .changeset/*.md --no-progress",
"check:dep": "vsh check-dep", "check:dep": "vsh check-dep",
"check:type": "turbo run typecheck", "check:type": "turbo run typecheck",
"clean": "vsh clean", "clean": "node ./scripts/clean.mjs",
"commit": "czg", "commit": "czg",
"dev": "turbo-run dev", "dev": "turbo-run dev",
"dev:antd": "pnpm -F @vben/web-antd run dev", "dev:antd": "pnpm -F @vben/web-antd run dev",
@ -50,20 +49,21 @@
"dev:play": "pnpm -F @vben/playground run dev", "dev:play": "pnpm -F @vben/playground run dev",
"format": "vsh lint --format", "format": "vsh lint --format",
"lint": "vsh lint", "lint": "vsh lint",
"postinstall": "turbo run stub", "postinstall": "pnpm -r run stub --if-present",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"prepare": "is-ci || husky", "prepare": "is-ci || husky",
"preview": "turbo-run preview", "preview": "turbo-run preview",
"publint": "vsh publint", "publint": "vsh publint",
"reinstall": "pnpm clean --del-lock && pnpm bootstrap", "reinstall": "pnpm clean --del-lock && pnpm install",
"test:unit": "vitest", "test:unit": "vitest run --dom",
"test:e2e": "turbo run test:e2e",
"update:deps": "pnpm update --latest --recursive", "update:deps": "pnpm update --latest --recursive",
"version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile" "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
}, },
"devDependencies": { "devDependencies": {
"@changesets/changelog-github": "catalog:", "@changesets/changelog-github": "catalog:",
"@changesets/cli": "catalog:", "@changesets/cli": "catalog:",
"@types/jsdom": "catalog:", "@playwright/test": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@vben/commitlint-config": "workspace:*", "@vben/commitlint-config": "workspace:*",
"@vben/eslint-config": "workspace:*", "@vben/eslint-config": "workspace:*",
@ -80,10 +80,11 @@
"autoprefixer": "catalog:", "autoprefixer": "catalog:",
"cross-env": "catalog:", "cross-env": "catalog:",
"cspell": "catalog:", "cspell": "catalog:",
"happy-dom": "catalog:",
"husky": "catalog:", "husky": "catalog:",
"is-ci": "catalog:", "is-ci": "catalog:",
"jsdom": "catalog:",
"lint-staged": "catalog:", "lint-staged": "catalog:",
"playwright": "catalog:",
"rimraf": "catalog:", "rimraf": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"turbo": "catalog:", "turbo": "catalog:",
@ -98,7 +99,7 @@
"node": ">=20.10.0", "node": ">=20.10.0",
"pnpm": ">=9.5.0" "pnpm": ">=9.5.0"
}, },
"packageManager": "pnpm@9.11.0", "packageManager": "pnpm@9.12.0",
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"allowedVersions": { "allowedVersions": {
@ -109,12 +110,11 @@
"@ctrl/tinycolor": "4.1.0", "@ctrl/tinycolor": "4.1.0",
"clsx": "2.1.1", "clsx": "2.1.1",
"pinia": "2.2.2", "pinia": "2.2.2",
"vue": "3.5.7" "vue": "3.5.10"
}, },
"neverBuiltDependencies": [ "neverBuiltDependencies": [
"canvas", "canvas",
"node-gyp", "node-gyp"
"playwright"
] ]
} }
} }

View File

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

View File

@ -82,11 +82,11 @@
@apply opacity-100; @apply opacity-100;
} }
input:-webkit-autofill { /* input:-webkit-autofill {
@apply border-none; @apply border-none;
box-shadow: 0 0 0 1000px transparent inset; box-shadow: 0 0 0 1000px transparent inset;
} } */
input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button { input[type='number']::-webkit-outer-spin-button {

View File

@ -53,6 +53,7 @@
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */ /* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
--accent: 216 5% 19%; --accent: 216 5% 19%;
--accent-lighter: 216 5% 11%;
--accent-hover: 216 5% 24%; --accent-hover: 216 5% 24%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
@ -78,6 +79,7 @@
/* 遮罩颜色 */ /* 遮罩颜色 */
--overlay: 0deg 0% 0% / 40%; --overlay: 0deg 0% 0% / 40%;
--overlay-content: 0deg 0% 0% / 40%;
/* 基本文字大小 */ /* 基本文字大小 */
--font-size-base: 16px; --font-size-base: 16px;

View File

@ -53,6 +53,7 @@
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */ /* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
--accent: 240 5% 96%; --accent: 240 5% 96%;
--accent-lighter: 240 0% 98%;
--accent-hover: 200deg 10% 90%; --accent-hover: 200deg 10% 90%;
--accent-foreground: 240 6% 10%; --accent-foreground: 240 6% 10%;
@ -78,7 +79,7 @@
/* 遮罩颜色 */ /* 遮罩颜色 */
--overlay: 0 0% 0% / 45%; --overlay: 0 0% 0% / 45%;
--overlay-light: 0 0% 95% / 45%; --overlay-content: 0 0% 95% / 45%;
/* 基本文字大小 */ /* 基本文字大小 */
--font-size-base: 16px; --font-size-base: 16px;

View File

@ -3,5 +3,19 @@ import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({ export default defineBuildConfig({
clean: true, clean: true,
declaration: 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'],
},
],
}); });

View File

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

View File

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

View File

@ -1,4 +1,5 @@
export { default as EmptyIcon } from './components/empty.vue';
export * from './create-icon'; export * from './create-icon';
export * from './lucide';
export * from './lucide';
export * from '@iconify/vue'; export * from '@iconify/vue';

View File

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

View File

@ -1,9 +1,11 @@
/** /** layout content 组件的高度 */
* @zh_CN css变量
* @en_US Layout content height
*/
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`; export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
/** layout content 组件的宽度 */
export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`; 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 * @zh_CN

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { bindMethods } from '../util'; import { bindMethods, getNestedValue } from '../util';
class TestClass { class TestClass {
public value: string; public value: string;
@ -78,3 +78,79 @@ describe('bindMethods', () => {
expect(value).toBe('test'); 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();
});
});

View File

@ -85,3 +85,11 @@ export function needsScrollbar() {
// 在其他情况下,根据 scrollHeight 和 innerHeight 比较判断 // 在其他情况下,根据 scrollHeight 和 innerHeight 比较判断
return doc.scrollHeight > window.innerHeight; return doc.scrollHeight > window.innerHeight;
} }
export function triggerWindowResize(): void {
// 创建一个新的 resize 事件
const resizeEvent = new Event('resize');
// 触发 window 的 resize 事件
window.dispatchEvent(resizeEvent);
}

View File

@ -1 +1,10 @@
import { createDefu } from 'defu';
export { createDefu as createMerge, defu as merge } 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;
}
});

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
export * from './use-content-style';
export * from './use-is-mobile'; export * from './use-is-mobile';
export * from './use-layout-style';
export * from './use-namespace'; export * from './use-namespace';
export * from './use-priority-value'; export * from './use-priority-value';
export * from './use-scroll-lock'; export * from './use-scroll-lock';

View File

@ -4,6 +4,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
import { import {
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT, CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH, CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT,
CSS_VARIABLE_LAYOUT_HEADER_HEIGHT,
} from '@vben-core/shared/constants'; } from '@vben-core/shared/constants';
import { import {
getElementVisibleRect, getElementVisibleRect,
@ -15,7 +17,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
/** /**
* @zh_CN content style * @zh_CN content style
*/ */
function useContentStyle() { export function useLayoutContentStyle() {
let resizeObserver: null | ResizeObserver = null; let resizeObserver: null | ResizeObserver = null;
const contentElement = ref<HTMLDivElement | null>(null); const contentElement = ref<HTMLDivElement | null>(null);
const visibleDomRect = ref<null | VisibleDomRect>(null); const visibleDomRect = ref<null | VisibleDomRect>(null);
@ -40,7 +42,7 @@ function useContentStyle() {
contentHeight.value = `${visibleDomRect.value.height}px`; contentHeight.value = `${visibleDomRect.value.height}px`;
contentWidth.value = `${visibleDomRect.value.width}px`; contentWidth.value = `${visibleDomRect.value.width}px`;
}, },
100, 16,
); );
onMounted(() => { onMounted(() => {
@ -58,4 +60,28 @@ function useContentStyle() {
return { contentElement, overlayStyle, visibleDomRect }; 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`;
},
};
}

View File

@ -2,8 +2,8 @@ import { getScrollbarWidth, needsScrollbar } from '@vben-core/shared/utils';
import { import {
useScrollLock as _useScrollLock, useScrollLock as _useScrollLock,
tryOnBeforeMount,
tryOnBeforeUnmount, tryOnBeforeUnmount,
tryOnMounted,
} from '@vueuse/core'; } from '@vueuse/core';
export const SCROLL_FIXED_CLASS = `_scroll__fixed_`; export const SCROLL_FIXED_CLASS = `_scroll__fixed_`;
@ -12,7 +12,7 @@ export function useScrollLock() {
const isLocked = _useScrollLock(document.body); const isLocked = _useScrollLock(document.body);
const scrollbarWidth = getScrollbarWidth(); const scrollbarWidth = getScrollbarWidth();
tryOnBeforeMount(() => { tryOnMounted(() => {
if (!needsScrollbar()) { if (!needsScrollbar()) {
return; return;
} }

View File

@ -39,7 +39,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"icpLink": "", "icpLink": "",
}, },
"footer": { "footer": {
"enable": true, "enable": false,
"fixed": false, "fixed": false,
}, },
"header": { "header": {

View File

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

View File

@ -39,7 +39,7 @@ const defaultPreferences: Preferences = {
icpLink: '', icpLink: '',
}, },
footer: { footer: {
enable: true, enable: false,
fixed: false, fixed: false,
}, },
header: { header: {

View File

@ -171,8 +171,9 @@ class PreferenceManager {
// 加载并合并当前存储的偏好设置 // 加载并合并当前存储的偏好设置
const mergedPreference = merge( const mergedPreference = merge(
{}, {},
overrides, // overrides,
this.loadCachedPreferences() || defaultPreferences, this.loadCachedPreferences() || {},
this.initialPreferences,
); );
// 更新偏好设置 // 更新偏好设置

View File

@ -28,6 +28,10 @@ function usePreferences() {
return isDarkTheme(preferences.theme.mode); return isDarkTheme(preferences.theme.mode);
}); });
const locale = computed(() => {
return preferences.app.locale;
});
const isMobile = computed(() => { const isMobile = computed(() => {
return appPreferences.value.isMobile; return appPreferences.value.isMobile;
}); });
@ -218,6 +222,7 @@ function usePreferences() {
isSideNav, isSideNav,
keepAlive, keepAlive,
layout, layout,
locale,
preferencesButtonPosition, preferencesButtonPosition,
sidebarCollapsed, sidebarCollapsed,
theme, theme,

View File

@ -1,24 +1,7 @@
// 假设这个文件为 FormApi.ts
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FormApi } from '../src/form-api'; 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', () => { describe('formApi', () => {
let formApi: FormApi; let formApi: FormApi;
@ -126,9 +109,8 @@ describe('formApi', () => {
}); });
it('should unmount form and reset state', () => { it('should unmount form and reset state', () => {
formApi.unmounted(); formApi.unmount();
expect(formApi.isMounted).toBe(false); expect(formApi.isMounted).toBe(false);
expect(formApi.stateHandler.reset).toHaveBeenCalled();
}); });
it('should validate form', async () => { it('should validate form', async () => {

View File

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, toRaw, unref } from 'vue'; import { computed, toRaw, unref, watch } from 'vue';
import { useSimpleLocale } from '@vben-core/composables'; import { useSimpleLocale } from '@vben-core/composables';
import { VbenExpandableArrow } from '@vben-core/shadcn-ui'; 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 { COMPONENT_MAP } from '../config';
import { injectFormProps } from '../use-form-context'; import { injectFormProps } from '../use-form-context';
@ -65,6 +65,16 @@ async function handleReset(e: Event) {
form.resetForm(); form.resetForm();
} }
} }
watch(
() => collapsed.value,
() => {
const props = unref(rootProps);
if (props.collapseTriggerResize) {
triggerWindowResize();
}
},
);
</script> </script>
<template> <template>
<div <div

View File

@ -1,4 +1,8 @@
import type { BaseFormComponentType, VbenFormAdapterOptions } from './types'; import type {
BaseFormComponentType,
FormCommonConfig,
VbenFormAdapterOptions,
} from './types';
import type { Component } from 'vue'; import type { Component } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
@ -16,6 +20,8 @@ import { defineRule } from 'vee-validate';
const DEFAULT_MODEL_PROP_NAME = 'modelValue'; const DEFAULT_MODEL_PROP_NAME = 'modelValue';
export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = { export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
DefaultResetActionButton: h(VbenButton, { size: 'sm', variant: 'outline' }), DefaultResetActionButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
DefaultSubmitActionButton: h(VbenButton, { size: 'sm', variant: 'default' }), DefaultSubmitActionButton: h(VbenButton, { size: 'sm', variant: 'default' }),
@ -37,6 +43,9 @@ export function setupVbenForm<
>(options: VbenFormAdapterOptions<T>) { >(options: VbenFormAdapterOptions<T>) {
const { components, config, defineRules } = options; const { components, config, defineRules } = options;
DEFAULT_FORM_COMMON_CONFIG.disabledOnChangeListener =
config?.disabledOnChangeListener ?? false;
if (defineRules) { if (defineRules) {
for (const key of Object.keys(defineRules)) { for (const key of Object.keys(defineRules)) {
defineRule(key, defineRules[key as never]); defineRule(key, defineRules[key as never]);

View File

@ -12,26 +12,23 @@ import { toRaw } from 'vue';
import { Store } from '@vben-core/shared/store'; import { Store } from '@vben-core/shared/store';
import { import {
bindMethods, bindMethods,
createMerge,
isFunction, isFunction,
mergeWithArrayOverride,
StateHandler, StateHandler,
} from '@vben-core/shared/utils'; } from '@vben-core/shared/utils';
const merge = createMerge((originObj, key, updates) => { import { objectPick } from '@vueuse/core';
if (Array.isArray(originObj[key]) && Array.isArray(updates)) {
originObj[key] = updates;
return true;
}
});
function getDefaultState(): VbenFormProps { function getDefaultState(): VbenFormProps {
return { return {
actionWrapperClass: '', actionWrapperClass: '',
collapsed: false, collapsed: false,
collapsedRows: 1, collapsedRows: 1,
collapseTriggerResize: false,
commonConfig: {}, commonConfig: {},
handleReset: undefined, handleReset: undefined,
handleSubmit: undefined, handleSubmit: undefined,
handleValuesChange: undefined,
layout: 'horizontal', layout: 'horizontal',
resetButtonOptions: {}, resetButtonOptions: {},
schema: [], schema: [],
@ -43,11 +40,11 @@ function getDefaultState(): VbenFormProps {
} }
export class FormApi { export class FormApi {
private prevState: null | VbenFormProps = null;
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>; // private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
public form = {} as FormActions; public form = {} as FormActions;
isMounted = false;
// private prevState!: ModalState; isMounted = false;
public state: null | VbenFormProps = null; public state: null | VbenFormProps = null;
stateHandler: StateHandler; stateHandler: StateHandler;
@ -66,7 +63,9 @@ export class FormApi {
}, },
{ {
onUpdate: () => { onUpdate: () => {
this.prevState = this.state;
this.state = this.store.state; this.state = this.store.state;
this.updateState();
}, },
}, },
); );
@ -87,6 +86,24 @@ export class FormApi {
return this.form; 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 方法 // 如果需要多次更新状态,可以使用 batch 方法
batchStore(cb: () => void) { batchStore(cb: () => void) {
this.store.batch(cb); this.store.batch(cb);
@ -101,6 +118,47 @@ export class FormApi {
return form.values; 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) { mount(formActions: FormActions) {
if (!this.isMounted) { if (!this.isMounted) {
Object.assign(this.form, formActions); Object.assign(this.form, formActions);
@ -155,19 +213,32 @@ export class FormApi {
) { ) {
if (isFunction(stateOrFn)) { if (isFunction(stateOrFn)) {
this.store.setState((prev) => { this.store.setState((prev) => {
return merge(stateOrFn(prev), prev); return mergeWithArrayOverride(stateOrFn(prev), prev);
}); });
} else { } 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( async setValues(
fields: Record<string, any>, fields: Record<string, any>,
filterFields: boolean = true,
shouldValidate: boolean = false, shouldValidate: boolean = false,
) { ) {
const form = await this.getForm(); const form = await this.getForm();
if (!filterFields) {
form.setValues(fields, shouldValidate); 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) { async submitForm(e?: Event) {
@ -180,7 +251,7 @@ export class FormApi {
return rawValues; return rawValues;
} }
unmounted() { unmount() {
// this.state = null; // this.state = null;
this.isMounted = false; this.isMounted = false;
this.stateHandler.reset(); this.stateHandler.reset();
@ -211,7 +282,10 @@ export class FormApi {
currentSchema.forEach((schema, index) => { currentSchema.forEach((schema, index) => {
const updatedData = updatedMap[schema.fieldName]; const updatedData = updatedMap[schema.fieldName];
if (updatedData) { if (updatedData) {
currentSchema[index] = merge(updatedData, schema) as FormSchema; currentSchema[index] = mergeWithArrayOverride(
updatedData,
schema,
) as FormSchema;
} }
}); });
this.setState({ schema: currentSchema }); this.setState({ schema: currentSchema });

View File

@ -3,7 +3,7 @@ import type { ZodType } from 'zod';
import type { FormSchema, MaybeComponentProps } from '../types'; import type { FormSchema, MaybeComponentProps } from '../types';
import { computed } from 'vue'; import { computed, nextTick, useTemplateRef, watch } from 'vue';
import { import {
FormControl, FormControl,
@ -32,6 +32,7 @@ const {
dependencies, dependencies,
description, description,
disabled, disabled,
disabledOnChangeListener,
fieldName, fieldName,
formFieldProps, formFieldProps,
label, label,
@ -49,6 +50,7 @@ const { componentBindEventMap, componentMap, isVertical } = useFormContext();
const formRenderProps = injectRenderFormProps(); const formRenderProps = injectRenderFormProps();
const values = useFormValues(); const values = useFormValues();
const errors = useFieldError(fieldName); const errors = useFieldError(fieldName);
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
const formApi = formRenderProps.form; const formApi = formRenderProps.form;
const isInValid = computed(() => errors.value?.length > 0); 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(() => { const shouldDisabled = computed(() => {
return isDisabled.value || disabled || computedProps.value?.disabled; return isDisabled.value || disabled || computedProps.value?.disabled;
}); });
@ -177,7 +191,7 @@ const fieldProps = computed(() => {
keepValue: true, keepValue: true,
label, label,
...(rules ? { rules } : {}), ...(rules ? { rules } : {}),
...formFieldProps, ...(formFieldProps as Record<string, any>),
}; };
}); });
@ -200,13 +214,14 @@ function fieldBindEvent(slotProps: Record<string, any>) {
return { return {
[`onUpdate:${bindEventField}`]: handler, [`onUpdate:${bindEventField}`]: handler,
[bindEventField]: value, [bindEventField]: value,
onChange: (e: Record<string, any>) => { onChange: disabledOnChangeListener
? undefined
: (e: Record<string, any>) => {
const shouldUnwrap = isEventObjectLike(e); const shouldUnwrap = isEventObjectLike(e);
const onChange = slotProps?.componentField?.onChange; const onChange = slotProps?.componentField?.onChange;
if (!shouldUnwrap) { if (!shouldUnwrap) {
return onChange?.(e); return onChange?.(e);
} }
return onChange?.(e?.target?.[bindEventField] ?? e); return onChange?.(e?.target?.[bindEventField] ?? e);
}, },
onInput: () => {}, onInput: () => {},
@ -226,6 +241,17 @@ function createComponentProps(slotProps: Record<string, any>) {
return binds; return binds;
} }
function autofocus() {
if (
fieldComponentRef.value &&
isFunction(fieldComponentRef.value.focus) &&
//
document.activeElement !== fieldComponentRef.value
) {
fieldComponentRef.value?.focus?.();
}
}
</script> </script>
<template> <template>
@ -275,6 +301,7 @@ function createComponentProps(slotProps: Record<string, any>) {
> >
<component <component
:is="fieldComponent" :is="fieldComponent"
ref="fieldComponentRef"
:class="{ :class="{
'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]': 'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
isInValid, isInValid,

View File

@ -1,12 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ZodTypeAny } from 'zod'; 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 { computed } from 'vue';
import { Form } from '@vben-core/shadcn-ui'; 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'; import { type GenericObject } from 'vee-validate';
@ -17,12 +22,16 @@ import { getBaseRules, getDefaultValueInZodStack } from './helper';
interface Props extends FormRenderProps {} interface Props extends FormRenderProps {}
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(
defineProps<{ globalCommonConfig?: FormCommonConfig } & Props>(),
{
collapsedRows: 1, collapsedRows: 1,
commonConfig: () => ({}), commonConfig: () => ({}),
globalCommonConfig: () => ({}),
showCollapseButton: false, showCollapseButton: false,
wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3', wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
}); },
);
const emits = defineEmits<{ const emits = defineEmits<{
submit: [event: any]; submit: [event: any];
@ -77,6 +86,7 @@ const computedSchema = computed(
componentProps = {}, componentProps = {},
controlClass = '', controlClass = '',
disabled, disabled,
disabledOnChangeListener = false,
formFieldProps = {}, formFieldProps = {},
formItemClass = '', formItemClass = '',
hideLabel = false, hideLabel = false,
@ -84,7 +94,7 @@ const computedSchema = computed(
labelClass = '', labelClass = '',
labelWidth = 100, labelWidth = 100,
wrapperClass = '', wrapperClass = '',
} = props.commonConfig; } = mergeWithArrayOverride(props.commonConfig, props.globalCommonConfig);
return (props.schema || []).map((schema, index) => { return (props.schema || []).map((schema, index) => {
const keepIndex = keepFormItemIndex.value; const keepIndex = keepFormItemIndex.value;
@ -96,6 +106,7 @@ const computedSchema = computed(
return { return {
disabled, disabled,
disabledOnChangeListener,
hideLabel, hideLabel,
hideRequiredMark, hideRequiredMark,
labelWidth, labelWidth,

View File

@ -1,5 +1,5 @@
import type { VbenButtonProps } from '@vben-core/shadcn-ui'; 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 { ZodTypeAny } from 'zod';
import type { FormApi } from './form-api'; import type { FormApi } from './form-api';
@ -33,6 +33,15 @@ export type FormItemClassType =
| (Record<never, never> & string) | (Record<never, never> & string)
| WrapperClassType; | WrapperClassType;
export type FormFieldOptions = Partial<
{
validateOnBlur?: boolean;
validateOnChange?: boolean;
validateOnInput?: boolean;
validateOnModelUpdate?: boolean;
} & FieldOptions
>;
export interface FormShape { export interface FormShape {
/** 默认值 */ /** 默认值 */
default?: any; default?: any;
@ -139,11 +148,16 @@ export interface FormCommonConfig {
* @default false * @default false
*/ */
disabled?: boolean; disabled?: boolean;
/**
* change事件监听
* @default false
*/
disabledOnChangeListener?: boolean;
/** /**
* *
* @default {} * @default {}
*/ */
formFieldProps?: Partial<typeof Field>; formFieldProps?: FormFieldOptions;
/** /**
* *
* @default "" * @default ""
@ -230,6 +244,11 @@ export interface FormRenderProps<
* @default 1 * @default 1
*/ */
collapsedRows?: number; collapsedRows?: number;
/**
* resize事件
* @default false
*/
collapseTriggerResize?: boolean;
/** /**
* 使 * 使
*/ */
@ -288,6 +307,10 @@ export interface VbenFormProps<
* *
*/ */
handleSubmit?: HandleSubmitFn; handleSubmit?: HandleSubmitFn;
/**
*
*/
handleValuesChange?: (values: Record<string, any>) => void;
/** /**
* *
*/ */
@ -317,6 +340,7 @@ export interface VbenFormAdapterOptions<
components: Partial<Record<T, Component>>; components: Partial<Record<T, Component>>;
config?: { config?: {
baseModelPropName?: string; baseModelPropName?: string;
disabledOnChangeListener?: boolean;
modelPropNameMap?: Partial<Record<T, string>>; modelPropNameMap?: Partial<Record<T, string>>;
}; };
defineRules?: { defineRules?: {

View File

@ -24,7 +24,7 @@ export function useVbenForm<
const Form = defineComponent( const Form = defineComponent(
(props: VbenFormProps, { attrs, slots }) => { (props: VbenFormProps, { attrs, slots }) => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
api.unmounted(); api.unmount();
}); });
return () => return () =>
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots); h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);

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