Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev
This commit is contained in:
commit
b0d44b07b4
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,3 +1,13 @@
|
||||
# 1.1.4
|
||||
|
||||
**Features**
|
||||
|
||||
- 通用的vxe-table排序事件(排序逻辑改为在排序事件中处理而非在api处理)
|
||||
|
||||
**BUG FIXES**
|
||||
|
||||
- 字典项为空时getDict方法无限调用接口((无奈兼容 不给字典item本来就是错误用法))
|
||||
|
||||
# 1.1.3
|
||||
|
||||
**REFACTOR**
|
||||
|
@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
@ -52,6 +52,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
@ -87,14 +88,32 @@ async function initComponentAdapter() {
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiSelect,
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelField: 'value',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
|
@ -130,3 +130,24 @@ export function vxeCheckboxChecked(
|
||||
) {
|
||||
return tableApi?.grid?.getCheckboxRecords?.()?.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的vxe-table排序事件 支持单/多字段排序
|
||||
* @param tableApi api
|
||||
* @param sortParams 排序参数
|
||||
*/
|
||||
export function vxeSortEvent(
|
||||
tableApi: ReturnType<typeof useVbenVxeGrid>[1],
|
||||
sortParams: VxeGridDefines.SortChangeEventParams,
|
||||
) {
|
||||
const { sortList } = sortParams;
|
||||
// 这里是排序取消 length为0 就不传参数了
|
||||
if (sortList.length === 0) {
|
||||
tableApi.query();
|
||||
return;
|
||||
}
|
||||
// 支持单/多字段排序
|
||||
const orderByColumn = sortList.map((item) => item.field).join(',');
|
||||
const isAsc = sortList.map((item) => item.order).join(',');
|
||||
tableApi.query({ orderByColumn, isAsc });
|
||||
}
|
||||
|
9
apps/web-antd/src/api/common.d.ts
vendored
9
apps/web-antd/src/api/common.d.ts
vendored
@ -21,13 +21,20 @@ export interface PageResult<T = any> {
|
||||
|
||||
/**
|
||||
* 分页查询参数
|
||||
*
|
||||
* 排序支持的用法如下:
|
||||
* {isAsc:"asc",orderByColumn:"id"} order by id asc
|
||||
* {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
|
||||
* {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
|
||||
* {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
|
||||
*
|
||||
* @param pageNum 当前页
|
||||
* @param pageSize 每页大小
|
||||
* @param orderByColumn 排序字段
|
||||
* @param isAsc 是否升序
|
||||
*/
|
||||
export interface PageQuery {
|
||||
isAsc?: boolean;
|
||||
isAsc?: string;
|
||||
orderByColumn?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
|
@ -20,7 +20,14 @@ export function getDict(dictName: string): DictData[] {
|
||||
})
|
||||
.finally(() => {
|
||||
// 移除请求状态缓存
|
||||
dictRequestCache.delete(dictName);
|
||||
/**
|
||||
* 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法)
|
||||
* 会导致if一直进入逻辑导致接口无限刷新
|
||||
* 在这里dictList为空时 不删除缓存
|
||||
*/
|
||||
if (dictList.length > 0) {
|
||||
dictRequestCache.delete(dictName);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -42,7 +49,14 @@ export function getDictOptions(dictName: string): Option[] {
|
||||
})
|
||||
.finally(() => {
|
||||
// 移除请求状态缓存
|
||||
dictRequestCache.delete(dictName);
|
||||
/**
|
||||
* 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法)
|
||||
* 会导致if一直进入逻辑导致接口五线刷新
|
||||
* 在这里dictList为空时 不删除缓存
|
||||
*/
|
||||
if (dictOptionList.length > 0) {
|
||||
dictRequestCache.delete(dictName);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -7,12 +7,12 @@ import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Modal, Space } from 'ant-design-vue';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
type VxeGridProps,
|
||||
vxeSortEvent,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
operLogClean,
|
||||
@ -60,18 +60,12 @@ const gridOptions: VxeGridProps<OperationLog> = {
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sort }, formValues = {}) => {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
|
||||
if (!isEmpty(sort)) {
|
||||
params.orderByColumn = sort.field;
|
||||
params.isAsc = sort.order;
|
||||
}
|
||||
|
||||
return await operLogList(params);
|
||||
},
|
||||
},
|
||||
@ -81,7 +75,10 @@ const gridOptions: VxeGridProps<OperationLog> = {
|
||||
keyField: 'operId',
|
||||
},
|
||||
sortConfig: {
|
||||
// 远程排序
|
||||
remote: true,
|
||||
// 支持多字段排序 默认关闭
|
||||
multiple: true,
|
||||
},
|
||||
id: 'monitor-operlog-index',
|
||||
};
|
||||
@ -90,9 +87,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
sortChange: () => {
|
||||
tableApi.query();
|
||||
},
|
||||
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -17,12 +17,12 @@ import {
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
type VxeGridProps,
|
||||
vxeSortEvent,
|
||||
} from '#/adapter/vxe-table';
|
||||
import { configInfoByKey } from '#/api/system/config';
|
||||
import { ossDownload, ossList, ossRemove } from '#/api/system/oss';
|
||||
@ -66,16 +66,12 @@ const gridOptions: VxeGridProps = {
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sort }, formValues = {}) => {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
if (!isEmpty(sort)) {
|
||||
params.orderByColumn = sort.field;
|
||||
params.isAsc = sort.order;
|
||||
}
|
||||
return await ossList(params);
|
||||
},
|
||||
},
|
||||
@ -86,7 +82,10 @@ const gridOptions: VxeGridProps = {
|
||||
height: 65,
|
||||
},
|
||||
sortConfig: {
|
||||
// 远程排序
|
||||
remote: true,
|
||||
// 支持多字段排序 默认关闭
|
||||
multiple: false,
|
||||
},
|
||||
id: 'system-oss-index',
|
||||
};
|
||||
@ -95,9 +94,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
sortChange: () => {
|
||||
tableApi.query();
|
||||
},
|
||||
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -192,7 +192,7 @@ const { hasAccessByCodes } = useAccess();
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<DeptTree
|
||||
v-model:select-dept-id="selectDeptId"
|
||||
:width="260"
|
||||
class="w-[260px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
|
@ -9,20 +9,22 @@ import type { Recordable } from '@vben/types';
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElCheckboxButton,
|
||||
ElCheckboxGroup,
|
||||
ElDatePicker,
|
||||
ElDivider,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElNotification,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElSelectV2,
|
||||
ElSpace,
|
||||
ElSwitch,
|
||||
@ -44,6 +46,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
@ -67,19 +70,55 @@ async function initComponentAdapter() {
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiSelect,
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: ElSelectV2,
|
||||
loadingSlot: 'loading',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: ElTreeSelect,
|
||||
props: { label: 'label', children: 'children' },
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'loading',
|
||||
optionsPropName: 'data',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: ElCheckboxGroup,
|
||||
CheckboxGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options, isButton } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(isButton ? ElCheckboxButton : ElCheckbox, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||
@ -104,8 +143,28 @@ async function initComponentAdapter() {
|
||||
},
|
||||
Input: withDefaultPlaceholder(ElInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
|
||||
RadioGroup: ElRadioGroup,
|
||||
Select: withDefaultPlaceholder(ElSelect, 'select'),
|
||||
RadioGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(attrs.isButton ? ElRadioButton : ElRadio, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElRadioGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
Select: (props, { attrs, slots }) => {
|
||||
return h(ElSelectV2, { ...props, attrs }, slots);
|
||||
},
|
||||
Space: ElSpace,
|
||||
Switch: ElSwitch,
|
||||
TimePicker: (props, { attrs, slots }) => {
|
||||
|
@ -12,6 +12,7 @@ setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
modelPropNameMap: {
|
||||
Upload: 'fileList',
|
||||
CheckboxGroup: 'model-value',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "Demos",
|
||||
"elementPlus": "Element Plus",
|
||||
"form": "Form",
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "演示",
|
||||
"elementPlus": "Element Plus",
|
||||
"form": "表单演示",
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
|
@ -23,6 +23,14 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/demos/element',
|
||||
component: () => import('#/views/demos/element/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.form'),
|
||||
},
|
||||
name: 'BasicForm',
|
||||
path: '/demos/form',
|
||||
component: () => import('#/views/demos/form/basic.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
180
apps/web-ele/src/views/demos/form/basic.vue
Normal file
180
apps/web-ele/src/views/demos/form/basic.vue
Normal file
@ -0,0 +1,180 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
|
||||
value: v,
|
||||
label: `选项${v}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox1',
|
||||
label: 'Checkbox1',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => {
|
||||
return ['A', 'B', 'C', 'D'].map((v) =>
|
||||
h(ElCheckbox, { label: v, value: v }),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbotton',
|
||||
label: 'CheckBotton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'select',
|
||||
label: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
checkbotton: ['B', 'C'],
|
||||
checkbox1: ['A', 'B'],
|
||||
date: new Date(),
|
||||
select: 'B',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
|
||||
title="表单演示"
|
||||
>
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="flex-auto">基础表单演示</span>
|
||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<Form />
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
@ -19,6 +19,8 @@ import {
|
||||
NDivider,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NRadio,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NSpace,
|
||||
@ -43,6 +45,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
@ -67,18 +70,52 @@ async function initComponentAdapter() {
|
||||
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiSelect,
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: NSelect,
|
||||
modelField: 'value',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: NTreeSelect,
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'arrow',
|
||||
keyField: 'value',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'options',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Checkbox: NCheckbox,
|
||||
CheckboxGroup: NCheckboxGroup,
|
||||
CheckboxGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () => options.map((option) => h(NCheckbox, option));
|
||||
}
|
||||
}
|
||||
return h(
|
||||
NCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ default: defaultSlot },
|
||||
);
|
||||
},
|
||||
DatePicker: NDatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
@ -98,7 +135,28 @@ async function initComponentAdapter() {
|
||||
},
|
||||
Input: withDefaultPlaceholder(NInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
|
||||
RadioGroup: NRadioGroup,
|
||||
RadioGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(attrs.isButton ? NRadioButton : NRadio, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
const groupRender = h(
|
||||
NRadioGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ default: defaultSlot },
|
||||
);
|
||||
return attrs.isButton
|
||||
? h(NSpace, { vertical: true }, () => groupRender)
|
||||
: groupRender;
|
||||
},
|
||||
Select: withDefaultPlaceholder(NSelect, 'select'),
|
||||
Space: NSpace,
|
||||
Switch: NSwitch,
|
||||
|
@ -2,6 +2,7 @@
|
||||
"title": "Demos",
|
||||
"naive": "Naive UI",
|
||||
"table": "Table",
|
||||
"form": "Form",
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
|
@ -2,6 +2,7 @@
|
||||
"title": "演示",
|
||||
"naive": "Naive UI",
|
||||
"table": "Table",
|
||||
"form": "表单",
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
|
@ -31,6 +31,14 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/demos/table',
|
||||
component: () => import('#/views/demos/table/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.form'),
|
||||
},
|
||||
name: 'Form',
|
||||
path: '/demos/form',
|
||||
component: () => import('#/views/demos/form/basic.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
143
apps/web-naive/src/views/demos/form/basic.vue
Normal file
143
apps/web-naive/src/views/demos/form/basic.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NCard, useMessage } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const message = useMessage();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
message.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
class: 'flex flex-wrap', // 如果选项过多,可以添加class来自动折叠
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
{ value: 'D', label: '选项D' },
|
||||
{ value: 'E', label: '选项E' },
|
||||
{ value: 'F', label: '选项F' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
],
|
||||
});
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
date: Date.now(),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="表单适配器重新包装了CheckboxGroup和RadioGroup,可以通过options属性传递选项数据(选项数据将作为子组件的属性)"
|
||||
title="表单演示"
|
||||
>
|
||||
<NCard title="基础表单">
|
||||
<template #header-extra>
|
||||
<NButton type="primary" @click="setFormValues">设置表单值</NButton>
|
||||
</template>
|
||||
<Form />
|
||||
</NCard>
|
||||
</Page>
|
||||
</template>
|
@ -162,6 +162,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
collapsed: false,
|
||||
text: '通用组件',
|
||||
items: [
|
||||
{
|
||||
link: 'common-ui/vben-api-component',
|
||||
text: 'ApiComponent Api组件包装器',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-modal',
|
||||
text: 'Modal 模态框',
|
||||
|
150
docs/src/components/common-ui/vben-api-component.md
Normal file
150
docs/src/components/common-ui/vben-api-component.md
Normal file
@ -0,0 +1,150 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben ApiComponent Api组件包装器
|
||||
|
||||
框架提供的API“包装器”,它一般不独立使用,主要用于包装其它组件,为目标组件提供自动获取远程数据的能力,但仍然保持了目标组件的原始用法。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
我们在各个应用的组件适配器中,使用ApiComponent包装了Select、TreeSelect组件,使得这些组件可以自动获取远程数据并生成选项。其它类似的组件(比如Cascader)如有需要也可以参考示例代码自行进行包装。
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过 `component` 传入其它组件的定义,并配置相关的其它属性(主要是一些名称映射)。包装组件将通过`api`获取数据(`beforerFetch`、`afterFetch`将分别在`api`运行前、运行后被调用),使用`resultField`从中提取数组,使用`valueField`、`labelField`等来从数据中提取value和label(如果提供了`childrenField`,会将其作为树形结构递归处理每一级数据),之后将处理好的数据通过`optionsPropName`指定的属性传递给目标组件。
|
||||
|
||||
::: details 包装级联选择器,点击下拉时开始加载远程数据
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { ApiComponent } from '@vben/common-ui';
|
||||
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
|
||||
const treeData: Record<string, any> = [
|
||||
{
|
||||
label: '浙江',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{
|
||||
value: 'hangzhou',
|
||||
label: '杭州',
|
||||
children: [
|
||||
{
|
||||
value: 'xihu',
|
||||
label: '西湖',
|
||||
},
|
||||
{
|
||||
value: 'sudi',
|
||||
label: '苏堤',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'jiaxing',
|
||||
label: '嘉兴',
|
||||
children: [
|
||||
{
|
||||
value: 'wuzhen',
|
||||
label: '乌镇',
|
||||
},
|
||||
{
|
||||
value: 'meihuazhou',
|
||||
label: '梅花洲',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'zhoushan',
|
||||
label: '舟山',
|
||||
children: [
|
||||
{
|
||||
value: 'putuoshan',
|
||||
label: '普陀山',
|
||||
},
|
||||
{
|
||||
value: 'taohuadao',
|
||||
label: '桃花岛',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '江苏',
|
||||
value: 'jiangsu',
|
||||
children: [
|
||||
{
|
||||
value: 'nanjing',
|
||||
label: '南京',
|
||||
children: [
|
||||
{
|
||||
value: 'zhonghuamen',
|
||||
label: '中华门',
|
||||
},
|
||||
{
|
||||
value: 'zijinshan',
|
||||
label: '紫金山',
|
||||
},
|
||||
{
|
||||
value: 'yuhuatai',
|
||||
label: '雨花台',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 模拟请求接口
|
||||
*/
|
||||
function fetchApi(): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(treeData);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ApiComponent
|
||||
:api="fetchApi"
|
||||
:component="Cascader"
|
||||
:immediate="false"
|
||||
children-field="children"
|
||||
loading-slot="suffixIcon"
|
||||
visible-event="onDropdownVisibleChange"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| component | 欲包装的组件 | `Component` | - |
|
||||
| numberToString | 是否将value从数字转为string | `boolean` | `false` |
|
||||
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - |
|
||||
| params | 传递给api的参数 | `Record<string, any>` | - |
|
||||
| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - |
|
||||
| labelField | label字段名 | `string` | `label` |
|
||||
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` |
|
||||
| valueField | value字段名 | `string` | `value` |
|
||||
| optionsPropName | 组件接收options数据的属性名称 | `string` | `options` |
|
||||
| modelPropName | 组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` |
|
||||
| immediate | 是否立即调用api | `boolean` | `true` |
|
||||
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` |
|
||||
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - |
|
||||
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - |
|
||||
| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
|
||||
| visibleEvent | 触发重新请求数据的事件名 | `string` | - |
|
||||
| loadingSlot | 组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
|
||||
|
||||
```
|
||||
|
||||
```
|
@ -74,6 +74,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
@ -95,6 +96,13 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
| headerClass | modal顶部区域的class | `string` | - |
|
||||
| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
|
||||
|
||||
::: info appendToMain
|
||||
|
||||
`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
|
||||
|
||||
:::
|
||||
|
||||
### Event
|
||||
|
||||
|
@ -306,6 +306,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| actionWrapperClass | 表单操作区域class | `any` | - |
|
||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>,) => void` | - |
|
||||
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
|
||||
|
@ -80,6 +80,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
@ -106,6 +107,13 @@ const [Modal, modalApi] = useVbenModal({
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
| headerClass | modal顶部区域的class | `string` | - |
|
||||
| bordered | 是否显示border | `boolean` | `false` |
|
||||
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
|
||||
|
||||
::: info appendToMain
|
||||
|
||||
`appendToMain`可以指定将弹窗挂载到内容区域,打开这种弹窗时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,弹窗会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。
|
||||
|
||||
:::
|
||||
|
||||
### Event
|
||||
|
||||
|
100
docs/src/demos/vben-api-component/cascader/index.vue
Normal file
100
docs/src/demos/vben-api-component/cascader/index.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import { ApiComponent } from '@vben/common-ui';
|
||||
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
|
||||
const treeData: Record<string, any> = [
|
||||
{
|
||||
label: '浙江',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{
|
||||
value: 'hangzhou',
|
||||
label: '杭州',
|
||||
children: [
|
||||
{
|
||||
value: 'xihu',
|
||||
label: '西湖',
|
||||
},
|
||||
{
|
||||
value: 'sudi',
|
||||
label: '苏堤',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'jiaxing',
|
||||
label: '嘉兴',
|
||||
children: [
|
||||
{
|
||||
value: 'wuzhen',
|
||||
label: '乌镇',
|
||||
},
|
||||
{
|
||||
value: 'meihuazhou',
|
||||
label: '梅花洲',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'zhoushan',
|
||||
label: '舟山',
|
||||
children: [
|
||||
{
|
||||
value: 'putuoshan',
|
||||
label: '普陀山',
|
||||
},
|
||||
{
|
||||
value: 'taohuadao',
|
||||
label: '桃花岛',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '江苏',
|
||||
value: 'jiangsu',
|
||||
children: [
|
||||
{
|
||||
value: 'nanjing',
|
||||
label: '南京',
|
||||
children: [
|
||||
{
|
||||
value: 'zhonghuamen',
|
||||
label: '中华门',
|
||||
},
|
||||
{
|
||||
value: 'zijinshan',
|
||||
label: '紫金山',
|
||||
},
|
||||
{
|
||||
value: 'yuhuatai',
|
||||
label: '雨花台',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 模拟请求接口
|
||||
*/
|
||||
function fetchApi(): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(treeData);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ApiComponent
|
||||
:api="fetchApi"
|
||||
:component="Cascader"
|
||||
:immediate="false"
|
||||
children-field="children"
|
||||
loading-slot="suffixIcon"
|
||||
visible-event="onDropdownVisibleChange"
|
||||
/>
|
||||
</template>
|
@ -99,7 +99,7 @@
|
||||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.14.4",
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
|
@ -7,6 +7,9 @@ export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
|
||||
/** layout footer 组件的高度 */
|
||||
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
|
||||
|
||||
/** 内容区域的组件ID */
|
||||
export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;
|
||||
|
||||
/**
|
||||
* @zh_CN 默认命名空间
|
||||
*/
|
||||
|
@ -142,13 +142,29 @@ defineExpose({
|
||||
"
|
||||
:style="queryFormStyle"
|
||||
>
|
||||
<template v-if="rootProps.actionButtonsReverse">
|
||||
<!-- 提交按钮前 -->
|
||||
<slot name="submit-before"></slot>
|
||||
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
>
|
||||
{{ submitButtonOptions.content }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<!-- 重置按钮前 -->
|
||||
<slot name="reset-before"></slot>
|
||||
|
||||
<component
|
||||
:is="COMPONENT_MAP.DefaultButton"
|
||||
v-if="resetButtonOptions.show"
|
||||
class="mr-3"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleReset"
|
||||
v-bind="resetButtonOptions"
|
||||
@ -156,18 +172,21 @@ defineExpose({
|
||||
{{ resetButtonOptions.content }}
|
||||
</component>
|
||||
|
||||
<!-- 提交按钮前 -->
|
||||
<slot name="submit-before"></slot>
|
||||
<template v-if="!rootProps.actionButtonsReverse">
|
||||
<!-- 提交按钮前 -->
|
||||
<slot name="submit-before"></slot>
|
||||
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
>
|
||||
{{ submitButtonOptions.content }}
|
||||
</component>
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
>
|
||||
{{ submitButtonOptions.content }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<!-- 展开按钮前 -->
|
||||
<slot name="expand-before"></slot>
|
||||
|
@ -307,6 +307,10 @@ export interface VbenFormProps<
|
||||
FormRenderProps<T>,
|
||||
'componentBindEventMap' | 'componentMap' | 'form'
|
||||
> {
|
||||
/**
|
||||
* 操作按钮是否反转(提交按钮前置)
|
||||
*/
|
||||
actionButtonsReverse?: boolean;
|
||||
/**
|
||||
* 表单操作区域class
|
||||
*/
|
||||
|
@ -62,9 +62,7 @@ function handleKeyDownEnter(event: KeyboardEvent) {
|
||||
watch(
|
||||
() => form.values,
|
||||
useDebounceFn(() => {
|
||||
(props.handleValuesChange ?? state.value.handleValuesChange)?.(
|
||||
toRaw(form.values),
|
||||
);
|
||||
forward.value.handleValuesChange?.(toRaw(form.values));
|
||||
state.value.submitOnChange && props.formApi?.submitForm();
|
||||
}, 300),
|
||||
{ deep: true },
|
||||
|
@ -40,6 +40,7 @@
|
||||
"@vben-core/composables": "workspace:*",
|
||||
"@vben-core/icons": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"vue": "catalog:"
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
} from '@vben-core/composables';
|
||||
import { Menu } from '@vben-core/icons';
|
||||
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
|
||||
|
||||
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
|
||||
|
||||
@ -457,6 +458,8 @@ function handleHeaderToggle() {
|
||||
emit('toggleSidebar');
|
||||
}
|
||||
}
|
||||
|
||||
const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -553,6 +556,7 @@ function handleHeaderToggle() {
|
||||
|
||||
<!-- </div> -->
|
||||
<LayoutContent
|
||||
:id="idMainContent"
|
||||
:content-compact="contentCompact"
|
||||
:content-compact-width="contentCompactWidth"
|
||||
:padding="contentPadding"
|
||||
|
@ -7,6 +7,11 @@ import type { Component, Ref } from 'vue';
|
||||
export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
|
||||
|
||||
export interface DrawerProps {
|
||||
/**
|
||||
* 是否挂载到内容区域
|
||||
* @default false
|
||||
*/
|
||||
appendToMain?: boolean;
|
||||
/**
|
||||
* 取消按钮文字
|
||||
*/
|
||||
@ -59,12 +64,12 @@ export interface DrawerProps {
|
||||
* 弹窗头部样式
|
||||
*/
|
||||
headerClass?: ClassType;
|
||||
|
||||
/**
|
||||
* 弹窗是否显示
|
||||
* @default false
|
||||
*/
|
||||
loading?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示遮罩
|
||||
* @default true
|
||||
@ -74,12 +79,12 @@ export interface DrawerProps {
|
||||
* 是否自动聚焦
|
||||
*/
|
||||
openAutoFocus?: boolean;
|
||||
|
||||
/**
|
||||
* 抽屉位置
|
||||
* @default right
|
||||
*/
|
||||
placement?: DrawerPlacement;
|
||||
|
||||
/**
|
||||
* 是否显示取消按钮
|
||||
* @default true
|
||||
@ -98,6 +103,10 @@ export interface DrawerProps {
|
||||
* 弹窗标题提示
|
||||
*/
|
||||
titleTooltip?: string;
|
||||
/**
|
||||
* 抽屉层级
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export interface DrawerState extends DrawerProps {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
||||
|
||||
import { provide, ref, useId, watch } from 'vue';
|
||||
import { computed, provide, ref, useId, watch } from 'vue';
|
||||
|
||||
import {
|
||||
useIsMobile,
|
||||
@ -23,6 +23,7 @@ import {
|
||||
VbenLoading,
|
||||
VisuallyHidden,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
|
||||
import { globalShareState } from '@vben-core/shared/global-state';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
@ -31,7 +32,9 @@ interface Props extends DrawerProps {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
appendToMain: false,
|
||||
drawerApi: undefined,
|
||||
zIndex: 1000,
|
||||
});
|
||||
|
||||
const components = globalShareState.getComponents();
|
||||
@ -46,6 +49,7 @@ const { isMobile } = useIsMobile();
|
||||
const state = props.drawerApi?.useStore?.();
|
||||
|
||||
const {
|
||||
appendToMain,
|
||||
cancelText,
|
||||
class: drawerClass,
|
||||
closable,
|
||||
@ -67,6 +71,7 @@ const {
|
||||
showConfirmButton,
|
||||
title,
|
||||
titleTooltip,
|
||||
zIndex,
|
||||
} = usePriorityValues(props, state);
|
||||
|
||||
watch(
|
||||
@ -110,6 +115,10 @@ function handleFocusOutside(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const getAppendTo = computed(() => {
|
||||
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Sheet
|
||||
@ -118,6 +127,7 @@ function handleFocusOutside(e: Event) {
|
||||
@update:open="() => drawerApi?.close()"
|
||||
>
|
||||
<SheetContent
|
||||
:append-to="getAppendTo"
|
||||
:class="
|
||||
cn('flex w-[520px] flex-col', drawerClass, {
|
||||
'!w-full': isMobile || placement === 'bottom' || placement === 'top',
|
||||
@ -127,6 +137,7 @@ function handleFocusOutside(e: Event) {
|
||||
:modal="modal"
|
||||
:open="state?.isOpen"
|
||||
:side="placement"
|
||||
:z-index="zIndex"
|
||||
@close-auto-focus="handleFocusOutside"
|
||||
@escape-key-down="escapeKeyDown"
|
||||
@focus-outside="handleFocusOutside"
|
||||
|
@ -3,6 +3,11 @@ import type { ModalApi } from './modal-api';
|
||||
import type { Component, Ref } from 'vue';
|
||||
|
||||
export interface ModalProps {
|
||||
/**
|
||||
* 是否要挂载到内容区域
|
||||
* @default false
|
||||
*/
|
||||
appendToMain?: boolean;
|
||||
/**
|
||||
* 是否显示边框
|
||||
* @default false
|
||||
@ -12,7 +17,6 @@ export interface ModalProps {
|
||||
* 取消按钮文字
|
||||
*/
|
||||
cancelText?: string;
|
||||
|
||||
/**
|
||||
* 是否居中
|
||||
* @default false
|
||||
@ -20,6 +24,7 @@ export interface ModalProps {
|
||||
centered?: boolean;
|
||||
|
||||
class?: string;
|
||||
|
||||
/**
|
||||
* 是否显示右上角的关闭按钮
|
||||
* @default true
|
||||
@ -112,6 +117,10 @@ export interface ModalProps {
|
||||
* 弹窗标题提示
|
||||
*/
|
||||
titleTooltip?: string;
|
||||
/**
|
||||
* 弹窗层级
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export interface ModalState extends ModalProps {
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
VbenLoading,
|
||||
VisuallyHidden,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
|
||||
import { globalShareState } from '@vben-core/shared/global-state';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
@ -32,6 +33,7 @@ interface Props extends ModalProps {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
appendToMain: false,
|
||||
modalApi: undefined,
|
||||
});
|
||||
|
||||
@ -52,6 +54,7 @@ const { isMobile } = useIsMobile();
|
||||
const state = props.modalApi?.useStore?.();
|
||||
|
||||
const {
|
||||
appendToMain,
|
||||
bordered,
|
||||
cancelText,
|
||||
centered,
|
||||
@ -78,6 +81,7 @@ const {
|
||||
showConfirmButton,
|
||||
title,
|
||||
titleTooltip,
|
||||
zIndex,
|
||||
} = usePriorityValues(props, state);
|
||||
|
||||
const shouldFullscreen = computed(
|
||||
@ -161,6 +165,9 @@ function handleFocusOutside(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
const getAppendTo = computed(() => {
|
||||
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Dialog
|
||||
@ -170,6 +177,7 @@ function handleFocusOutside(e: Event) {
|
||||
>
|
||||
<DialogContent
|
||||
ref="contentRef"
|
||||
:append-to="getAppendTo"
|
||||
:class="
|
||||
cn(
|
||||
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
|
||||
@ -187,6 +195,7 @@ function handleFocusOutside(e: Event) {
|
||||
:modal="modal"
|
||||
:open="state?.isOpen"
|
||||
:show-close="closable"
|
||||
:z-index="zIndex"
|
||||
close-class="top-3"
|
||||
@close-auto-focus="handleFocusOutside"
|
||||
@closed="() => modalApi?.onClosed()"
|
||||
|
@ -20,14 +20,16 @@ import DialogOverlay from './DialogOverlay.vue';
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
{
|
||||
appendTo?: HTMLElement | string;
|
||||
class?: ClassType;
|
||||
closeClass?: ClassType;
|
||||
modal?: boolean;
|
||||
open?: boolean;
|
||||
showClose?: boolean;
|
||||
zIndex?: number;
|
||||
} & DialogContentProps
|
||||
>(),
|
||||
{ showClose: true },
|
||||
{ appendTo: 'body', showClose: true, zIndex: 1000 },
|
||||
);
|
||||
const emits = defineEmits<
|
||||
{ close: []; closed: []; opened: [] } & DialogContentEmits
|
||||
@ -45,6 +47,18 @@ const delegatedProps = computed(() => {
|
||||
return delegated;
|
||||
});
|
||||
|
||||
function isAppendToBody() {
|
||||
return (
|
||||
props.appendTo === 'body' ||
|
||||
props.appendTo === document.body ||
|
||||
!props.appendTo
|
||||
);
|
||||
}
|
||||
|
||||
const position = computed(() => {
|
||||
return isAppendToBody() ? 'fixed' : 'absolute';
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
|
||||
@ -64,17 +78,22 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogPortal :to="appendTo">
|
||||
<Transition name="fade">
|
||||
<DialogOverlay v-if="open && modal" @click="() => emits('close')" />
|
||||
<DialogOverlay
|
||||
v-if="open && modal"
|
||||
:style="{ zIndex, position }"
|
||||
@click="() => emits('close')"
|
||||
/>
|
||||
</Transition>
|
||||
<DialogContent
|
||||
ref="contentRef"
|
||||
:style="{ zIndex, position }"
|
||||
@animationend="onAnimationEnd"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed z-[1000] w-full p-6 shadow-lg outline-none sm:rounded-xl',
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
@ -7,8 +7,5 @@ useScrollLock();
|
||||
const id = inject('DISMISSABLE_MODAL_ID');
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:data-dismissable-modal="id"
|
||||
class="bg-overlay fixed inset-0 z-[1000]"
|
||||
></div>
|
||||
<div :data-dismissable-modal="id" class="bg-overlay inset-0"></div>
|
||||
</template>
|
||||
|
@ -14,7 +14,10 @@ import {
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue';
|
||||
|
||||
const props = defineProps<{ class?: any } & DialogContentProps>();
|
||||
const props = withDefaults(
|
||||
defineProps<{ class?: any; zIndex?: number } & DialogContentProps>(),
|
||||
{ zIndex: 1000 },
|
||||
);
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
@ -29,7 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border fixed inset-0 z-[1000] grid place-items-center overflow-y-auto border bg-black/80"
|
||||
:style="{ zIndex }"
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border absolute inset-0 grid place-items-center overflow-y-auto border bg-black/80"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
@ -38,6 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
:style="{ zIndex }"
|
||||
v-bind="forwarded"
|
||||
@pointer-down-outside="
|
||||
(event) => {
|
||||
|
@ -15,17 +15,22 @@ import { type SheetVariants, sheetVariants } from './sheet';
|
||||
import SheetOverlay from './SheetOverlay.vue';
|
||||
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
appendTo?: HTMLElement | string;
|
||||
class?: any;
|
||||
modal?: boolean;
|
||||
open?: boolean;
|
||||
side?: SheetVariants['side'];
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<SheetContentProps>();
|
||||
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||
appendTo: 'body',
|
||||
zIndex: 1000,
|
||||
});
|
||||
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
@ -41,16 +46,29 @@ const delegatedProps = computed(() => {
|
||||
return delegated;
|
||||
});
|
||||
|
||||
function isAppendToBody() {
|
||||
return (
|
||||
props.appendTo === 'body' ||
|
||||
props.appendTo === document.body ||
|
||||
!props.appendTo
|
||||
);
|
||||
}
|
||||
|
||||
const position = computed(() => {
|
||||
return isAppendToBody() ? 'fixed' : 'absolute';
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogPortal :to="appendTo">
|
||||
<Transition name="fade">
|
||||
<SheetOverlay v-if="open && modal" />
|
||||
<SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
|
||||
</Transition>
|
||||
<DialogContent
|
||||
:class="cn(sheetVariants({ side }), 'z-[1000]', props.class)"
|
||||
:class="cn(sheetVariants({ side }), props.class)"
|
||||
:style="{ zIndex, position }"
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
>
|
||||
<slot></slot>
|
||||
|
@ -7,8 +7,5 @@ useScrollLock();
|
||||
const id = inject('DISMISSABLE_DRAWER_ID');
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:data-dismissable-drawer="id"
|
||||
class="bg-overlay fixed inset-0 z-[1000]"
|
||||
></div>
|
||||
<div :data-dismissable-drawer="id" class="bg-overlay inset-0"></div>
|
||||
</template>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export const sheetVariants = cva(
|
||||
'fixed z-[1000] bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
|
||||
'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
|
||||
{
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
@ -12,7 +12,7 @@ export const sheetVariants = cva(
|
||||
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
|
||||
'inset-y-0 right-0 w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
},
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnyPromiseFunction } from '@vben/types';
|
||||
|
||||
import { computed, ref, unref, useAttrs, type VNode, watch } from 'vue';
|
||||
import { type Component, computed, ref, unref, useAttrs, watch } from 'vue';
|
||||
|
||||
import { LoaderCircle } from '@vben/icons';
|
||||
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
|
||||
@ -10,37 +10,56 @@ import { objectOmit } from '@vueuse/core';
|
||||
|
||||
type OptionsItem = {
|
||||
[name: string]: any;
|
||||
children?: OptionsItem[];
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
// 组件
|
||||
component: VNode;
|
||||
/** 组件 */
|
||||
component: Component;
|
||||
/** 是否将value从数字转为string */
|
||||
numberToString?: boolean;
|
||||
/** 获取options数据的函数 */
|
||||
api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;
|
||||
/** 传递给api的参数 */
|
||||
params?: Record<string, any>;
|
||||
/** 从api返回的结果中提取options数组的字段名 */
|
||||
resultField?: string;
|
||||
/** label字段名 */
|
||||
labelField?: string;
|
||||
/** children字段名,需要层级数据的组件可用 */
|
||||
childrenField?: string;
|
||||
/** value字段名 */
|
||||
valueField?: string;
|
||||
/** 组件接收options数据的属性名 */
|
||||
optionsPropName?: string;
|
||||
/** 是否立即调用api */
|
||||
immediate?: boolean;
|
||||
/** 每次`visibleEvent`事件发生时都重新请求数据 */
|
||||
alwaysLoad?: boolean;
|
||||
/** 在api请求之前的回调函数 */
|
||||
beforeFetch?: AnyPromiseFunction<any, any>;
|
||||
/** 在api请求之后的回调函数 */
|
||||
afterFetch?: AnyPromiseFunction<any, any>;
|
||||
/** 直接传入选项数据,也作为api返回空数据时的后备数据 */
|
||||
options?: OptionsItem[];
|
||||
// 尾部插槽
|
||||
/** 组件的插槽名称,用来显示一个"加载中"的图标 */
|
||||
loadingSlot?: string;
|
||||
// 可见时触发的事件名
|
||||
/** 触发api请求的事件名 */
|
||||
visibleEvent?: string;
|
||||
modelField?: string;
|
||||
/** 组件的v-model属性名,默认为modelValue。部分组件可能为value */
|
||||
modelPropName?: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'ApiSelect', inheritAttrs: false });
|
||||
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
labelField: 'label',
|
||||
valueField: 'value',
|
||||
childrenField: '',
|
||||
optionsPropName: 'options',
|
||||
resultField: '',
|
||||
visibleEvent: '',
|
||||
numberToString: false,
|
||||
@ -50,7 +69,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
loadingSlot: '',
|
||||
beforeFetch: undefined,
|
||||
afterFetch: undefined,
|
||||
modelField: 'modelValue',
|
||||
modelPropName: 'modelValue',
|
||||
api: undefined,
|
||||
options: () => [],
|
||||
});
|
||||
@ -69,29 +88,34 @@ const loading = ref(false);
|
||||
const isFirstLoaded = ref(false);
|
||||
|
||||
const getOptions = computed(() => {
|
||||
const { labelField, valueField, numberToString } = props;
|
||||
const { labelField, valueField, childrenField, numberToString } = props;
|
||||
|
||||
const data: OptionsItem[] = [];
|
||||
const refOptionsData = unref(refOptions);
|
||||
|
||||
for (const next of refOptionsData) {
|
||||
if (next) {
|
||||
const value = get(next, valueField);
|
||||
data.push({
|
||||
...objectOmit(next, [labelField, valueField]),
|
||||
label: get(next, labelField),
|
||||
function transformData(data: OptionsItem[]): OptionsItem[] {
|
||||
return data.map((item) => {
|
||||
const value = get(item, valueField);
|
||||
return {
|
||||
...objectOmit(item, [labelField, valueField, childrenField]),
|
||||
label: get(item, labelField),
|
||||
value: numberToString ? `${value}` : value,
|
||||
});
|
||||
}
|
||||
...(childrenField && item[childrenField]
|
||||
? { children: transformData(item[childrenField]) }
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const data: OptionsItem[] = transformData(refOptionsData);
|
||||
|
||||
return data.length > 0 ? data : props.options;
|
||||
});
|
||||
|
||||
const bindProps = computed(() => {
|
||||
return {
|
||||
[props.modelField]: unref(modelValue),
|
||||
[`onUpdate:${props.modelField}`]: (val: string) => {
|
||||
[props.modelPropName]: unref(modelValue),
|
||||
[props.optionsPropName]: unref(getOptions),
|
||||
[`onUpdate:${props.modelPropName}`]: (val: string) => {
|
||||
modelValue.value = val;
|
||||
},
|
||||
...objectOmit(attrs, ['onUpdate:value']),
|
||||
@ -168,7 +192,6 @@ function emitChange() {
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="bindProps"
|
||||
:options="getOptions"
|
||||
:placeholder="$attrs.placeholder"
|
||||
>
|
||||
<template v-for="item in Object.keys($slots)" #[item]="data">
|
@ -0,0 +1 @@
|
||||
export { default as ApiComponent } from './api-component.vue';
|
@ -1 +0,0 @@
|
||||
export { default as ApiSelect } from './api-select.vue';
|
@ -1,4 +1,4 @@
|
||||
export * from './api-select';
|
||||
export * from './api-component';
|
||||
export * from './captcha';
|
||||
export * from './code-mirror';
|
||||
export * from './ellipsis-text';
|
||||
|
@ -1,5 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
ref,
|
||||
type StyleValue,
|
||||
useTemplateRef,
|
||||
} from 'vue';
|
||||
|
||||
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
@ -29,13 +36,11 @@ const shouldAutoHeight = ref(false);
|
||||
const headerRef = useTemplateRef<HTMLDivElement>('headerRef');
|
||||
const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
const contentStyle = computed<StyleValue>(() => {
|
||||
if (autoContentHeight) {
|
||||
return {
|
||||
height: shouldAutoHeight.value
|
||||
? `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`
|
||||
: '0',
|
||||
// 'overflow-y': shouldAutoHeight.value?'auto':'unset',
|
||||
height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`,
|
||||
overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
VxeInput,
|
||||
VxeLoading,
|
||||
VxeModal,
|
||||
VxeNumberInput,
|
||||
VxePager,
|
||||
// VxeList,
|
||||
// VxeModal,
|
||||
@ -82,6 +83,7 @@ export function initVxeTable() {
|
||||
// VxeUI.component(VxeList);
|
||||
VxeUI.component(VxeLoading);
|
||||
VxeUI.component(VxeModal);
|
||||
VxeUI.component(VxeNumberInput);
|
||||
// VxeUI.component(VxeOptgroup);
|
||||
// VxeUI.component(VxeOption);
|
||||
VxeUI.component(VxePager);
|
||||
|
@ -42,7 +42,7 @@ export interface VxeGridProps {
|
||||
/**
|
||||
* vxe-grid 事件
|
||||
*/
|
||||
gridEvents?: DeepPartial<VxeGridListeners>;
|
||||
gridEvents?: Partial<VxeGridListeners>;
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
|
@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
@ -49,6 +49,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
@ -82,13 +83,31 @@ async function initComponentAdapter() {
|
||||
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiSelect,
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelField: 'value',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
|
21
playground/src/views/examples/drawer/in-content-demo.vue
Normal file
21
playground/src/views/examples/drawer/in-content-demo.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
message.info('onConfirm');
|
||||
// drawerApi.close();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer append-to-main title="基础抽屉示例" title-tooltip="标题提示内容">
|
||||
<template #extra> extra </template>
|
||||
本抽屉指定在内容区域打开
|
||||
</Drawer>
|
||||
</template>
|
@ -8,6 +8,7 @@ import AutoHeightDemo from './auto-height-demo.vue';
|
||||
import BaseDemo from './base-demo.vue';
|
||||
import DynamicDemo from './dynamic-demo.vue';
|
||||
import FormDrawerDemo from './form-drawer-demo.vue';
|
||||
import inContentDemo from './in-content-demo.vue';
|
||||
import SharedDataDemo from './shared-data-demo.vue';
|
||||
|
||||
const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
|
||||
@ -16,6 +17,12 @@ const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
|
||||
// placement: 'left',
|
||||
});
|
||||
|
||||
const [InContentDrawer, inContentDrawerApi] = useVbenDrawer({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: inContentDemo,
|
||||
// placement: 'left',
|
||||
});
|
||||
|
||||
const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: AutoHeightDemo,
|
||||
});
|
||||
@ -37,6 +44,23 @@ function openBaseDrawer(placement: DrawerPlacement = 'right') {
|
||||
baseDrawerApi.open();
|
||||
}
|
||||
|
||||
function openInContentDrawer(placement: DrawerPlacement = 'right') {
|
||||
inContentDrawerApi.setState({ class: '', placement });
|
||||
if (placement === 'top') {
|
||||
// 页面顶部区域的层级只有200,所以设置一个低于200的值,抽屉从顶部滑出来的时候才比较合适
|
||||
inContentDrawerApi.setState({ zIndex: 199 });
|
||||
} else {
|
||||
inContentDrawerApi.setState({ zIndex: undefined });
|
||||
}
|
||||
inContentDrawerApi.open();
|
||||
}
|
||||
|
||||
function openMaxContentDrawer() {
|
||||
// 这里只是用来演示方便。实际上自己使用的时候可以直接将这些配置卸载Drawer的属性里
|
||||
inContentDrawerApi.setState({ class: 'w-full', placement: 'right' });
|
||||
inContentDrawerApi.open();
|
||||
}
|
||||
|
||||
function openAutoHeightDrawer() {
|
||||
autoHeightDrawerApi.open();
|
||||
}
|
||||
@ -69,6 +93,7 @@ function openFormDrawer() {
|
||||
|
||||
<template>
|
||||
<Page
|
||||
auto-content-height
|
||||
description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。"
|
||||
title="抽屉组件示例"
|
||||
>
|
||||
@ -76,6 +101,7 @@ function openFormDrawer() {
|
||||
<DocButton path="/components/common-ui/vben-drawer" />
|
||||
</template>
|
||||
<BaseDrawer />
|
||||
<InContentDrawer />
|
||||
<AutoHeightDrawer />
|
||||
<DynamicDrawer />
|
||||
<SharedDataDrawer />
|
||||
@ -83,18 +109,55 @@ function openFormDrawer() {
|
||||
|
||||
<Card class="mb-4" title="基本使用">
|
||||
<p class="mb-3">一个基础的抽屉示例</p>
|
||||
<Button type="primary" @click="openBaseDrawer('right')">右侧打开</Button>
|
||||
<Button class="ml-2" type="primary" @click="openBaseDrawer('bottom')">
|
||||
<Button class="mb-2" type="primary" @click="openBaseDrawer('right')">
|
||||
右侧打开
|
||||
</Button>
|
||||
<Button
|
||||
class="mb-2 ml-2"
|
||||
type="primary"
|
||||
@click="openBaseDrawer('bottom')"
|
||||
>
|
||||
底部打开
|
||||
</Button>
|
||||
<Button class="ml-2" type="primary" @click="openBaseDrawer('left')">
|
||||
<Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('left')">
|
||||
左侧打开
|
||||
</Button>
|
||||
<Button class="ml-2" type="primary" @click="openBaseDrawer('top')">
|
||||
<Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('top')">
|
||||
顶部打开
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="在内容区域打开">
|
||||
<p class="mb-3">指定抽屉在内容区域打开,不会覆盖顶部和左侧菜单等区域</p>
|
||||
<Button class="mb-2" type="primary" @click="openInContentDrawer('right')">
|
||||
右侧打开
|
||||
</Button>
|
||||
<Button
|
||||
class="mb-2 ml-2"
|
||||
type="primary"
|
||||
@click="openInContentDrawer('bottom')"
|
||||
>
|
||||
底部打开
|
||||
</Button>
|
||||
<Button
|
||||
class="mb-2 ml-2"
|
||||
type="primary"
|
||||
@click="openInContentDrawer('left')"
|
||||
>
|
||||
左侧打开
|
||||
</Button>
|
||||
<Button
|
||||
class="mb-2 ml-2"
|
||||
type="primary"
|
||||
@click="openInContentDrawer('top')"
|
||||
>
|
||||
顶部打开
|
||||
</Button>
|
||||
<Button class="mb-2 ml-2" type="primary" @click="openMaxContentDrawer">
|
||||
内容区域全屏打开
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="内容高度自适应滚动">
|
||||
<p class="mb-3">可根据内容自动计算滚动高度</p>
|
||||
<Button type="primary" @click="openAutoHeightDrawer">打开抽屉</Button>
|
||||
|
@ -1,11 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const isReverseActionButtons = ref(false);
|
||||
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
// 翻转操作按钮的位置
|
||||
actionButtonsReverse: isReverseActionButtons.value,
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
@ -83,6 +89,7 @@ function handleClick(
|
||||
| 'labelWidth'
|
||||
| 'resetDisabled'
|
||||
| 'resetLabelWidth'
|
||||
| 'reverseActionButtons'
|
||||
| 'showAction'
|
||||
| 'showResetButton'
|
||||
| 'showSubmitButton'
|
||||
@ -158,6 +165,11 @@ function handleClick(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reverseActionButtons': {
|
||||
isReverseActionButtons.value = !isReverseActionButtons.value;
|
||||
formApi.setState({ actionButtonsReverse: isReverseActionButtons.value });
|
||||
break;
|
||||
}
|
||||
case 'showAction': {
|
||||
formApi.setState({ showDefaultActions: true });
|
||||
break;
|
||||
@ -177,6 +189,7 @@ function handleClick(
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateResetButton': {
|
||||
formApi.setState({
|
||||
resetButtonOptions: { disabled: true },
|
||||
@ -226,6 +239,9 @@ function handleClick(
|
||||
<Button @click="handleClick('resetLabelWidth')">还原labelWidth</Button>
|
||||
<Button @click="handleClick('disabled')">禁用表单</Button>
|
||||
<Button @click="handleClick('resetDisabled')">解除禁用</Button>
|
||||
<Button @click="handleClick('reverseActionButtons')">
|
||||
翻转操作按钮位置
|
||||
</Button>
|
||||
<Button @click="handleClick('hiddenAction')">隐藏操作按钮</Button>
|
||||
<Button @click="handleClick('showAction')">显示操作按钮</Button>
|
||||
<Button @click="handleClick('hiddenResetButton')">隐藏重置按钮</Button>
|
||||
|
@ -55,13 +55,28 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
placeholder: '请选择',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
|
25
playground/src/views/examples/modal/in-content-demo.vue
Normal file
25
playground/src/views/examples/modal/in-content-demo.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
message.info('onConfirm');
|
||||
// modalApi.close();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal
|
||||
append-to-main
|
||||
class="w-[600px]"
|
||||
title="基础弹窗示例"
|
||||
title-tooltip="标题提示内容"
|
||||
>
|
||||
此弹窗指定在内容区域打开
|
||||
</Modal>
|
||||
</template>
|
@ -9,6 +9,7 @@ import BaseDemo from './base-demo.vue';
|
||||
import DragDemo from './drag-demo.vue';
|
||||
import DynamicDemo from './dynamic-demo.vue';
|
||||
import FormModalDemo from './form-modal-demo.vue';
|
||||
import InContentModalDemo from './in-content-demo.vue';
|
||||
import SharedDataDemo from './shared-data-demo.vue';
|
||||
|
||||
const [BaseModal, baseModalApi] = useVbenModal({
|
||||
@ -16,6 +17,11 @@ const [BaseModal, baseModalApi] = useVbenModal({
|
||||
connectedComponent: BaseDemo,
|
||||
});
|
||||
|
||||
const [InContentModal, inContentModalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: InContentModalDemo,
|
||||
});
|
||||
|
||||
const [AutoHeightModal, autoHeightModalApi] = useVbenModal({
|
||||
connectedComponent: AutoHeightDemo,
|
||||
});
|
||||
@ -40,6 +46,10 @@ function openBaseModal() {
|
||||
baseModalApi.open();
|
||||
}
|
||||
|
||||
function openInContentModal() {
|
||||
inContentModalApi.open();
|
||||
}
|
||||
|
||||
function openAutoHeightModal() {
|
||||
autoHeightModalApi.open();
|
||||
}
|
||||
@ -76,6 +86,7 @@ function openFormModal() {
|
||||
|
||||
<template>
|
||||
<Page
|
||||
auto-content-height
|
||||
description="弹窗组件常用于在不离开当前页面的情况下,显示额外的信息、表单或操作提示,更多api请查看组件文档。"
|
||||
title="弹窗组件示例"
|
||||
>
|
||||
@ -83,6 +94,7 @@ function openFormModal() {
|
||||
<DocButton path="/components/common-ui/vben-modal" />
|
||||
</template>
|
||||
<BaseModal />
|
||||
<InContentModal />
|
||||
<AutoHeightModal />
|
||||
<DragModal />
|
||||
<DynamicModal />
|
||||
@ -93,6 +105,11 @@ function openFormModal() {
|
||||
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="指定容器">
|
||||
<p class="mb-3">在内容区域打开弹窗的示例</p>
|
||||
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="内容高度自适应">
|
||||
<p class="mb-3">可根据内容并自动调整高度</p>
|
||||
<Button type="primary" @click="openAutoHeightModal">打开弹窗</Button>
|
||||
|
@ -13,7 +13,7 @@ packages:
|
||||
- docs
|
||||
- playground
|
||||
catalog:
|
||||
'@ast-grep/napi': ^0.31.0
|
||||
'@ast-grep/napi': ^0.31.1
|
||||
'@changesets/changelog-github': ^0.5.0
|
||||
'@changesets/cli': ^2.27.10
|
||||
'@changesets/git': ^3.0.2
|
||||
@ -23,20 +23,20 @@ catalog:
|
||||
'@ctrl/tinycolor': ^4.1.0
|
||||
'@eslint/js': ^9.16.0
|
||||
'@faker-js/faker': ^9.3.0
|
||||
'@iconify/json': ^2.2.279
|
||||
'@iconify/tailwind': ^1.1.3
|
||||
'@iconify/vue': ^4.1.2
|
||||
'@iconify/json': ^2.2.281
|
||||
'@iconify/tailwind': ^1.2.0
|
||||
'@iconify/vue': ^4.2.0
|
||||
'@intlify/core-base': ^10.0.5
|
||||
'@intlify/unplugin-vue-i18n': ^6.0.0
|
||||
'@intlify/unplugin-vue-i18n': ^6.0.1
|
||||
'@jspm/generator': ^2.4.1
|
||||
'@manypkg/get-packages': ^2.2.2
|
||||
'@nolebase/vitepress-plugin-git-changelog': ^2.11.1
|
||||
'@playwright/test': ^1.49.0
|
||||
'@playwright/test': ^1.49.1
|
||||
'@pnpm/workspace.read-manifest': ^1000.0.0
|
||||
'@stylistic/stylelint-plugin': ^3.1.1
|
||||
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
|
||||
'@tailwindcss/typography': ^0.5.15
|
||||
'@tanstack/vue-query': ^5.62.2
|
||||
'@tanstack/vue-query': ^5.62.7
|
||||
'@tanstack/vue-store': ^0.6.0
|
||||
'@types/archiver': ^6.0.3
|
||||
'@types/eslint': ^9.6.1
|
||||
@ -50,8 +50,8 @@ catalog:
|
||||
'@types/postcss-import': ^14.0.3
|
||||
'@types/qrcode': ^1.5.5
|
||||
'@types/sortablejs': ^1.15.8
|
||||
'@typescript-eslint/eslint-plugin': ^8.17.0
|
||||
'@typescript-eslint/parser': ^8.17.0
|
||||
'@typescript-eslint/eslint-plugin': ^8.18.0
|
||||
'@typescript-eslint/parser': ^8.18.0
|
||||
'@vee-validate/zod': ^4.14.7
|
||||
'@vite-pwa/vitepress': ^0.5.3
|
||||
'@vitejs/plugin-vue': ^5.2.1
|
||||
@ -90,9 +90,9 @@ catalog:
|
||||
eslint-plugin-command: ^0.2.6
|
||||
eslint-plugin-eslint-comments: ^3.2.0
|
||||
eslint-plugin-import-x: ^4.5.0
|
||||
eslint-plugin-jsdoc: ^50.6.0
|
||||
eslint-plugin-jsdoc: ^50.6.1
|
||||
eslint-plugin-jsonc: ^2.18.2
|
||||
eslint-plugin-n: ^17.14.0
|
||||
eslint-plugin-n: ^17.15.0
|
||||
eslint-plugin-no-only-tests: ^3.3.0
|
||||
eslint-plugin-perfectionist: ^3.9.1
|
||||
eslint-plugin-prettier: ^5.2.1
|
||||
@ -101,7 +101,7 @@ catalog:
|
||||
eslint-plugin-unused-imports: ^4.1.4
|
||||
eslint-plugin-vitest: ^0.5.4
|
||||
eslint-plugin-vue: ^9.32.0
|
||||
execa: ^9.5.1
|
||||
execa: ^9.5.2
|
||||
find-up: ^7.0.0
|
||||
get-port: ^7.1.0
|
||||
globals: ^15.13.0
|
||||
@ -112,7 +112,7 @@ catalog:
|
||||
is-ci: ^3.0.1
|
||||
jsonc-eslint-parser: ^2.4.0
|
||||
jsonwebtoken: ^9.0.2
|
||||
lint-staged: ^15.2.10
|
||||
lint-staged: ^15.2.11
|
||||
lodash.clonedeep: ^4.5.0
|
||||
lodash.get: ^4.4.2
|
||||
lodash.isequal: ^4.5.0
|
||||
@ -125,7 +125,7 @@ catalog:
|
||||
pinia: 2.2.2
|
||||
pinia-plugin-persistedstate: ^4.1.3
|
||||
pkg-types: ^1.2.1
|
||||
playwright: ^1.49.0
|
||||
playwright: ^1.49.1
|
||||
postcss: ^8.4.49
|
||||
postcss-antd-fixes: ^0.2.0
|
||||
postcss-html: ^1.7.0
|
||||
@ -136,10 +136,10 @@ catalog:
|
||||
prettier-plugin-tailwindcss: ^0.6.9
|
||||
publint: ^0.2.12
|
||||
qrcode: ^1.5.4
|
||||
radix-vue: ^1.9.10
|
||||
radix-vue: ^1.9.11
|
||||
resolve.exports: ^2.0.3
|
||||
rimraf: ^6.0.1
|
||||
rollup: ^4.28.0
|
||||
rollup: ^4.28.1
|
||||
rollup-plugin-visualizer: ^5.12.0
|
||||
sass: 1.80.6
|
||||
sortablejs: ^1.15.6
|
||||
@ -161,7 +161,7 @@ catalog:
|
||||
unbuild: ^3.0.0-rc.11
|
||||
unplugin-element-plus: ^0.8.0
|
||||
vee-validate: ^4.14.7
|
||||
vite: ^6.0.2
|
||||
vite: ^6.0.3
|
||||
vite-plugin-compression: ^0.5.1
|
||||
vite-plugin-dts: 4.2.1
|
||||
vite-plugin-html: ^3.2.2
|
||||
@ -179,5 +179,5 @@ catalog:
|
||||
vxe-pc-ui: ^4.3.4
|
||||
vxe-table: 4.9.5
|
||||
watermark-js-plus: ^1.5.7
|
||||
zod: ^3.23.8
|
||||
zod: ^3.24.1
|
||||
zod-defaults: ^0.1.3
|
||||
|
Loading…
Reference in New Issue
Block a user