Browse Source
fix: improve the scroll bar flashing when the modal box is opened (#4438)
pull/4322/merge
fix: improve the scroll bar flashing when the modal box is opened (#4438)
pull/4322/merge
Vben
9 hours ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 530 additions and 42 deletions
-
16packages/@core/base/shared/src/utils/dom.ts
-
8packages/@core/composables/src/use-scroll-lock.ts
-
114packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
-
10packages/@core/preferences/__tests__/config.test.ts
-
6packages/@core/preferences/__tests__/preferences.test.ts
-
2packages/@core/preferences/tsconfig.json
-
146packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts
-
10packages/@core/ui-kit/form-ui/src/form-api.ts
-
2packages/@core/ui-kit/form-ui/tsconfig.json
-
3playground/src/locales/langs/en-US.json
-
3playground/src/locales/langs/zh-CN.json
-
8playground/src/router/routes/modules/examples.ts
-
208playground/src/views/examples/form/api.vue
-
3playground/src/views/examples/form/basic.vue
-
6playground/src/views/examples/form/custom.vue
-
17playground/src/views/examples/form/dynamic.vue
-
6playground/src/views/examples/form/query.vue
-
4playground/src/views/examples/form/rules.vue
@ -0,0 +1,114 @@ |
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html |
|||
|
|||
exports[`defaultPreferences immutability test > should not modify the config object 1`] = ` |
|||
{ |
|||
"app": { |
|||
"accessMode": "frontend", |
|||
"authPageLayout": "panel-right", |
|||
"checkUpdatesInterval": 1, |
|||
"colorGrayMode": false, |
|||
"colorWeakMode": false, |
|||
"compact": false, |
|||
"contentCompact": "wide", |
|||
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.6/source/avatar-v1.webp", |
|||
"dynamicTitle": true, |
|||
"enableCheckUpdates": true, |
|||
"enablePreferences": true, |
|||
"enableRefreshToken": false, |
|||
"isMobile": false, |
|||
"layout": "sidebar-nav", |
|||
"locale": "zh-CN", |
|||
"loginExpiredMode": "page", |
|||
"name": "Vben Admin", |
|||
"preferencesButtonPosition": "auto", |
|||
"watermark": false, |
|||
}, |
|||
"breadcrumb": { |
|||
"enable": true, |
|||
"hideOnlyOne": false, |
|||
"showHome": false, |
|||
"showIcon": true, |
|||
"styleType": "normal", |
|||
}, |
|||
"copyright": { |
|||
"companyName": "Vben", |
|||
"companySiteLink": "https://www.vben.pro", |
|||
"date": "2024", |
|||
"enable": true, |
|||
"icp": "", |
|||
"icpLink": "", |
|||
}, |
|||
"footer": { |
|||
"enable": true, |
|||
"fixed": false, |
|||
}, |
|||
"header": { |
|||
"enable": true, |
|||
"hidden": false, |
|||
"mode": "fixed", |
|||
}, |
|||
"logo": { |
|||
"enable": true, |
|||
"source": "https://unpkg.com/@vbenjs/static-source@0.1.6/source/logo-v1.webp", |
|||
}, |
|||
"navigation": { |
|||
"accordion": true, |
|||
"split": true, |
|||
"styleType": "rounded", |
|||
}, |
|||
"shortcutKeys": { |
|||
"enable": true, |
|||
"globalLockScreen": true, |
|||
"globalLogout": true, |
|||
"globalPreferences": true, |
|||
"globalSearch": true, |
|||
}, |
|||
"sidebar": { |
|||
"collapsed": false, |
|||
"collapsedShowTitle": false, |
|||
"enable": true, |
|||
"expandOnHover": true, |
|||
"extraCollapse": true, |
|||
"hidden": false, |
|||
"width": 224, |
|||
}, |
|||
"tabbar": { |
|||
"dragable": true, |
|||
"enable": true, |
|||
"height": 38, |
|||
"keepAlive": true, |
|||
"persist": true, |
|||
"showIcon": true, |
|||
"showMaximize": true, |
|||
"showMore": true, |
|||
"showRefresh": true, |
|||
"styleType": "chrome", |
|||
}, |
|||
"theme": { |
|||
"builtinType": "default", |
|||
"colorDestructive": "hsl(348 100% 61%)", |
|||
"colorPrimary": "hsl(212 100% 45%)", |
|||
"colorSuccess": "hsl(144 57% 58%)", |
|||
"colorWarning": "hsl(42 84% 61%)", |
|||
"mode": "dark", |
|||
"radius": "0.5", |
|||
"semiDarkHeader": false, |
|||
"semiDarkSidebar": true, |
|||
}, |
|||
"transition": { |
|||
"enable": true, |
|||
"loading": true, |
|||
"name": "fade-slide", |
|||
"progress": true, |
|||
}, |
|||
"widget": { |
|||
"fullscreen": true, |
|||
"globalSearch": true, |
|||
"languageToggle": true, |
|||
"lockScreen": true, |
|||
"notification": true, |
|||
"sidebarToggle": true, |
|||
"themeToggle": true, |
|||
}, |
|||
} |
|||
`; |
@ -0,0 +1,10 @@ |
|||
import { describe, expect, it } from 'vitest'; |
|||
|
|||
import { defaultPreferences } from '../src/config'; |
|||
|
|||
describe('defaultPreferences immutability test', () => { |
|||
// 创建快照,确保默认配置对象不被修改
|
|||
it('should not modify the config object', () => { |
|||
expect(defaultPreferences).toMatchSnapshot(); |
|||
}); |
|||
}); |
@ -1,8 +1,8 @@ |
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
|||
|
|||
import { defaultPreferences } from './config'; |
|||
import { PreferenceManager } from './preferences'; |
|||
import { isDarkTheme } from './update-css-variables'; |
|||
import { defaultPreferences } from '../src/config'; |
|||
import { PreferenceManager } from '../src/preferences'; |
|||
import { isDarkTheme } from '../src/update-css-variables'; |
|||
|
|||
describe('preferences', () => { |
|||
let preferenceManager: PreferenceManager; |
@ -1,6 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"include": ["src", "__tests__"], |
|||
"exclude": ["node_modules"] |
|||
} |
@ -0,0 +1,146 @@ |
|||
// 假设这个文件为 FormApi.ts
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
|||
|
|||
import { FormApi } from '../src/form-api'; |
|||
|
|||
vi.mock('@vben-core/shared/utils', () => ({ |
|||
bindMethods: vi.fn(), |
|||
createMerge: vi.fn((mergeFn) => { |
|||
return (stateOrFn, prev) => { |
|||
mergeFn(prev, 'key', stateOrFn); |
|||
return { ...prev, ...stateOrFn }; |
|||
}; |
|||
}), |
|||
isFunction: (fn: any) => typeof fn === 'function', |
|||
StateHandler: vi.fn().mockImplementation(() => ({ |
|||
reset: vi.fn(), |
|||
setConditionTrue: vi.fn(), |
|||
waitForCondition: vi.fn().mockResolvedValue(true), |
|||
})), |
|||
})); |
|||
|
|||
describe('formApi', () => { |
|||
let formApi: FormApi; |
|||
|
|||
beforeEach(() => { |
|||
formApi = new FormApi(); |
|||
}); |
|||
|
|||
it('should initialize with default state', () => { |
|||
expect(formApi.state).toEqual( |
|||
expect.objectContaining({ |
|||
actionWrapperClass: '', |
|||
collapsed: false, |
|||
collapsedRows: 1, |
|||
commonConfig: {}, |
|||
handleReset: undefined, |
|||
handleSubmit: undefined, |
|||
layout: 'horizontal', |
|||
resetButtonOptions: {}, |
|||
schema: [], |
|||
showCollapseButton: false, |
|||
showDefaultActions: true, |
|||
submitButtonOptions: {}, |
|||
wrapperClass: 'grid-cols-1', |
|||
}), |
|||
); |
|||
expect(formApi.isMounted).toBe(false); |
|||
}); |
|||
|
|||
it('should mount form actions', async () => { |
|||
const formActions: any = { |
|||
meta: {}, |
|||
resetForm: vi.fn(), |
|||
setFieldValue: vi.fn(), |
|||
setValues: vi.fn(), |
|||
submitForm: vi.fn(), |
|||
validate: vi.fn(), |
|||
values: { name: 'test' }, |
|||
}; |
|||
|
|||
await formApi.mount(formActions); |
|||
expect(formApi.isMounted).toBe(true); |
|||
expect(formApi.form).toEqual(formActions); |
|||
}); |
|||
|
|||
it('should get values from form', async () => { |
|||
const formActions: any = { |
|||
meta: {}, |
|||
values: { name: 'test' }, |
|||
}; |
|||
|
|||
await formApi.mount(formActions); |
|||
const values = await formApi.getValues(); |
|||
expect(values).toEqual({ name: 'test' }); |
|||
}); |
|||
|
|||
it('should set field value', async () => { |
|||
const setFieldValueMock = vi.fn(); |
|||
const formActions: any = { |
|||
meta: {}, |
|||
setFieldValue: setFieldValueMock, |
|||
values: { name: 'test' }, |
|||
}; |
|||
|
|||
await formApi.mount(formActions); |
|||
await formApi.setFieldValue('name', 'new value'); |
|||
expect(setFieldValueMock).toHaveBeenCalledWith( |
|||
'name', |
|||
'new value', |
|||
undefined, |
|||
); |
|||
}); |
|||
|
|||
it('should reset form', async () => { |
|||
const resetFormMock = vi.fn(); |
|||
const formActions: any = { |
|||
meta: {}, |
|||
resetForm: resetFormMock, |
|||
values: { name: 'test' }, |
|||
}; |
|||
|
|||
await formApi.mount(formActions); |
|||
await formApi.resetForm(); |
|||
expect(resetFormMock).toHaveBeenCalled(); |
|||
}); |
|||
|
|||
it('should call handleSubmit on submit', async () => { |
|||
const handleSubmitMock = vi.fn(); |
|||
const formActions: any = { |
|||
meta: {}, |
|||
submitForm: vi.fn().mockResolvedValue(true), |
|||
values: { name: 'test' }, |
|||
}; |
|||
|
|||
const state = { |
|||
handleSubmit: handleSubmitMock, |
|||
}; |
|||
|
|||
formApi.setState(state); |
|||
await formApi.mount(formActions); |
|||
|
|||
const result = await formApi.submitForm(); |
|||
expect(formActions.submitForm).toHaveBeenCalled(); |
|||
expect(handleSubmitMock).toHaveBeenCalledWith({ name: 'test' }); |
|||
expect(result).toEqual({ name: 'test' }); |
|||
}); |
|||
|
|||
it('should unmount form and reset state', () => { |
|||
formApi.unmounted(); |
|||
expect(formApi.isMounted).toBe(false); |
|||
expect(formApi.stateHandler.reset).toHaveBeenCalled(); |
|||
}); |
|||
|
|||
it('should validate form', async () => { |
|||
const validateMock = vi.fn().mockResolvedValue(true); |
|||
const formActions: any = { |
|||
meta: {}, |
|||
validate: validateMock, |
|||
}; |
|||
|
|||
await formApi.mount(formActions); |
|||
const isValid = await formApi.validate(); |
|||
expect(validateMock).toHaveBeenCalled(); |
|||
expect(isValid).toBe(true); |
|||
}); |
|||
}); |
@ -1,6 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"include": ["src", "__tests__"], |
|||
"exclude": ["node_modules"] |
|||
} |
@ -0,0 +1,208 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
|
|||
import { Button, Card, message, Space } from 'ant-design-vue'; |
|||
|
|||
import { useVbenForm } from '#/adapter'; |
|||
|
|||
const [BaseForm, formApi] = useVbenForm({ |
|||
// 所有表单项共用,可单独在表单内覆盖 |
|||
commonConfig: { |
|||
// 所有表单项 |
|||
componentProps: { |
|||
class: 'w-full', |
|||
}, |
|||
}, |
|||
// 使用 tailwindcss grid布局 |
|||
// 提交函数 |
|||
handleSubmit: onSubmit, |
|||
// 垂直布局,label和input在不同行,值为vertical |
|||
layout: 'horizontal', |
|||
// 水平布局,label和input在同一行 |
|||
schema: [ |
|||
{ |
|||
// 组件需要在 #/adapter.ts内注册,并加上类型 |
|||
component: 'Input', |
|||
// 对应组件的参数 |
|||
componentProps: { |
|||
placeholder: '请输入用户名', |
|||
}, |
|||
// 字段名 |
|||
fieldName: 'field1', |
|||
// 界面显示的label |
|||
label: 'field1', |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
componentProps: { |
|||
placeholder: '请输入', |
|||
}, |
|||
fieldName: 'field2', |
|||
label: 'field2', |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
componentProps: { |
|||
placeholder: '请输入', |
|||
}, |
|||
fieldName: 'field3', |
|||
label: 'field3', |
|||
}, |
|||
], |
|||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个 |
|||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', |
|||
}); |
|||
|
|||
function onSubmit(values: Record<string, any>) { |
|||
message.success({ |
|||
content: `form values: ${JSON.stringify(values)}`, |
|||
}); |
|||
} |
|||
|
|||
function handleClick( |
|||
action: |
|||
| 'batchAddSchema' |
|||
| 'batchDeleteSchema' |
|||
| 'disabled' |
|||
| 'hiddenAction' |
|||
| 'hiddenResetButton' |
|||
| 'hiddenSubmitButton' |
|||
| 'labelWidth' |
|||
| 'resetDisabled' |
|||
| 'resetLabelWidth' |
|||
| 'showAction' |
|||
| 'showResetButton' |
|||
| 'showSubmitButton' |
|||
| 'updateActionAlign' |
|||
| 'updateResetButton' |
|||
| 'updateSubmitButton', |
|||
) { |
|||
switch (action) { |
|||
case 'labelWidth': { |
|||
formApi.setState({ |
|||
commonConfig: { |
|||
labelWidth: 150, |
|||
}, |
|||
}); |
|||
break; |
|||
} |
|||
case 'resetLabelWidth': { |
|||
formApi.setState({ |
|||
commonConfig: { |
|||
labelWidth: 100, |
|||
}, |
|||
}); |
|||
break; |
|||
} |
|||
case 'disabled': { |
|||
formApi.setState({ commonConfig: { disabled: true } }); |
|||
break; |
|||
} |
|||
case 'resetDisabled': { |
|||
formApi.setState({ commonConfig: { disabled: false } }); |
|||
break; |
|||
} |
|||
case 'hiddenAction': { |
|||
formApi.setState({ showDefaultActions: false }); |
|||
break; |
|||
} |
|||
case 'showAction': { |
|||
formApi.setState({ showDefaultActions: true }); |
|||
break; |
|||
} |
|||
case 'hiddenResetButton': { |
|||
formApi.setState({ resetButtonOptions: { show: false } }); |
|||
break; |
|||
} |
|||
case 'showResetButton': { |
|||
formApi.setState({ resetButtonOptions: { show: true } }); |
|||
break; |
|||
} |
|||
case 'hiddenSubmitButton': { |
|||
formApi.setState({ submitButtonOptions: { show: false } }); |
|||
break; |
|||
} |
|||
case 'showSubmitButton': { |
|||
formApi.setState({ submitButtonOptions: { show: true } }); |
|||
break; |
|||
} |
|||
case 'updateResetButton': { |
|||
formApi.setState({ |
|||
resetButtonOptions: { disabled: true }, |
|||
}); |
|||
break; |
|||
} |
|||
case 'updateSubmitButton': { |
|||
formApi.setState({ |
|||
submitButtonOptions: { loading: true }, |
|||
}); |
|||
break; |
|||
} |
|||
case 'updateActionAlign': { |
|||
formApi.setState({ |
|||
// 可以自行调整class |
|||
actionWrapperClass: 'text-center', |
|||
}); |
|||
break; |
|||
} |
|||
case 'batchAddSchema': { |
|||
formApi.setState((prev) => { |
|||
const currentSchema = prev?.schema ?? []; |
|||
const newSchema = []; |
|||
for (let i = 0; i < 3; i++) { |
|||
newSchema.push({ |
|||
component: 'Input', |
|||
componentProps: { |
|||
placeholder: '请输入', |
|||
}, |
|||
fieldName: `field${i}${Date.now()}`, |
|||
label: `field+`, |
|||
}); |
|||
} |
|||
return { |
|||
schema: [...currentSchema, ...newSchema], |
|||
}; |
|||
}); |
|||
break; |
|||
} |
|||
case 'batchDeleteSchema': { |
|||
formApi.setState((prev) => { |
|||
const currentSchema = prev?.schema ?? []; |
|||
return { |
|||
schema: currentSchema.slice(0, -3), |
|||
}; |
|||
}); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Page description="表单组件api操作示例。" title="表单组件"> |
|||
<Space class="mb-5 flex-wrap"> |
|||
<Button @click="handleClick('labelWidth')">更改labelWidth</Button> |
|||
<Button @click="handleClick('resetLabelWidth')">还原labelWidth</Button> |
|||
<Button @click="handleClick('disabled')">禁用表单</Button> |
|||
<Button @click="handleClick('resetDisabled')">解除禁用</Button> |
|||
<Button @click="handleClick('hiddenAction')">隐藏操作按钮</Button> |
|||
<Button @click="handleClick('showAction')">显示操作按钮</Button> |
|||
<Button @click="handleClick('hiddenResetButton')">隐藏重置按钮</Button> |
|||
<Button @click="handleClick('showResetButton')">显示重置按钮</Button> |
|||
<Button @click="handleClick('hiddenSubmitButton')">隐藏提交按钮</Button> |
|||
<Button @click="handleClick('showSubmitButton')">显示提交按钮</Button> |
|||
<Button @click="handleClick('updateResetButton')">修改重置按钮</Button> |
|||
<Button @click="handleClick('updateSubmitButton')">修改提交按钮</Button> |
|||
<Button @click="handleClick('updateActionAlign')"> |
|||
调整操作按钮位置 |
|||
</Button> |
|||
<Button @click="handleClick('batchAddSchema')"> 批量添加表单项 </Button> |
|||
<Button @click="handleClick('batchDeleteSchema')"> |
|||
批量删除表单项 |
|||
</Button> |
|||
</Space> |
|||
<Card title="操作示例"> |
|||
<BaseForm /> |
|||
</Card> |
|||
</Page> |
|||
</template> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue