This commit is contained in:
dap
2024-12-13 08:36:45 +08:00
57 changed files with 1240 additions and 135 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()"
/>

View File

@@ -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 }) => {

View File

@@ -12,6 +12,7 @@ setupVbenForm<ComponentType>({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
},
},
defineRules: {

View File

@@ -1,6 +1,7 @@
{
"title": "Demos",
"elementPlus": "Element Plus",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",

View File

@@ -1,6 +1,7 @@
{
"title": "演示",
"elementPlus": "Element Plus",
"form": "表单演示",
"vben": {
"title": "项目",
"about": "关于",

View File

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

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

View File

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

View File

@@ -2,6 +2,7 @@
"title": "Demos",
"naive": "Naive UI",
"table": "Table",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",

View File

@@ -2,6 +2,7 @@
"title": "演示",
"naive": "Naive UI",
"table": "Table",
"form": "表单",
"vben": {
"title": "项目",
"about": "关于",

View File

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

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