Merge remote-tracking branch 'yudao/dev' into dev
commit
e1b639640d
|
@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject
|
||||||
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
|
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
|
||||||
|
|
||||||
- If adding a new feature:
|
- If adding a new feature:
|
||||||
|
|
||||||
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
|
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
|
||||||
|
|
||||||
- If fixing bug:
|
- If fixing bug:
|
||||||
|
|
||||||
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
||||||
|
|
||||||
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
|
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
|
||||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import {
|
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
getCurrentInstance,
|
|
||||||
h,
|
|
||||||
ref,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
@ -85,16 +79,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||||
$t(`ui.placeholder.${type}`);
|
$t(`ui.placeholder.${type}`);
|
||||||
// 透传组件暴露的方法
|
// 透传组件暴露的方法
|
||||||
const innerRef = ref();
|
const innerRef = ref();
|
||||||
const publicApi: Recordable<any> = {};
|
expose(
|
||||||
expose(publicApi);
|
new Proxy(
|
||||||
const instance = getCurrentInstance();
|
{},
|
||||||
instance?.proxy?.$nextTick(() => {
|
{
|
||||||
for (const key in innerRef.value) {
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
if (typeof innerRef.value[key] === 'function') {
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
publicApi[key] = innerRef.value[key];
|
},
|
||||||
}
|
),
|
||||||
}
|
);
|
||||||
});
|
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
component,
|
component,
|
||||||
|
|
|
@ -63,7 +63,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO xingyu 暴露 modalApi 给父组件是否合适? trigger-node-config.vue 会有多个 conditionDialog 实例
|
// TODO xingyu 暴露 modalApi 给父组件是否合适? trigger-node-config.vue 会有多个 conditionDialog 实例
|
||||||
// 不用暴露啊,用 useVbenModal 就可以了
|
// TODO @jason:回复 from xingyu:不用暴露啊,用 useVbenModal 就可以了
|
||||||
defineExpose({ modalApi });
|
defineExpose({ modalApi });
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"#/*": "./src/*"
|
"#/*": "./src/*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@form-create/designer": "^3.2.6",
|
"@form-create/designer": "catalog:",
|
||||||
"@form-create/element-ui": "^3.2.11",
|
"@form-create/element-ui": "catalog:",
|
||||||
"@tinymce/tinymce-vue": "catalog:",
|
"@tinymce/tinymce-vue": "catalog:",
|
||||||
"@vben/access": "workspace:*",
|
"@vben/access": "workspace:*",
|
||||||
"@vben/common-ui": "workspace:*",
|
"@vben/common-ui": "workspace:*",
|
||||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import {
|
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
getCurrentInstance,
|
|
||||||
h,
|
|
||||||
ref,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
@ -142,16 +136,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||||
$t(`ui.placeholder.${type}`);
|
$t(`ui.placeholder.${type}`);
|
||||||
// 透传组件暴露的方法
|
// 透传组件暴露的方法
|
||||||
const innerRef = ref();
|
const innerRef = ref();
|
||||||
const publicApi: Recordable<any> = {};
|
expose(
|
||||||
expose(publicApi);
|
new Proxy(
|
||||||
const instance = getCurrentInstance();
|
{},
|
||||||
instance?.proxy?.$nextTick(() => {
|
{
|
||||||
for (const key in innerRef.value) {
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
if (typeof innerRef.value[key] === 'function') {
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
publicApi[key] = innerRef.value[key];
|
},
|
||||||
}
|
),
|
||||||
}
|
);
|
||||||
});
|
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
component,
|
component,
|
||||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import {
|
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
getCurrentInstance,
|
|
||||||
h,
|
|
||||||
ref,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
@ -85,16 +79,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||||
$t(`ui.placeholder.${type}`);
|
$t(`ui.placeholder.${type}`);
|
||||||
// 透传组件暴露的方法
|
// 透传组件暴露的方法
|
||||||
const innerRef = ref();
|
const innerRef = ref();
|
||||||
const publicApi: Recordable<any> = {};
|
expose(
|
||||||
expose(publicApi);
|
new Proxy(
|
||||||
const instance = getCurrentInstance();
|
{},
|
||||||
instance?.proxy?.$nextTick(() => {
|
{
|
||||||
for (const key in innerRef.value) {
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
if (typeof innerRef.value[key] === 'function') {
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
publicApi[key] = innerRef.value[key];
|
},
|
||||||
}
|
),
|
||||||
}
|
);
|
||||||
});
|
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
component,
|
component,
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FormModelDemo',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入',
|
||||||
|
},
|
||||||
|
fieldName: 'field1',
|
||||||
|
label: '字段1',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入',
|
||||||
|
},
|
||||||
|
fieldName: 'field2',
|
||||||
|
label: '字段2',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{ label: '选项1', value: '1' },
|
||||||
|
{ label: '选项2', value: '2' },
|
||||||
|
],
|
||||||
|
placeholder: '请输入',
|
||||||
|
},
|
||||||
|
fieldName: 'field3',
|
||||||
|
label: '字段3',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
await formApi.validateAndSubmitForm();
|
||||||
|
// modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const { values } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
formApi.setValues(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: '内嵌表单示例',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal>
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
|
@ -22,7 +22,7 @@ outline: deep
|
||||||
|
|
||||||
## 基础用法
|
## 基础用法
|
||||||
|
|
||||||
使用 `useVbenDrawer` 创建最基础的模态框。
|
使用 `useVbenDrawer` 创建最基础的抽屉。
|
||||||
|
|
||||||
<DemoPreview dir="demos/vben-drawer/basic" />
|
<DemoPreview dir="demos/vben-drawer/basic" />
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
|
||||||
|
|
||||||
::: info 注意
|
::: info 注意
|
||||||
|
|
||||||
- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
- `VbenDrawer` 组件对于参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
||||||
- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
||||||
|
@ -77,7 +77,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
| 属性名 | 描述 | 类型 | 默认值 |
|
| 属性名 | 描述 | 类型 | 默认值 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
| connectedComponent | 连接另一个Drawer组件 | `Component` | - |
|
||||||
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||||
| title | 标题 | `string\|slot` | - |
|
| title | 标题 | `string\|slot` | - |
|
||||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||||
|
@ -96,7 +96,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
||||||
| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
|
| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
|
||||||
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
||||||
| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
|
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
|
||||||
| class | modal的class,宽度通过这个配置 | `string` | - |
|
| class | modal的class,宽度通过这个配置 | `string` | - |
|
||||||
| contentClass | modal内容区域的class | `string` | - |
|
| contentClass | modal内容区域的class | `string` | - |
|
||||||
| footerClass | modal底部区域的class | `string` | - |
|
| footerClass | modal底部区域的class | `string` | - |
|
||||||
|
|
|
@ -324,6 +324,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||||
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
||||||
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
|
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
|
||||||
|
| scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | `boolean` | false |
|
||||||
|
|
||||||
::: tip handleValuesChange
|
::: tip handleValuesChange
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ const [Form] = useVbenForm({
|
||||||
handleSubmit: onSubmit,
|
handleSubmit: onSubmit,
|
||||||
// 垂直布局,label和input在不同行,值为vertical
|
// 垂直布局,label和input在不同行,值为vertical
|
||||||
// 水平布局,label和input在同一行
|
// 水平布局,label和input在同一行
|
||||||
|
scrollToFirstError: true,
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: [
|
schema: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
- If you want to contribute code to the project, please ensure your code complies with the project's coding standards.
|
- If you want to contribute code to the project, please ensure your code complies with the project's coding standards.
|
||||||
- If you are using `vscode`, you need to install the following plugins:
|
- If you are using `vscode`, you need to install the following plugins:
|
||||||
|
|
||||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code checking
|
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code checking
|
||||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting
|
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting
|
||||||
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Word syntax checking
|
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Word syntax checking
|
||||||
|
@ -157,7 +156,6 @@ The most effective solution is to perform Lint checks locally before committing.
|
||||||
The project defines corresponding hooks inside `lefthook.yml`:
|
The project defines corresponding hooks inside `lefthook.yml`:
|
||||||
|
|
||||||
- `pre-commit`: Runs before commit, used for code formatting and checking
|
- `pre-commit`: Runs before commit, used for code formatting and checking
|
||||||
|
|
||||||
- `code-workspace`: Updates VSCode workspace configuration
|
- `code-workspace`: Updates VSCode workspace configuration
|
||||||
- `lint-md`: Formats Markdown files
|
- `lint-md`: Formats Markdown files
|
||||||
- `lint-vue`: Formats and checks Vue files
|
- `lint-vue`: Formats and checks Vue files
|
||||||
|
@ -167,7 +165,6 @@ The project defines corresponding hooks inside `lefthook.yml`:
|
||||||
- `lint-json`: Formats other JSON files
|
- `lint-json`: Formats other JSON files
|
||||||
|
|
||||||
- `post-merge`: Runs after merge, used for automatic dependency installation
|
- `post-merge`: Runs after merge, used for automatic dependency installation
|
||||||
|
|
||||||
- `install`: Runs `pnpm install` to install new dependencies
|
- `install`: Runs `pnpm install` to install new dependencies
|
||||||
|
|
||||||
- `commit-msg`: Runs during commit, used for checking commit message format
|
- `commit-msg`: Runs during commit, used for checking commit message format
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
### 友情链接
|
### 友情链接
|
||||||
|
|
||||||
- 在您的网站上添加我们的友情链接,链接如下:
|
- 在您的网站上添加我们的友情链接,链接如下:
|
||||||
|
|
||||||
- 名称:Vben Admin
|
- 名称:Vben Admin
|
||||||
- 链接:https://www.vben.pro
|
- 链接:https://www.vben.pro
|
||||||
- 描述:Vben Admin 企业级开箱即用的中后台前端解决方案
|
- 描述:Vben Admin 企业级开箱即用的中后台前端解决方案
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
- 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。
|
- 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。
|
||||||
- 如果你使用的是 `vscode`,需要安装以下插件:
|
- 如果你使用的是 `vscode`,需要安装以下插件:
|
||||||
|
|
||||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查
|
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查
|
||||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化
|
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化
|
||||||
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查
|
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查
|
||||||
|
@ -157,7 +156,6 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风
|
||||||
项目在 `lefthook.yml` 内部定义了相应的 hooks:
|
项目在 `lefthook.yml` 内部定义了相应的 hooks:
|
||||||
|
|
||||||
- `pre-commit`: 在提交前运行,用于代码格式化和检查
|
- `pre-commit`: 在提交前运行,用于代码格式化和检查
|
||||||
|
|
||||||
- `code-workspace`: 更新 VSCode 工作区配置
|
- `code-workspace`: 更新 VSCode 工作区配置
|
||||||
- `lint-md`: 格式化 Markdown 文件
|
- `lint-md`: 格式化 Markdown 文件
|
||||||
- `lint-vue`: 格式化并检查 Vue 文件
|
- `lint-vue`: 格式化并检查 Vue 文件
|
||||||
|
@ -167,7 +165,6 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风
|
||||||
- `lint-json`: 格式化其他 JSON 文件
|
- `lint-json`: 格式化其他 JSON 文件
|
||||||
|
|
||||||
- `post-merge`: 在合并后运行,用于自动安装依赖
|
- `post-merge`: 在合并后运行,用于自动安装依赖
|
||||||
|
|
||||||
- `install`: 运行 `pnpm install` 安装新依赖
|
- `install`: 运行 `pnpm install` 安装新依赖
|
||||||
|
|
||||||
- `commit-msg`: 在提交时运行,用于检查提交信息格式
|
- `commit-msg`: 在提交时运行,用于检查提交信息格式
|
||||||
|
|
|
@ -98,7 +98,7 @@
|
||||||
"node": ">=20.10.0",
|
"node": ">=20.10.0",
|
||||||
"pnpm": ">=9.12.0"
|
"pnpm": ">=9.12.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0",
|
"packageManager": "pnpm@10.12.4",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"peerDependencyRules": {
|
"peerDependencyRules": {
|
||||||
"allowedVersions": {
|
"allowedVersions": {
|
||||||
|
|
|
@ -48,13 +48,18 @@ const queryFormStyle = computed(() => {
|
||||||
async function handleSubmit(e: Event) {
|
async function handleSubmit(e: Event) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
const { valid } = await form.validate();
|
const props = unref(rootProps);
|
||||||
|
if (!props.formApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { valid } = await props.formApi.validate();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = toRaw(await unref(rootProps).formApi?.getValues());
|
const values = toRaw(await props.formApi.getValues());
|
||||||
await unref(rootProps).handleSubmit?.(values);
|
await props.handleSubmit?.(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleReset(e: Event) {
|
async function handleReset(e: Event) {
|
||||||
|
|
|
@ -39,6 +39,7 @@ function getDefaultState(): VbenFormProps {
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
resetButtonOptions: {},
|
resetButtonOptions: {},
|
||||||
schema: [],
|
schema: [],
|
||||||
|
scrollToFirstError: false,
|
||||||
showCollapseButton: false,
|
showCollapseButton: false,
|
||||||
showDefaultActions: true,
|
showDefaultActions: true,
|
||||||
submitButtonOptions: {},
|
submitButtonOptions: {},
|
||||||
|
@ -253,6 +254,41 @@ export class FormApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滚动到第一个错误字段
|
||||||
|
* @param errors 验证错误对象
|
||||||
|
*/
|
||||||
|
scrollToFirstError(errors: Record<string, any> | string) {
|
||||||
|
// https://github.com/logaretm/vee-validate/discussions/3835
|
||||||
|
const firstErrorFieldName =
|
||||||
|
typeof errors === 'string' ? errors : Object.keys(errors)[0];
|
||||||
|
|
||||||
|
if (!firstErrorFieldName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let el = document.querySelector(
|
||||||
|
`[name="${firstErrorFieldName}"]`,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
// 如果通过 name 属性找不到,尝试通过组件引用查找, 正常情况下不会走到这,怕哪天 vee-validate 改了 name 属性有个兜底的
|
||||||
|
if (!el) {
|
||||||
|
const componentRef = this.getFieldComponentRef(firstErrorFieldName);
|
||||||
|
if (componentRef && componentRef.$el instanceof HTMLElement) {
|
||||||
|
el = componentRef.$el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
// 滚动到错误字段,添加一些偏移量以确保字段完全可见
|
||||||
|
el.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'nearest',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
|
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
|
||||||
const form = await this.getForm();
|
const form = await this.getForm();
|
||||||
form.setFieldValue(field, value, shouldValidate);
|
form.setFieldValue(field, value, shouldValidate);
|
||||||
|
@ -389,14 +425,21 @@ export class FormApi {
|
||||||
|
|
||||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||||
console.error('validate error', validateResult?.errors);
|
console.error('validate error', validateResult?.errors);
|
||||||
|
|
||||||
|
if (this.state?.scrollToFirstError) {
|
||||||
|
this.scrollToFirstError(validateResult.errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return validateResult;
|
return validateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateAndSubmitForm() {
|
async validateAndSubmitForm() {
|
||||||
const form = await this.getForm();
|
const form = await this.getForm();
|
||||||
const { valid } = await form.validate();
|
const { valid, errors } = await form.validate();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
if (this.state?.scrollToFirstError) {
|
||||||
|
this.scrollToFirstError(errors);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await this.submitForm();
|
return await this.submitForm();
|
||||||
|
@ -408,6 +451,10 @@ export class FormApi {
|
||||||
|
|
||||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||||
console.error('validate error', validateResult?.errors);
|
console.error('validate error', validateResult?.errors);
|
||||||
|
|
||||||
|
if (this.state?.scrollToFirstError) {
|
||||||
|
this.scrollToFirstError(fieldName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return validateResult;
|
return validateResult;
|
||||||
}
|
}
|
||||||
|
|
|
@ -389,6 +389,12 @@ export interface VbenFormProps<
|
||||||
*/
|
*/
|
||||||
resetButtonOptions?: ActionButtonOptions;
|
resetButtonOptions?: ActionButtonOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证失败时是否自动滚动到第一个错误字段
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
scrollToFirstError?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示默认操作按钮
|
* 是否显示默认操作按钮
|
||||||
* @default true
|
* @default true
|
||||||
|
|
|
@ -287,7 +287,11 @@ defineExpose({
|
||||||
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
|
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
|
||||||
>
|
>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
v-if="item.hasChildren"
|
v-if="
|
||||||
|
item.hasChildren &&
|
||||||
|
Array.isArray(item.value[childrenField]) &&
|
||||||
|
item.value[childrenField].length > 0
|
||||||
|
"
|
||||||
class="size-4 cursor-pointer transition"
|
class="size-4 cursor-pointer transition"
|
||||||
:class="{ 'rotate-90': isExpanded }"
|
:class="{ 'rotate-90': isExpanded }"
|
||||||
@click.stop="
|
@click.stop="
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
"@vueuse/core": "catalog:",
|
"@vueuse/core": "catalog:",
|
||||||
"@vueuse/integrations": "catalog:",
|
"@vueuse/integrations": "catalog:",
|
||||||
"crypto-js": "catalog:",
|
"crypto-js": "catalog:",
|
||||||
|
"json-bigint": "catalog:",
|
||||||
"qrcode": "catalog:",
|
"qrcode": "catalog:",
|
||||||
"tippy.js": "catalog:",
|
"tippy.js": "catalog:",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
|
|
|
@ -3,6 +3,7 @@ export { default as PointSelectionCaptchaCard } from './point-selection-captcha/
|
||||||
|
|
||||||
export { default as SliderCaptcha } from './slider-captcha/index.vue';
|
export { default as SliderCaptcha } from './slider-captcha/index.vue';
|
||||||
export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue';
|
export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue';
|
||||||
|
export { default as SliderTranslateCaptcha } from './slider-translate-captcha/index.vue';
|
||||||
export type * from './types';
|
export type * from './types';
|
||||||
|
|
||||||
export { default as Verification } from './verification/index.vue';
|
export { default as Verification } from './verification/index.vue';
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {
|
||||||
|
CaptchaVerifyPassingData,
|
||||||
|
SliderCaptchaActionType,
|
||||||
|
SliderRotateVerifyPassingData,
|
||||||
|
SliderTranslateCaptchaProps,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
useTemplateRef,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import SliderCaptcha from '../slider-captcha/index.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SliderTranslateCaptchaProps>(), {
|
||||||
|
defaultTip: '',
|
||||||
|
canvasWidth: 420,
|
||||||
|
canvasHeight: 280,
|
||||||
|
squareLength: 42,
|
||||||
|
circleRadius: 10,
|
||||||
|
src: '',
|
||||||
|
diffDistance: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [CaptchaVerifyPassingData];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const PI: number = Math.PI;
|
||||||
|
enum CanvasOpr {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Clip = 'clip',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Fill = 'fill',
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalValue = defineModel<boolean>({ default: false });
|
||||||
|
|
||||||
|
const slideBarRef = useTemplateRef<SliderCaptchaActionType>('slideBarRef');
|
||||||
|
const puzzleCanvasRef = useTemplateRef<HTMLCanvasElement>('puzzleCanvasRef');
|
||||||
|
const pieceCanvasRef = useTemplateRef<HTMLCanvasElement>('pieceCanvasRef');
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dragging: false,
|
||||||
|
startTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
pieceX: 0,
|
||||||
|
pieceY: 0,
|
||||||
|
moveDistance: 0,
|
||||||
|
isPassing: false,
|
||||||
|
showTip: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const left = ref('0');
|
||||||
|
|
||||||
|
const pieceStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
left: left.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function setLeft(val: string) {
|
||||||
|
left.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyTip = computed(() => {
|
||||||
|
return state.isPassing
|
||||||
|
? $t('ui.captcha.sliderTranslateSuccessTip', [
|
||||||
|
((state.endTime - state.startTime) / 1000).toFixed(1),
|
||||||
|
])
|
||||||
|
: $t('ui.captcha.sliderTranslateFailTip');
|
||||||
|
});
|
||||||
|
function handleStart() {
|
||||||
|
state.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragBarMove(data: SliderRotateVerifyPassingData) {
|
||||||
|
state.dragging = true;
|
||||||
|
const { moveX } = data;
|
||||||
|
state.moveDistance = moveX;
|
||||||
|
setLeft(`${moveX}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
const { pieceX } = state;
|
||||||
|
const { diffDistance } = props;
|
||||||
|
|
||||||
|
if (Math.abs(pieceX - state.moveDistance) >= (diffDistance || 3)) {
|
||||||
|
setLeft('0');
|
||||||
|
state.moveDistance = 0;
|
||||||
|
} else {
|
||||||
|
checkPass();
|
||||||
|
}
|
||||||
|
state.showTip = true;
|
||||||
|
state.dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPass() {
|
||||||
|
state.isPassing = true;
|
||||||
|
state.endTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.isPassing,
|
||||||
|
(isPassing) => {
|
||||||
|
if (isPassing) {
|
||||||
|
const { endTime, startTime } = state;
|
||||||
|
const time = (endTime - startTime) / 1000;
|
||||||
|
emit('success', { isPassing, time: time.toFixed(1) });
|
||||||
|
}
|
||||||
|
modalValue.value = isPassing;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function resetCanvas() {
|
||||||
|
const { canvasWidth, canvasHeight } = props;
|
||||||
|
const puzzleCanvas = unref(puzzleCanvasRef);
|
||||||
|
const pieceCanvas = unref(pieceCanvasRef);
|
||||||
|
if (!puzzleCanvas || !pieceCanvas) return;
|
||||||
|
pieceCanvas.width = canvasWidth;
|
||||||
|
const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
|
||||||
|
// Canvas2D: Multiple readback operations using getImageData
|
||||||
|
// are faster with the willReadFrequently attribute set to true.
|
||||||
|
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
|
||||||
|
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
|
||||||
|
willReadFrequently: true,
|
||||||
|
});
|
||||||
|
if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
|
||||||
|
puzzleCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
pieceCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
const { canvasWidth, canvasHeight, squareLength, circleRadius, src } = props;
|
||||||
|
const puzzleCanvas = unref(puzzleCanvasRef);
|
||||||
|
const pieceCanvas = unref(pieceCanvasRef);
|
||||||
|
if (!puzzleCanvas || !pieceCanvas) return;
|
||||||
|
const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
|
||||||
|
// Canvas2D: Multiple readback operations using getImageData
|
||||||
|
// are faster with the willReadFrequently attribute set to true.
|
||||||
|
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
|
||||||
|
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
|
||||||
|
willReadFrequently: true,
|
||||||
|
});
|
||||||
|
if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
|
||||||
|
const img = new Image();
|
||||||
|
// 解决跨域
|
||||||
|
img.crossOrigin = 'Anonymous';
|
||||||
|
img.src = src;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
draw(puzzleCanvasCtx, pieceCanvasCtx);
|
||||||
|
puzzleCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||||
|
pieceCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||||
|
const pieceLength = squareLength + 2 * circleRadius + 3;
|
||||||
|
const sx = state.pieceX;
|
||||||
|
const sy = state.pieceY - 2 * circleRadius - 1;
|
||||||
|
const imageData = pieceCanvasCtx.getImageData(
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
pieceLength,
|
||||||
|
pieceLength,
|
||||||
|
);
|
||||||
|
pieceCanvas.width = pieceLength;
|
||||||
|
pieceCanvasCtx.putImageData(imageData, 0, sy);
|
||||||
|
setLeft('0');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomNumberByRange(start: number, end: number) {
|
||||||
|
return Math.round(Math.random() * (end - start) + start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制拼图
|
||||||
|
function draw(ctx1: CanvasRenderingContext2D, ctx2: CanvasRenderingContext2D) {
|
||||||
|
const { canvasWidth, canvasHeight, squareLength, circleRadius } = props;
|
||||||
|
state.pieceX = getRandomNumberByRange(
|
||||||
|
squareLength + 2 * circleRadius,
|
||||||
|
canvasWidth - (squareLength + 2 * circleRadius),
|
||||||
|
);
|
||||||
|
state.pieceY = getRandomNumberByRange(
|
||||||
|
3 * circleRadius,
|
||||||
|
canvasHeight - (squareLength + 2 * circleRadius),
|
||||||
|
);
|
||||||
|
drawPiece(ctx1, state.pieceX, state.pieceY, CanvasOpr.Fill);
|
||||||
|
drawPiece(ctx2, state.pieceX, state.pieceY, CanvasOpr.Clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制拼图切块
|
||||||
|
function drawPiece(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
opr: CanvasOpr,
|
||||||
|
) {
|
||||||
|
const { squareLength, circleRadius } = props;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
ctx.arc(
|
||||||
|
x + squareLength / 2,
|
||||||
|
y - circleRadius + 2,
|
||||||
|
circleRadius,
|
||||||
|
0.72 * PI,
|
||||||
|
2.26 * PI,
|
||||||
|
);
|
||||||
|
ctx.lineTo(x + squareLength, y);
|
||||||
|
ctx.arc(
|
||||||
|
x + squareLength + circleRadius - 2,
|
||||||
|
y + squareLength / 2,
|
||||||
|
circleRadius,
|
||||||
|
1.21 * PI,
|
||||||
|
2.78 * PI,
|
||||||
|
);
|
||||||
|
ctx.lineTo(x + squareLength, y + squareLength);
|
||||||
|
ctx.lineTo(x, y + squareLength);
|
||||||
|
ctx.arc(
|
||||||
|
x + circleRadius - 2,
|
||||||
|
y + squareLength / 2,
|
||||||
|
circleRadius + 0.4,
|
||||||
|
2.76 * PI,
|
||||||
|
1.24 * PI,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
|
||||||
|
ctx.stroke();
|
||||||
|
opr === CanvasOpr.Clip ? ctx.clip() : ctx.fill();
|
||||||
|
ctx.globalCompositeOperation = 'destination-over';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resume() {
|
||||||
|
state.showTip = false;
|
||||||
|
const basicEl = unref(slideBarRef);
|
||||||
|
if (!basicEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.dragging = false;
|
||||||
|
state.isPassing = false;
|
||||||
|
state.pieceX = 0;
|
||||||
|
state.pieceY = 0;
|
||||||
|
|
||||||
|
basicEl.resume();
|
||||||
|
resetCanvas();
|
||||||
|
initCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCanvas();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
class="border-border relative flex cursor-pointer overflow-hidden border shadow-md"
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref="puzzleCanvasRef"
|
||||||
|
:width="canvasWidth"
|
||||||
|
:height="canvasHeight"
|
||||||
|
@click="resume"
|
||||||
|
></canvas>
|
||||||
|
<canvas
|
||||||
|
ref="pieceCanvasRef"
|
||||||
|
:width="canvasWidth"
|
||||||
|
:height="canvasHeight"
|
||||||
|
:style="pieceStyle"
|
||||||
|
class="absolute"
|
||||||
|
@click="resume"
|
||||||
|
></canvas>
|
||||||
|
<div
|
||||||
|
class="h-15 absolute bottom-3 left-0 z-10 block w-full text-center text-xs leading-[30px] text-white"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="state.showTip"
|
||||||
|
:class="{
|
||||||
|
'bg-success/80': state.isPassing,
|
||||||
|
'bg-destructive/80': !state.isPassing,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ verifyTip }}
|
||||||
|
</div>
|
||||||
|
<div v-if="!state.dragging" class="bg-black/30">
|
||||||
|
{{ defaultTip || $t('ui.captcha.sliderTranslateDefaultTip') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SliderCaptcha
|
||||||
|
ref="slideBarRef"
|
||||||
|
v-model="modalValue"
|
||||||
|
class="mt-5"
|
||||||
|
is-slot
|
||||||
|
@end="handleDragEnd"
|
||||||
|
@move="handleDragBarMove"
|
||||||
|
@start="handleStart"
|
||||||
|
>
|
||||||
|
<template v-for="(_, key) in $slots" :key="key" #[key]="slotProps">
|
||||||
|
<slot :name="key" v-bind="slotProps"></slot>
|
||||||
|
</template>
|
||||||
|
</SliderCaptcha>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -159,6 +159,42 @@ export interface SliderRotateCaptchaProps {
|
||||||
defaultTip?: string;
|
defaultTip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SliderTranslateCaptchaProps {
|
||||||
|
/**
|
||||||
|
* @description 拼图的宽度
|
||||||
|
* @default 420
|
||||||
|
*/
|
||||||
|
canvasWidth?: number;
|
||||||
|
/**
|
||||||
|
* @description 拼图的高度
|
||||||
|
* @default 280
|
||||||
|
*/
|
||||||
|
canvasHeight?: number;
|
||||||
|
/**
|
||||||
|
* @description 切块上正方形的长度
|
||||||
|
* @default 42
|
||||||
|
*/
|
||||||
|
squareLength?: number;
|
||||||
|
/**
|
||||||
|
* @description 切块上圆形的半径
|
||||||
|
* @default 10
|
||||||
|
*/
|
||||||
|
circleRadius?: number;
|
||||||
|
/**
|
||||||
|
* @description 图片的地址
|
||||||
|
*/
|
||||||
|
src?: string;
|
||||||
|
/**
|
||||||
|
* @description 允许的最大差距
|
||||||
|
* @default 3
|
||||||
|
*/
|
||||||
|
diffDistance?: number;
|
||||||
|
/**
|
||||||
|
* @description 默认提示文本
|
||||||
|
*/
|
||||||
|
defaultTip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CaptchaVerifyPassingData {
|
export interface CaptchaVerifyPassingData {
|
||||||
isPassing: boolean;
|
isPassing: boolean;
|
||||||
time: number | string;
|
time: number | string;
|
||||||
|
|
|
@ -29,7 +29,7 @@ function close() {
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
v-if="isDocAlertEnable() && isVisible"
|
v-if="isDocAlertEnable() && isVisible"
|
||||||
class="border-primary bg-primary/10 relative my-2 flex h-8 w-full items-center gap-2 rounded-md border p-2"
|
class="border-primary bg-primary/10 relative m-3 my-2 flex h-8 items-center gap-2 rounded-md border p-2"
|
||||||
>
|
>
|
||||||
<span class="grid shrink-0 place-items-center">
|
<span class="grid shrink-0 place-items-center">
|
||||||
<VbenIcon icon="mdi:information-outline" class="text-primary size-5" />
|
<VbenIcon icon="mdi:information-outline" class="text-primary size-5" />
|
||||||
|
|
|
@ -81,7 +81,7 @@ watch(keywordDebounce, () => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
watchDebounced(
|
watchDebounced(
|
||||||
() => props.prefix,
|
() => props.prefix,
|
||||||
async (prefix) => {
|
async (prefix) => {
|
||||||
|
|
|
@ -18,6 +18,7 @@ export {
|
||||||
VbenAvatar,
|
VbenAvatar,
|
||||||
VbenButton,
|
VbenButton,
|
||||||
VbenButtonGroup,
|
VbenButtonGroup,
|
||||||
|
VbenCheckbox,
|
||||||
VbenCheckButtonGroup,
|
VbenCheckButtonGroup,
|
||||||
VbenCountToAnimator,
|
VbenCountToAnimator,
|
||||||
VbenFullScreen,
|
VbenFullScreen,
|
||||||
|
@ -25,6 +26,7 @@ export {
|
||||||
VbenLoading,
|
VbenLoading,
|
||||||
VbenLogo,
|
VbenLogo,
|
||||||
VbenPinInput,
|
VbenPinInput,
|
||||||
|
VbenSelect,
|
||||||
VbenSpinner,
|
VbenSpinner,
|
||||||
VbenTree,
|
VbenTree,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
|
@ -18,6 +18,9 @@ import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import { isBoolean } from '@vben-core/shared/utils';
|
import { isBoolean } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import JsonBigint from 'json-bigint';
|
||||||
|
|
||||||
defineOptions({ name: 'JsonViewer' });
|
defineOptions({ name: 'JsonViewer' });
|
||||||
|
|
||||||
const props = withDefaults(defineProps<JsonViewerProps>(), {
|
const props = withDefaults(defineProps<JsonViewerProps>(), {
|
||||||
|
@ -68,6 +71,20 @@ function handleClick(event: MouseEvent) {
|
||||||
emit('click', event);
|
emit('click', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支持显示 bigint 数据,如较长的订单号
|
||||||
|
const jsonData = computed<Record<string, any>>(() => {
|
||||||
|
if (typeof props.value !== 'string') {
|
||||||
|
return props.value || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JsonBigint({ storeAsString: true }).parse(props.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JSON parse error:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const bindProps = computed<Recordable<any>>(() => {
|
const bindProps = computed<Recordable<any>>(() => {
|
||||||
const copyable = {
|
const copyable = {
|
||||||
copyText: $t('ui.jsonViewer.copy'),
|
copyText: $t('ui.jsonViewer.copy'),
|
||||||
|
@ -79,6 +96,7 @@ const bindProps = computed<Recordable<any>>(() => {
|
||||||
return {
|
return {
|
||||||
...props,
|
...props,
|
||||||
...attrs,
|
...attrs,
|
||||||
|
value: jsonData.value,
|
||||||
onCopied: (event: JsonViewerAction) => emit('copied', event),
|
onCopied: (event: JsonViewerAction) => emit('copied', event),
|
||||||
onKeyclick: (key: string) => emit('keyClick', key),
|
onKeyclick: (key: string) => emit('keyClick', key),
|
||||||
onClick: (event: MouseEvent) => handleClick(event),
|
onClick: (event: MouseEvent) => handleClick(event),
|
||||||
|
|
|
@ -86,18 +86,20 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
</VbenButtonGroup>
|
</VbenButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 flex justify-start">
|
<div class="mt-2 flex justify-start">
|
||||||
<p class="w-24 p-2">软件外包:</p>
|
<p class="w-24 p-2">软件外包:</p>
|
||||||
<img
|
<img
|
||||||
src="/wx-xingyu.png"
|
src="/wx-xingyu.png"
|
||||||
alt="数舵科技"
|
alt="数舵科技"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
|
width="80%"
|
||||||
@click="openWindow('https://shuduokeji.com')"
|
@click="openWindow('https://shuduokeji.com')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 flex justify-center pt-4 text-sm italic">
|
<p class="mt-2 flex justify-center pt-4 text-sm italic">
|
||||||
本项目采用 <Badge class="mx-2" variant="destructive">MIT</Badge>
|
本项目采用 <Badge class="mx-2" variant="destructive">MIT</Badge>
|
||||||
开源协议,个人与企业可100% 免费使用。
|
开源协议,个人与企业可100% 免费使用
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -56,8 +56,10 @@ async function handleChange(id: number | undefined) {
|
||||||
class="hover:bg-accent ml-1 mr-2 h-8 w-32 cursor-pointer rounded-full p-1.5"
|
class="hover:bg-accent ml-1 mr-2 h-8 w-32 cursor-pointer rounded-full p-1.5"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:align-justify" class="mr-4" />
|
<IconifyIcon icon="lucide:align-justify" class="mr-4" />
|
||||||
{{ $t('page.tenant.placeholder') }}
|
{{
|
||||||
<!-- {{ tenants.find((item) => item.id === visitTenantId)?.name }} -->
|
tenants.find((item) => item.id === visitTenantId)?.name ||
|
||||||
|
$t('page.tenant.placeholder')
|
||||||
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent class="w-40 p-0 pb-1">
|
<DropdownMenuContent class="w-40 p-0 pb-1">
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from '@vueuse/core';
|
} from '@vueuse/core';
|
||||||
|
|
||||||
import echarts from './echarts';
|
import echarts from './echarts';
|
||||||
|
// TODO @xingyu:有 500kb,china.json 会影响打包么?
|
||||||
import chinaMap from './map/china.json';
|
import chinaMap from './map/china.json';
|
||||||
|
|
||||||
type EchartsUIType = typeof EchartsUI | undefined;
|
type EchartsUIType = typeof EchartsUI | undefined;
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import type { VxeGridSlots, VxeGridSlotTypes } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { SlotsType } from 'vue';
|
||||||
|
|
||||||
import type { BaseFormComponentType } from '@vben-core/form-ui';
|
import type { BaseFormComponentType } from '@vben-core/form-ui';
|
||||||
|
|
||||||
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
||||||
|
@ -9,6 +13,12 @@ import { useStore } from '@vben-core/shared/store';
|
||||||
import { VxeGridApi } from './api';
|
import { VxeGridApi } from './api';
|
||||||
import VxeGrid from './use-vxe-grid.vue';
|
import VxeGrid from './use-vxe-grid.vue';
|
||||||
|
|
||||||
|
type FilteredSlots<T> = {
|
||||||
|
[K in keyof VxeGridSlots<T> as K extends 'form'
|
||||||
|
? never
|
||||||
|
: K]: VxeGridSlots<T>[K];
|
||||||
|
};
|
||||||
|
|
||||||
export function useVbenVxeGrid<
|
export function useVbenVxeGrid<
|
||||||
T extends Record<string, any> = any,
|
T extends Record<string, any> = any,
|
||||||
D extends BaseFormComponentType = BaseFormComponentType,
|
D extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
@ -31,6 +41,16 @@ export function useVbenVxeGrid<
|
||||||
{
|
{
|
||||||
name: 'VbenVxeGrid',
|
name: 'VbenVxeGrid',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
slots: Object as SlotsType<
|
||||||
|
{
|
||||||
|
// 表格标题
|
||||||
|
'table-title': undefined;
|
||||||
|
// 工具栏左侧部分
|
||||||
|
'toolbar-actions': VxeGridSlotTypes.DefaultSlotParams<T>;
|
||||||
|
// 工具栏右侧部分
|
||||||
|
'toolbar-tools': VxeGridSlotTypes.DefaultSlotParams<T>;
|
||||||
|
} & FilteredSlots<T>
|
||||||
|
>,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// Add reactivity support
|
// Add reactivity support
|
||||||
|
|
|
@ -46,8 +46,11 @@
|
||||||
"sliderDefaultText": "Slider and drag",
|
"sliderDefaultText": "Slider and drag",
|
||||||
"alt": "Supports img tag src attribute value",
|
"alt": "Supports img tag src attribute value",
|
||||||
"sliderRotateDefaultTip": "Click picture to refresh",
|
"sliderRotateDefaultTip": "Click picture to refresh",
|
||||||
|
"sliderTranslateDefaultTip": "Click picture to refresh",
|
||||||
"sliderRotateFailTip": "Validation failed",
|
"sliderRotateFailTip": "Validation failed",
|
||||||
"sliderRotateSuccessTip": "Validation successful, time {0} seconds",
|
"sliderRotateSuccessTip": "Validation successful, time {0} seconds",
|
||||||
|
"sliderTranslateFailTip": "Validation failed",
|
||||||
|
"sliderTranslateSuccessTip": "Validation successful, time {0} seconds",
|
||||||
"refreshAriaLabel": "Refresh captcha",
|
"refreshAriaLabel": "Refresh captcha",
|
||||||
"confirmAriaLabel": "Confirm selection",
|
"confirmAriaLabel": "Confirm selection",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
|
|
|
@ -45,8 +45,11 @@
|
||||||
"sliderSuccessText": "验证通过",
|
"sliderSuccessText": "验证通过",
|
||||||
"sliderDefaultText": "请按住滑块拖动",
|
"sliderDefaultText": "请按住滑块拖动",
|
||||||
"sliderRotateDefaultTip": "点击图片可刷新",
|
"sliderRotateDefaultTip": "点击图片可刷新",
|
||||||
|
"sliderTranslateDefaultTip": "点击图片可刷新",
|
||||||
"sliderRotateFailTip": "验证失败",
|
"sliderRotateFailTip": "验证失败",
|
||||||
"sliderRotateSuccessTip": "验证成功,耗时{0}秒",
|
"sliderRotateSuccessTip": "验证成功,耗时{0}秒",
|
||||||
|
"sliderTranslateFailTip": "验证失败",
|
||||||
|
"sliderTranslateSuccessTip": "验证成功,耗时{0}秒",
|
||||||
"alt": "支持img标签src属性值",
|
"alt": "支持img标签src属性值",
|
||||||
"refreshAriaLabel": "刷新验证码",
|
"refreshAriaLabel": "刷新验证码",
|
||||||
"confirmAriaLabel": "确认选择",
|
"confirmAriaLabel": "确认选择",
|
||||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import {
|
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
getCurrentInstance,
|
|
||||||
h,
|
|
||||||
ref,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
@ -82,16 +76,24 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||||
$t(`ui.placeholder.${type}`);
|
$t(`ui.placeholder.${type}`);
|
||||||
// 透传组件暴露的方法
|
// 透传组件暴露的方法
|
||||||
const innerRef = ref();
|
const innerRef = ref();
|
||||||
const publicApi: Recordable<any> = {};
|
// const publicApi: Recordable<any> = {};
|
||||||
expose(publicApi);
|
expose(
|
||||||
const instance = getCurrentInstance();
|
new Proxy(
|
||||||
instance?.proxy?.$nextTick(() => {
|
{},
|
||||||
for (const key in innerRef.value) {
|
{
|
||||||
if (typeof innerRef.value[key] === 'function') {
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
publicApi[key] = innerRef.value[key];
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
}
|
},
|
||||||
}
|
),
|
||||||
});
|
);
|
||||||
|
// const instance = getCurrentInstance();
|
||||||
|
// instance?.proxy?.$nextTick(() => {
|
||||||
|
// for (const key in innerRef.value) {
|
||||||
|
// if (typeof innerRef.value[key] === 'function') {
|
||||||
|
// publicApi[key] = innerRef.value[key];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
component,
|
component,
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"custom": "Custom Component",
|
"custom": "Custom Component",
|
||||||
"api": "Api",
|
"api": "Api",
|
||||||
"merge": "Merge Form",
|
"merge": "Merge Form",
|
||||||
|
"scrollToError": "Scroll to Error Field",
|
||||||
"upload-error": "Partial file upload failed",
|
"upload-error": "Partial file upload failed",
|
||||||
"upload-urls": "Urls after file upload",
|
"upload-urls": "Urls after file upload",
|
||||||
"file": "file",
|
"file": "file",
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
"pointSelection": "Point Selection Captcha",
|
"pointSelection": "Point Selection Captcha",
|
||||||
"sliderCaptcha": "Slider Captcha",
|
"sliderCaptcha": "Slider Captcha",
|
||||||
"sliderRotateCaptcha": "Rotate Captcha",
|
"sliderRotateCaptcha": "Rotate Captcha",
|
||||||
|
"sliderTranslateCaptcha": "Translate Captcha",
|
||||||
"captchaCardTitle": "Please complete the security verification",
|
"captchaCardTitle": "Please complete the security verification",
|
||||||
"pageDescription": "Verify user identity by clicking on specific locations in the image.",
|
"pageDescription": "Verify user identity by clicking on specific locations in the image.",
|
||||||
"pageTitle": "Captcha Component Example",
|
"pageTitle": "Captcha Component Example",
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"custom": "自定义组件",
|
"custom": "自定义组件",
|
||||||
"api": "Api",
|
"api": "Api",
|
||||||
"merge": "合并表单",
|
"merge": "合并表单",
|
||||||
|
"scrollToError": "滚动到错误字段",
|
||||||
"upload-error": "部分文件上传失败",
|
"upload-error": "部分文件上传失败",
|
||||||
"upload-urls": "文件上传后的网址",
|
"upload-urls": "文件上传后的网址",
|
||||||
"file": "文件",
|
"file": "文件",
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
"pointSelection": "点选验证",
|
"pointSelection": "点选验证",
|
||||||
"sliderCaptcha": "滑块验证",
|
"sliderCaptcha": "滑块验证",
|
||||||
"sliderRotateCaptcha": "旋转验证",
|
"sliderRotateCaptcha": "旋转验证",
|
||||||
|
"sliderTranslateCaptcha": "拼图滑块验证",
|
||||||
"captchaCardTitle": "请完成安全验证",
|
"captchaCardTitle": "请完成安全验证",
|
||||||
"pageDescription": "通过点击图片中的特定位置来验证用户身份。",
|
"pageDescription": "通过点击图片中的特定位置来验证用户身份。",
|
||||||
"pageTitle": "验证码组件示例",
|
"pageTitle": "验证码组件示例",
|
||||||
|
|
|
@ -85,6 +85,15 @@ const routes: RouteRecordRaw[] = [
|
||||||
title: $t('examples.form.merge'),
|
title: $t('examples.form.merge'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'FormScrollToErrorExample',
|
||||||
|
path: '/examples/form/scroll-to-error-test',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/examples/form/scroll-to-error-test.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('examples.form.scrollToError'),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -196,6 +205,15 @@ const routes: RouteRecordRaw[] = [
|
||||||
title: $t('examples.captcha.sliderRotateCaptcha'),
|
title: $t('examples.captcha.sliderRotateCaptcha'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'TranslateVerifyExample',
|
||||||
|
path: '/examples/captcha/slider-translate',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/examples/captcha/slider-translate-captcha.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('examples.captcha.sliderTranslateCaptcha'),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'CaptchaPointSelectionExample',
|
name: 'CaptchaPointSelectionExample',
|
||||||
path: '/examples/captcha/point-selection',
|
path: '/examples/captcha/point-selection',
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Page, SliderTranslateCaptcha } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Card, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
function handleSuccess() {
|
||||||
|
message.success('success!');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
description="用于前端简单的拼图滑块水平拖动校验场景"
|
||||||
|
title="拼图滑块校验"
|
||||||
|
>
|
||||||
|
<Card class="mb-5" title="基本示例">
|
||||||
|
<div class="flex items-center justify-center p-4">
|
||||||
|
<SliderTranslateCaptcha
|
||||||
|
src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/pro-avatar.webp"
|
||||||
|
:canvas-width="420"
|
||||||
|
:canvas-height="420"
|
||||||
|
@success="handleSuccess"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
|
@ -0,0 +1,183 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ScrollToErrorTest',
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollEnabled = ref(true);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
scrollToFirstError: scrollEnabled.value,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名',
|
||||||
|
},
|
||||||
|
fieldName: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
},
|
||||||
|
fieldName: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入手机号',
|
||||||
|
},
|
||||||
|
fieldName: 'phone',
|
||||||
|
label: '手机号',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入地址',
|
||||||
|
},
|
||||||
|
fieldName: 'address',
|
||||||
|
label: '地址',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入公司名称',
|
||||||
|
},
|
||||||
|
fieldName: 'company',
|
||||||
|
label: '公司名称',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入职位',
|
||||||
|
},
|
||||||
|
fieldName: 'position',
|
||||||
|
label: '职位',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{ label: '男', value: 'male' },
|
||||||
|
{ label: '女', value: 'female' },
|
||||||
|
],
|
||||||
|
placeholder: '请选择性别',
|
||||||
|
},
|
||||||
|
fieldName: 'gender',
|
||||||
|
label: '性别',
|
||||||
|
rules: 'selectRequired',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试 validateAndSubmitForm(验证并提交)
|
||||||
|
async function testValidateAndSubmit() {
|
||||||
|
await formApi.validateAndSubmitForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试 validate(手动验证整个表单)
|
||||||
|
async function testValidate() {
|
||||||
|
await formApi.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试 validateField(验证单个字段)
|
||||||
|
async function testValidateField() {
|
||||||
|
await formApi.validateField('username');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换滚动功能
|
||||||
|
function toggleScrollToError() {
|
||||||
|
formApi.setState({ scrollToFirstError: scrollEnabled.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充部分数据测试
|
||||||
|
async function fillPartialData() {
|
||||||
|
await formApi.resetForm();
|
||||||
|
await formApi.setFieldValue('username', '测试用户');
|
||||||
|
await formApi.setFieldValue('email', 'test@example.com');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
description="测试表单验证失败时自动滚动到错误字段的功能"
|
||||||
|
title="滚动到错误字段测试"
|
||||||
|
>
|
||||||
|
<Card title="功能测试">
|
||||||
|
<template #extra>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
v-model:checked="scrollEnabled"
|
||||||
|
@change="toggleScrollToError"
|
||||||
|
/>
|
||||||
|
<span>启用滚动到错误字段</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded bg-blue-50 p-4">
|
||||||
|
<h3 class="mb-2 font-medium">测试说明:</h3>
|
||||||
|
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||||
|
<li>所有验证方法在验证失败时都会自动滚动到第一个错误字段</li>
|
||||||
|
<li>可以通过右上角的开关控制是否启用自动滚动功能</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded border p-4">
|
||||||
|
<h4 class="mb-3 font-medium">验证方法测试:</h4>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Button type="primary" @click="testValidateAndSubmit">
|
||||||
|
测试 validateAndSubmitForm()
|
||||||
|
</Button>
|
||||||
|
<Button @click="testValidate"> 测试 validate() </Button>
|
||||||
|
<Button @click="testValidateField"> 测试 validateField() </Button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-xs text-gray-500">
|
||||||
|
<p>• validateAndSubmitForm(): 验证表单并提交</p>
|
||||||
|
<p>• validate(): 手动验证整个表单</p>
|
||||||
|
<p>• validateField(): 验证单个字段(这里测试用户名字段)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded border p-4">
|
||||||
|
<h4 class="mb-3 font-medium">数据填充测试:</h4>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Button @click="fillPartialData"> 填充部分数据 </Button>
|
||||||
|
<Button @click="() => formApi.resetForm()"> 清空表单 </Button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-xs text-gray-500">
|
||||||
|
<p>• 填充部分数据后验证,会滚动到第一个错误字段</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
5439
pnpm-lock.yaml
5439
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -12,121 +12,123 @@ packages:
|
||||||
- scripts/*
|
- scripts/*
|
||||||
- docs
|
- docs
|
||||||
- playground
|
- playground
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
'@ast-grep/napi': ^0.37.0
|
'@ast-grep/napi': ^0.37.0
|
||||||
'@changesets/changelog-github': ^0.5.1
|
'@changesets/changelog-github': ^0.5.1
|
||||||
'@changesets/cli': ^2.29.2
|
'@changesets/cli': ^2.29.5
|
||||||
'@changesets/git': ^3.0.4
|
'@changesets/git': ^3.0.4
|
||||||
'@clack/prompts': ^0.10.1
|
'@clack/prompts': ^0.10.1
|
||||||
'@commitlint/cli': ^19.8.0
|
'@commitlint/cli': ^19.8.1
|
||||||
'@commitlint/config-conventional': ^19.8.0
|
'@commitlint/config-conventional': ^19.8.1
|
||||||
'@ctrl/tinycolor': ^4.1.0
|
'@ctrl/tinycolor': ^4.1.0
|
||||||
'@eslint/js': ^9.26.0
|
'@eslint/js': ^9.30.1
|
||||||
'@faker-js/faker': ^9.7.0
|
'@faker-js/faker': ^9.9.0
|
||||||
'@iconify/json': ^2.2.334
|
'@iconify/json': ^2.2.354
|
||||||
'@iconify/tailwind': ^1.2.0
|
'@iconify/tailwind': ^1.2.0
|
||||||
'@iconify/vue': ^5.0.0
|
'@iconify/vue': ^5.0.0
|
||||||
'@intlify/core-base': ^11.1.3
|
'@intlify/core-base': ^11.1.7
|
||||||
'@intlify/unplugin-vue-i18n': ^6.0.8
|
'@intlify/unplugin-vue-i18n': ^6.0.8
|
||||||
'@jspm/generator': ^2.5.1
|
'@jspm/generator': ^2.6.2
|
||||||
'@manypkg/get-packages': ^3.0.0
|
'@manypkg/get-packages': ^3.0.0
|
||||||
'@microsoft/fetch-event-source': ^2.0.1
|
'@microsoft/fetch-event-source': ^2.0.1
|
||||||
'@nolebase/vitepress-plugin-git-changelog': ^2.17.0
|
'@nolebase/vitepress-plugin-git-changelog': ^2.18.0
|
||||||
'@playwright/test': ^1.52.0
|
'@playwright/test': ^1.53.2
|
||||||
'@pnpm/workspace.read-manifest': ^1000.1.4
|
'@pnpm/workspace.read-manifest': ^1000.2.0
|
||||||
'@stylistic/stylelint-plugin': ^3.1.2
|
'@stylistic/stylelint-plugin': ^3.1.3
|
||||||
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
|
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
|
||||||
'@tailwindcss/typography': ^0.5.16
|
'@tailwindcss/typography': ^0.5.16
|
||||||
'@tanstack/vue-query': ^5.75.1
|
'@tanstack/vue-query': ^5.81.5
|
||||||
'@tanstack/vue-store': ^0.7.0
|
'@tanstack/vue-store': ^0.7.1
|
||||||
'@tinymce/tinymce-vue': ^6.1.0
|
'@tinymce/tinymce-vue': ^6.1.0
|
||||||
'@form-create/ant-design-vue': ^3.2.22
|
'@form-create/ant-design-vue': ^3.2.27
|
||||||
'@ant-design/icons-vue': ^7.0.1
|
'@form-create/antd-designer': ^3.3.0
|
||||||
'@form-create/antd-designer': ^3.2.11
|
'@form-create/designer': ^3.3.0
|
||||||
'@form-create/naive-ui': ^3.2.22
|
'@form-create/naive-ui': ^3.2.27
|
||||||
|
'@form-create/element-ui': ^3.2.27
|
||||||
'@types/archiver': ^6.0.3
|
'@types/archiver': ^6.0.3
|
||||||
'@types/eslint': ^9.6.1
|
'@types/eslint': ^9.6.1
|
||||||
'@types/html-minifier-terser': ^7.0.2
|
'@types/html-minifier-terser': ^7.0.2
|
||||||
'@types/json-bigint': ^1.0.4
|
'@types/json-bigint': ^1.0.4
|
||||||
'@types/jsonwebtoken': ^9.0.9
|
'@types/jsonwebtoken': ^9.0.10
|
||||||
'@types/lodash.clonedeep': ^4.5.9
|
'@types/lodash.clonedeep': ^4.5.9
|
||||||
'@types/lodash.get': ^4.4.9
|
'@types/lodash.get': ^4.4.9
|
||||||
'@types/lodash.isequal': ^4.5.8
|
'@types/lodash.isequal': ^4.5.8
|
||||||
'@types/lodash.set': ^4.3.9
|
'@types/lodash.set': ^4.3.9
|
||||||
'@types/markdown-it': ^14.1.2
|
'@types/markdown-it': ^14.1.2
|
||||||
'@types/node': ^22.15.3
|
'@types/node': ^22.16.0
|
||||||
'@types/nprogress': ^0.2.3
|
'@types/nprogress': ^0.2.3
|
||||||
'@types/postcss-import': ^14.0.3
|
'@types/postcss-import': ^14.0.3
|
||||||
'@types/qrcode': ^1.5.5
|
'@types/qrcode': ^1.5.5
|
||||||
'@types/qs': ^6.9.18
|
'@types/qs': ^6.14.0
|
||||||
'@types/sortablejs': ^1.15.8
|
'@types/sortablejs': ^1.15.8
|
||||||
'@types/crypto-js': ^4.2.2
|
'@types/crypto-js': ^4.2.2
|
||||||
'@typescript-eslint/eslint-plugin': ^8.31.1
|
'@typescript-eslint/eslint-plugin': ^8.35.1
|
||||||
'@typescript-eslint/parser': ^8.31.1
|
'@typescript-eslint/parser': ^8.35.1
|
||||||
'@vee-validate/zod': ^4.15.0
|
'@vee-validate/zod': ^4.15.1
|
||||||
'@vite-pwa/vitepress': ^1.0.0
|
'@vite-pwa/vitepress': ^1.0.0
|
||||||
'@vitejs/plugin-vue': ^5.2.3
|
'@vitejs/plugin-vue': ^5.2.4
|
||||||
'@vitejs/plugin-vue-jsx': ^4.1.2
|
'@vitejs/plugin-vue-jsx': ^4.2.0
|
||||||
'@vue/reactivity': ^3.5.13
|
'@vue/reactivity': ^3.5.17
|
||||||
'@vue/shared': ^3.5.13
|
'@vue/shared': ^3.5.17
|
||||||
'@vue/test-utils': ^2.4.6
|
'@vue/test-utils': ^2.4.6
|
||||||
'@vueuse/core': ^13.1.0
|
'@vueuse/core': ^13.4.0
|
||||||
'@vueuse/integrations': ^13.1.0
|
'@vueuse/integrations': ^13.4.0
|
||||||
'@vueuse/motion': ^3.0.3
|
'@vueuse/motion': ^3.0.3
|
||||||
ant-design-vue: ^4.2.6
|
ant-design-vue: ^4.2.6
|
||||||
archiver: ^7.0.1
|
archiver: ^7.0.1
|
||||||
autoprefixer: ^10.4.21
|
autoprefixer: ^10.4.21
|
||||||
axios: ^1.9.0
|
axios: ^1.10.0
|
||||||
axios-mock-adapter: ^2.1.0
|
axios-mock-adapter: ^2.1.0
|
||||||
cac: ^6.7.14
|
cac: ^6.7.14
|
||||||
chalk: ^5.4.1
|
chalk: ^5.4.1
|
||||||
cheerio: ^1.0.0
|
cheerio: ^1.1.0
|
||||||
circular-dependency-scanner: ^2.3.0
|
circular-dependency-scanner: ^2.3.0
|
||||||
class-variance-authority: ^0.7.1
|
class-variance-authority: ^0.7.1
|
||||||
clsx: ^2.1.1
|
clsx: ^2.1.1
|
||||||
commitlint-plugin-function-rules: ^4.0.1
|
commitlint-plugin-function-rules: ^4.0.2
|
||||||
consola: ^3.4.2
|
consola: ^3.4.2
|
||||||
cross-env: ^7.0.3
|
cross-env: ^7.0.3
|
||||||
cropperjs: ^1.6.2
|
cropperjs: ^1.6.2
|
||||||
crypto-js: ^4.2.0
|
crypto-js: ^4.2.0
|
||||||
cspell: ^8.18.3
|
cspell: ^8.19.4
|
||||||
cssnano: ^7.0.6
|
cssnano: ^7.0.7
|
||||||
cz-git: ^1.11.1
|
cz-git: ^1.11.2
|
||||||
czg: ^1.11.1
|
czg: ^1.11.1
|
||||||
dayjs: ^1.11.13
|
dayjs: ^1.11.13
|
||||||
defu: ^6.1.4
|
defu: ^6.1.4
|
||||||
depcheck: ^1.4.7
|
depcheck: ^1.4.7
|
||||||
dotenv: ^16.5.0
|
dotenv: ^16.6.1
|
||||||
echarts: ^5.6.0
|
echarts: ^5.6.0
|
||||||
element-plus: ^2.9.9
|
element-plus: ^2.10.2
|
||||||
eslint: ^9.26.0
|
eslint: ^9.30.1
|
||||||
eslint-config-turbo: ^2.5.2
|
eslint-config-turbo: ^2.5.4
|
||||||
eslint-plugin-command: ^3.2.0
|
eslint-plugin-command: ^3.3.1
|
||||||
eslint-plugin-eslint-comments: ^3.2.0
|
eslint-plugin-eslint-comments: ^3.2.0
|
||||||
eslint-plugin-import-x: ^4.11.0
|
eslint-plugin-import-x: ^4.16.1
|
||||||
eslint-plugin-jsdoc: ^50.6.11
|
eslint-plugin-jsdoc: ^50.8.0
|
||||||
eslint-plugin-jsonc: ^2.20.0
|
eslint-plugin-jsonc: ^2.20.1
|
||||||
eslint-plugin-n: ^17.17.0
|
eslint-plugin-n: ^17.20.0
|
||||||
eslint-plugin-no-only-tests: ^3.3.0
|
eslint-plugin-no-only-tests: ^3.3.0
|
||||||
eslint-plugin-perfectionist: ^4.12.3
|
eslint-plugin-perfectionist: ^4.15.0
|
||||||
eslint-plugin-prettier: ^5.2.6
|
eslint-plugin-prettier: ^5.5.1
|
||||||
eslint-plugin-regexp: ^2.7.0
|
eslint-plugin-regexp: ^2.9.0
|
||||||
eslint-plugin-unicorn: ^59.0.0
|
eslint-plugin-unicorn: ^59.0.1
|
||||||
eslint-plugin-unused-imports: ^4.1.4
|
eslint-plugin-unused-imports: ^4.1.4
|
||||||
eslint-plugin-vitest: ^0.5.4
|
eslint-plugin-vitest: ^0.5.4
|
||||||
eslint-plugin-vue: ^10.1.0
|
eslint-plugin-vue: ^10.2.0
|
||||||
execa: ^9.5.2
|
execa: ^9.6.0
|
||||||
find-up: ^7.0.0
|
find-up: ^7.0.0
|
||||||
get-port: ^7.1.0
|
get-port: ^7.1.0
|
||||||
globals: ^16.0.0
|
globals: ^16.3.0
|
||||||
h3: ^1.15.3
|
h3: ^1.15.3
|
||||||
happy-dom: ^17.4.6
|
happy-dom: ^17.6.3
|
||||||
html-minifier-terser: ^7.2.0
|
html-minifier-terser: ^7.2.0
|
||||||
is-ci: ^4.1.0
|
is-ci: ^4.1.0
|
||||||
json-bigint: ^1.0.0
|
json-bigint: ^1.0.0
|
||||||
jsonc-eslint-parser: ^2.4.0
|
jsonc-eslint-parser: ^2.4.0
|
||||||
jsonwebtoken: ^9.0.2
|
jsonwebtoken: ^9.0.2
|
||||||
lefthook: ^1.11.12
|
lefthook: ^1.11.14
|
||||||
lodash.clonedeep: ^4.5.0
|
lodash.clonedeep: ^4.5.0
|
||||||
lodash.get: ^4.4.2
|
lodash.get: ^4.4.2
|
||||||
lodash.isequal: ^4.5.0
|
lodash.isequal: ^4.5.0
|
||||||
|
@ -138,74 +140,74 @@ catalog:
|
||||||
markmap-toolbar: ^0.17.0
|
markmap-toolbar: ^0.17.0
|
||||||
markmap-view: ^0.16.0
|
markmap-view: ^0.16.0
|
||||||
medium-zoom: ^1.1.0
|
medium-zoom: ^1.1.0
|
||||||
naive-ui: ^2.41.0
|
naive-ui: ^2.42.0
|
||||||
nitropack: ^2.11.11
|
nitropack: ^2.11.13
|
||||||
nprogress: ^0.2.0
|
nprogress: ^0.2.0
|
||||||
ora: ^8.2.0
|
ora: ^8.2.0
|
||||||
pinia: ^3.0.2
|
pinia: ^3.0.3
|
||||||
pinia-plugin-persistedstate: ^4.2.0
|
pinia-plugin-persistedstate: ^4.4.1
|
||||||
pkg-types: ^2.1.0
|
pkg-types: ^2.2.0
|
||||||
playwright: ^1.52.0
|
playwright: ^1.53.2
|
||||||
postcss: ^8.5.3
|
postcss: ^8.5.6
|
||||||
postcss-antd-fixes: ^0.2.0
|
postcss-antd-fixes: ^0.2.0
|
||||||
postcss-html: ^1.8.0
|
postcss-html: ^1.8.0
|
||||||
postcss-import: ^16.1.0
|
postcss-import: ^16.1.1
|
||||||
postcss-preset-env: ^10.1.6
|
postcss-preset-env: ^10.2.4
|
||||||
postcss-scss: ^4.0.9
|
postcss-scss: ^4.0.9
|
||||||
prettier: ^3.5.3
|
prettier: ^3.6.2
|
||||||
prettier-plugin-tailwindcss: ^0.6.11
|
prettier-plugin-tailwindcss: ^0.6.13
|
||||||
publint: ^0.3.12
|
publint: ^0.3.12
|
||||||
qrcode: ^1.5.4
|
qrcode: ^1.5.4
|
||||||
qs: ^6.14.0
|
qs: ^6.14.0
|
||||||
radix-vue: ^1.9.17
|
radix-vue: ^1.9.17
|
||||||
resolve.exports: ^2.0.3
|
resolve.exports: ^2.0.3
|
||||||
rimraf: ^6.0.1
|
rimraf: ^6.0.1
|
||||||
rollup: ^4.40.1
|
rollup: ^4.44.1
|
||||||
rollup-plugin-visualizer: ^5.14.0
|
rollup-plugin-visualizer: ^5.14.0
|
||||||
sass: ^1.87.0
|
sass: ^1.89.2
|
||||||
secure-ls: ^2.0.0
|
secure-ls: ^2.0.0
|
||||||
sortablejs: ^1.15.6
|
sortablejs: ^1.15.6
|
||||||
stylelint: ^16.19.1
|
stylelint: ^16.21.0
|
||||||
stylelint-config-recess-order: ^6.0.0
|
stylelint-config-recess-order: ^6.1.0
|
||||||
stylelint-config-recommended: ^16.0.0
|
stylelint-config-recommended: ^16.0.0
|
||||||
stylelint-config-recommended-scss: ^14.1.0
|
stylelint-config-recommended-scss: ^14.1.0
|
||||||
stylelint-config-recommended-vue: ^1.6.0
|
stylelint-config-recommended-vue: ^1.6.1
|
||||||
stylelint-config-standard: ^38.0.0
|
stylelint-config-standard: ^38.0.0
|
||||||
stylelint-order: ^7.0.0
|
stylelint-order: ^7.0.0
|
||||||
stylelint-prettier: ^5.0.3
|
stylelint-prettier: ^5.0.3
|
||||||
stylelint-scss: ^6.11.1
|
stylelint-scss: ^6.12.1
|
||||||
tailwind-merge: ^2.6.0
|
tailwind-merge: ^2.6.0
|
||||||
tailwindcss: ^3.4.17
|
tailwindcss: ^3.4.17
|
||||||
tailwindcss-animate: ^1.0.7
|
tailwindcss-animate: ^1.0.7
|
||||||
theme-colors: ^0.1.0
|
theme-colors: ^0.1.0
|
||||||
tippy.js: ^6.3.7
|
tippy.js: ^6.3.7
|
||||||
turbo: ^2.5.2
|
turbo: ^2.5.4
|
||||||
typescript: ^5.8.3
|
typescript: ^5.8.3
|
||||||
unbuild: ^3.5.0
|
unbuild: ^3.5.0
|
||||||
unplugin-element-plus: ^0.10.0
|
unplugin-element-plus: ^0.10.0
|
||||||
vee-validate: ^4.15.0
|
vee-validate: ^4.15.1
|
||||||
vite: ^6.3.4
|
vite: ^6.3.5
|
||||||
vite-plugin-compression: ^0.5.1
|
vite-plugin-compression: ^0.5.1
|
||||||
vite-plugin-dts: ^4.5.3
|
vite-plugin-dts: ^4.5.4
|
||||||
vite-plugin-html: ^3.2.2
|
vite-plugin-html: ^3.2.2
|
||||||
vite-plugin-lazy-import: ^1.0.7
|
vite-plugin-lazy-import: ^1.0.7
|
||||||
vite-plugin-pwa: ^1.0.0
|
vite-plugin-pwa: ^1.0.1
|
||||||
vite-plugin-vue-devtools: ^7.7.6
|
vite-plugin-vue-devtools: ^7.7.7
|
||||||
vitepress: ^1.6.3
|
vitepress: ^1.6.3
|
||||||
vitepress-plugin-group-icons: ^1.5.2
|
vitepress-plugin-group-icons: ^1.6.1
|
||||||
vitest: ^3.1.2
|
vitest: ^3.2.4
|
||||||
vue: ^3.5.13
|
vue: ^3.5.17
|
||||||
vue-dompurify-html: ^5.2.0
|
vue-dompurify-html: ^5.3.0
|
||||||
vue-eslint-parser: ^10.1.3
|
vue-eslint-parser: ^10.2.0
|
||||||
vue-i18n: ^11.1.3
|
vue-i18n: ^11.1.7
|
||||||
vue-json-viewer: ^3.0.4
|
vue-json-viewer: ^3.0.4
|
||||||
vue-router: ^4.5.1
|
vue-router: ^4.5.1
|
||||||
vue-tippy: ^6.7.0
|
vue-tippy: ^6.7.1
|
||||||
vue-tsc: 2.2.10
|
vue-tsc: 2.2.10
|
||||||
vxe-pc-ui: ^4.5.35
|
vxe-pc-ui: ^4.6.42
|
||||||
vxe-table: ^4.13.16
|
vxe-table: ^4.13.51
|
||||||
watermark-js-plus: ^1.6.0
|
watermark-js-plus: ^1.6.2
|
||||||
zod: ^3.24.3
|
zod: ^3.25.67
|
||||||
zod-defaults: ^0.1.3
|
zod-defaults: ^0.1.3
|
||||||
highlight.js: ^11.11.1
|
highlight.js: ^11.11.1
|
||||||
vue3-signature: ^0.2.4
|
vue3-signature: ^0.2.4
|
||||||
|
|
Loading…
Reference in New Issue