This commit is contained in:
dap 2025-03-29 22:53:47 +08:00
commit 467d337515
13 changed files with 193 additions and 27 deletions

View File

@ -323,6 +323,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| schema | 表单项的每一项配置 | `FormSchema[]` | - |
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
::: tip fieldMappingTime
@ -365,13 +366,6 @@ export interface FormCommonConfig {
* 所有表单项的props
*/
componentProps?: ComponentProps;
/**
* 是否紧凑模式(移除表单底部为显示校验错误信息所预留的空间)。
* 在有设置校验规则的场景下建议不要将其设置为true
* 默认为false。但用作表格的搜索表单时默认为true
* @default false
*/
compact?: boolean;
/**
* 所有表单项的控件样式
*/

View File

@ -110,6 +110,36 @@ VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
项目中默认自带了基于 `axios` 封装的基础的请求配置,核心由 `@vben/request` 包提供。项目没有过多的封装只是简单的封装了一些常用的配置如有其他需求可以自行增加或者调整配置。针对不同的app可能是用到了不同的组件库以及`store`,所以在应用目录下的`src/api/request.ts`文件夹下,有对应的请求配置文件,如`web-antd`项目下的`src/api/request.ts`文件,可以根据自己的需求进行配置。
### 扩展的配置
除了基础的Axios配置外扩展了部分配置。
```ts
type ExtendOptions<T = any> = {
/**
* 参数序列化方式。预置了几种针对数组的序列化类型
* - brackets: ids[]=1&ids[]=2&ids[]=3
* - comma: ids=1,2,3
* - indices: ids[0]=1&ids[1]=2&ids[2]=3
* - repeat: ids=1&ids=2&ids=3
* @default 'brackets'
*/
paramsSerializer?:
| 'brackets'
| 'comma'
| 'indices'
| 'repeat'
| AxiosRequestConfig<T>['paramsSerializer'];
/**
* 响应数据的返回方式。
* - raw: 原始的AxiosResponse包括headers、status等不做是否成功请求的检查。
* - body: 返回响应数据的BODY部分只会根据status检查请求是否成功忽略对code的判断这种情况下应由调用方检查请求是否成功
* - data: 解构响应的BODY数据只返回其中的data节点数据会检查status和code是否为成功状态
*/
responseReturn?: 'body' | 'data' | 'raw';
};
```
### 请求示例
#### GET 请求

View File

@ -210,7 +210,7 @@ defineExpose({
v-slot="{ flattenItems }"
:class="
cn(
'text-blackA11 select-none list-none rounded-lg p-2 text-sm font-medium',
'text-blackA11 container select-none list-none rounded-lg p-2 text-sm font-medium',
$attrs.class as unknown as ClassType,
bordered ? 'border' : '',
)
@ -219,11 +219,7 @@ defineExpose({
<div class="w-full" v-if="$slots.header">
<slot name="header"> </slot>
</div>
<TransitionGroup
:name="transition ? 'fade' : ''"
mode="out-in"
class="container"
>
<TransitionGroup :name="transition ? 'fade' : ''">
<TreeItem
v-for="item in flattenItems"
v-slot="{

View File

@ -31,6 +31,9 @@ async function generateAccessible(
const root = router.getRoutes().find((item) => item.path === '/');
// 获取已有的路由名称列表
const names = root?.children?.map((item) => item.name) ?? [];
// 动态添加到router实例内
accessibleRoutes.forEach((route) => {
/**
@ -46,7 +49,10 @@ async function generateAccessible(
if (route.children && route.children.length > 0) {
delete route.component;
}
root.children?.push(route);
// 根据router name判断如果路由已经存在则不再添加
if (!names?.includes(route.name)) {
root.children?.push(route);
}
} else {
router.addRoute(route);
}

View File

@ -23,10 +23,10 @@
"@vben/locales": "workspace:*",
"@vben/utils": "workspace:*",
"axios": "catalog:",
"qs": "^6.13.1"
"qs": "catalog:"
},
"devDependencies": {
"@types/qs": "^6.9.17",
"@types/qs": "catalog:",
"axios-mock-adapter": "catalog:"
}
}

View File

@ -2,14 +2,39 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestClientConfig, RequestClientOptions } from './types';
import { bindMethods, merge } from '@vben/utils';
import { bindMethods, isString, merge } from '@vben/utils';
import axios from 'axios';
import qs from 'qs';
import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor';
import { FileUploader } from './modules/uploader';
function getParamsSerializer(
paramsSerializer: RequestClientOptions['paramsSerializer'],
) {
if (isString(paramsSerializer)) {
switch (paramsSerializer) {
case 'brackets': {
return (params: any) =>
qs.stringify(params, { arrayFormat: 'brackets' });
}
case 'comma': {
return (params: any) => qs.stringify(params, { arrayFormat: 'comma' });
}
case 'indices': {
return (params: any) =>
qs.stringify(params, { arrayFormat: 'indices' });
}
case 'repeat': {
return (params: any) => qs.stringify(params, { arrayFormat: 'repeat' });
}
}
}
return paramsSerializer;
}
class RequestClient {
public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
@ -39,6 +64,9 @@ class RequestClient {
};
const { ...axiosConfig } = options;
const requestConfig = merge(axiosConfig, defaultConfig);
requestConfig.paramsSerializer = getParamsSerializer(
requestConfig.paramsSerializer,
);
this.instance = axios.create(requestConfig);
bindMethods(this);
@ -154,6 +182,9 @@ class RequestClient {
const response: AxiosResponse<T> = await this.instance({
url,
...config,
...(config.paramsSerializer
? { paramsSerializer: getParamsSerializer(config.paramsSerializer) }
: {}),
});
return response as T;
} catch (error: any) {

View File

@ -5,15 +5,29 @@ import type {
InternalAxiosRequestConfig,
} from 'axios';
type ExtendOptions = {
/**
* raw: 原始的AxiosResponseheadersstatus等
* body: 返回响应数据的BODY部分status检查请求是否成功code的判断
* data: 解构响应的BODY数据data节点数据status和code是否为成功状态
type ExtendOptions<T = any> = {
/**
*
* - brackets: ids[]=1&ids[]=2&ids[]=3
* - comma: ids=1,2,3
* - indices: ids[0]=1&ids[1]=2&ids[2]=3
* - repeat: ids=1&ids=2&ids=3
*/
paramsSerializer?:
| 'brackets'
| 'comma'
| 'indices'
| 'repeat'
| AxiosRequestConfig<T>['paramsSerializer'];
/**
*
* - raw: 原始的AxiosResponseheadersstatus等
* - body: 返回响应数据的BODY部分status检查请求是否成功code的判断
* - data: 解构响应的BODY数据data节点数据status和code是否为成功状态
*/
responseReturn?: 'body' | 'data' | 'raw';
};
type RequestClientConfig<T = any> = AxiosRequestConfig<T> & ExtendOptions;
type RequestClientConfig<T = any> = AxiosRequestConfig<T> & ExtendOptions<T>;
type RequestResponse<T = any> = AxiosResponse<T> & {
config: RequestClientConfig<T>;

View File

@ -1,5 +1,5 @@
export * from './iconify/index.js';
export * from './iconify-offline/index.js';
export * from './iconify';
export * from './iconify-offline';
export { default as EmptyIcon } from './icons/empty-icon.vue';
export * from './svg/index.js';
export * from './svg';
export { VbenIcon } from '@vben-core/shadcn-ui';

View File

@ -0,0 +1,19 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
async function getParamsData(
params: Recordable<any>,
type: 'brackets' | 'comma' | 'indices' | 'repeat',
) {
return requestClient.get('/status', {
params,
paramsSerializer: type,
responseReturn: 'raw',
});
}
export { getParamsData };

View File

@ -50,7 +50,8 @@
"clipboard": "剪贴板",
"menuWithQuery": "带参菜单",
"openInNewWindow": "新窗口打开",
"fileDownload": "文件下载"
"fileDownload": "文件下载",
"requestParamsSerializer": "参数序列化"
},
"breadcrumb": {
"navigation": "面包屑导航",

View File

@ -243,6 +243,18 @@ const routes: RouteRecordRaw[] = [
title: 'Tanstack Query',
},
},
{
name: 'RequestParamsSerializerDemo',
path: '/demos/features/request-params-serializer',
component: () =>
import(
'#/views/demos/features/request-params-serializer/index.vue'
),
meta: {
icon: 'lucide:git-pull-request-arrow',
title: $t('demos.features.requestParamsSerializer'),
},
},
],
},
// 面包屑导航

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { computed, ref, watchEffect } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Radio, RadioGroup } from 'ant-design-vue';
import { getParamsData } from '#/api/examples/params';
const params = { ids: [2512, 3241, 4255] };
const paramsSerializer = ref<'brackets' | 'comma' | 'indices' | 'repeat'>(
'brackets',
);
const response = ref('');
const paramsStr = computed(() => {
// URL
const url = response.value;
return new URL(url).searchParams.toString();
});
watchEffect(() => {
getParamsData(params, paramsSerializer.value).then((res) => {
response.value = res.request.responseURL;
});
});
</script>
<template>
<Page
title="请求参数序列化"
description="不同的后台接口可能对数组类型的GET参数的解析方式不同我们预置了几种数组序列化方式通过配置 paramsSerializer 来实现不同的序列化方式"
>
<Card>
<RadioGroup v-model:value="paramsSerializer" name="paramsSerializer">
<Radio value="brackets">brackets</Radio>
<Radio value="comma">comma</Radio>
<Radio value="indices">indices</Radio>
<Radio value="repeat">repeat</Radio>
</RadioGroup>
<div class="mt-4 flex flex-col gap-4">
<div>
<h3>需要提交的参数</h3>
<div>{{ JSON.stringify(params, null, 2) }}</div>
</div>
<template v-if="response">
<div>
<h3>访问地址</h3>
<pre>{{ response }}</pre>
</div>
<div>
<h3>参数字符串</h3>
<pre>{{ paramsStr }}</pre>
</div>
<div>
<h3>参数解码</h3>
<pre>{{ decodeURIComponent(paramsStr) }}</pre>
</div>
</template>
</div>
</Card>
</Page>
</template>

View File

@ -50,6 +50,7 @@ catalog:
'@types/nprogress': ^0.2.3
'@types/postcss-import': ^14.0.3
'@types/qrcode': ^1.5.5
'@types/qs': ^6.9.18
'@types/sortablejs': ^1.15.8
'@typescript-eslint/eslint-plugin': ^8.26.0
'@typescript-eslint/parser': ^8.26.0
@ -139,6 +140,7 @@ catalog:
prettier-plugin-tailwindcss: ^0.6.11
publint: ^0.2.12
qrcode: ^1.5.4
qs: ^6.14.0
radix-vue: ^1.9.17
resolve.exports: ^2.0.3
rimraf: ^6.0.1