在开发过程中,为了满足业务场景、增加灵活性,前端自定义请求不可避免。下面将会从——需不需要自定义请求,不同场景下怎么自定义请求的维度,提供渐进式的自定义请求介绍。
1. 什么场景不需要自定义请求
自定义请求的背后是获取数据,如果数据已经拿到了,就不需要自定义请求了。
首先,Oinone 提供了前端组件的默认实现。所以生成默认页面的时候,请求数据都是通的,可以看到表格、表单、表单里的字段等组件数据都是能回填的。
这意味着,当我们简单替换组件,不需要特殊获取数据的时候,如果需要拿值,不需要自定义请求,直接从props
里接就好,例如:
- 自定义表格
这里继承平台的表格组件,就有了平台表格自动获取数据的能力
import { BaseElementWidget, SPI, TABLE_WIDGET, TableWidget, ViewType } from '@kunlun/dependencies';
import Test from './Test.vue';
@SPI.ClassFactory(
BaseElementWidget.Token({
viewType: ViewType.Table,
widget: ['table', TABLE_WIDGET]
})
)
export class TestWidget extends TableWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(Test);
return this;
}
}
在 vue
中用 props
接一下 dataSource
,就能获取数据
<template>
<div class="Test">
{{ dataSource }}
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Test',
props: ['dataSource']
});
</script>
效果如下:
- 自定义表单
这里继承平台的表单组件,就有了平台表单自动获取数据的能力
import { BaseElementWidget, SPI, FORM_WIDGET, FormWidget, ViewType } from '@kunlun/dependencies';
import Test from './Test.vue';
@SPI.ClassFactory(
BaseElementWidget.Token({
viewType: ViewType.Form,
widget: ['form', FORM_WIDGET]
})
)
export class TestWidget extends FormWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(Test);
return this;
}
}
在 vue
中用 props
接一下 formData
,就能获取数据
<template>
<div class="Test">
{{ formData }}
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Test',
props: ['formData']
});
</script>
效果如下:
- 自定义字段
这里以单行文本字段为例,继承平台的单行文本字段组件,字段的数据实际上是从表单或表格里拿的,可以通过value
快捷地拿到字段值。如果是关系型字段,想拿到选项数据而不是字段值,可以参考方法3、方法4。
import { SPI, ViewType, FormStringFieldSingleWidget, BaseFieldWidget, ModelFieldType } from '@kunlun/dependencies';
import Test from './Test.vue';
@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: [ViewType.Form, ViewType.Search],
ttype: ModelFieldType.String
})
)
export class TestWidget extends FormStringFieldSingleWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(Test);
return this;
}
}
在 vue
中用 props
接一下 value
,就能获取数据
<template>
<div class="Test" style="color: red">
{{ value }}
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Test',
props: ['value']
});
</script>
效果如下:
- 其他自定义
例如自定义详情、画廊、动作、搜索等等的组件,获取数据等方式都一样,在vue
接activeRecords、formData 、dataSource、value
其中的一个,就能拿到数据。
2. 手写 GraphQL 请求的场景
当默认拿数据的方法满足不了我们,同时又满足请求不需要复用,想要精确控制请求体大小的情况,我们可以采取手写 GraphQL
当方式。
例如,我要重写顶部 mask
中的用户组件,展示用户信息。这个请求就只需请求一次,而且不需要复用,就很适合手写 GraphQL
。
这里继承平台的用户组件,然后手动发起请求。但是 GraphQL
语句怎么拼呢?可以去默认页面,打开浏览器控制台,找到相应的请求,把 GraphQL
语句复制出来,这里复制下默认的用户请求。
http.query
参数的构造、相应结果的获取都能从请求中得到。可以看到我这里精简了请求,只取了用户名。
import { SPI, UserWidget, MaskWidget, Widget, http } from '@kunlun/dependencies';
import Test from './Test.vue';
@SPI.ClassFactory(MaskWidget.Token({ widget: 'user' }))
export class TestWidget extends UserWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(Test);
return this;
}
// 添加响应式注解,这样能在 vue 中接受到 ts 中的变量
@Widget.Reactive()
public testUserInfo: { pamirsUser: { name: string } } | undefined;
public async queryUser() {
const query = `
{
topBarUserBlockQuery {
construct(data: {}) {
pamirsUser {
name
}
}
}
}
`;
const result = await http.query('user', query);
this.testUserInfo = result.data['topBarUserBlockQuery']['construct'] as { pamirsUser: { name: string } };
}
public mounted() {
this.queryUser();
}
}
<template>
<div class="Test">
{{ testUserInfo }}
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Test',
props: ['testUserInfo']
});
</script>
效果如下:
3. 调用平台 api,实现通用的自定义请求组件
手写 GraphQL
的方式,写得代码又多,又不通用,一旦换一个模型,请求体又得重新改造,也很容易出错。调用平台封装的请求 api
可以完美解决这些问题。这种方法适合对于平台基类还不熟悉的情况,我们不清楚基类有没有提供对应的能力,所以把请求相关的功能全量自定义了,如果对于平台已经很熟悉了,可以参考方法4。
平台请求相关的api用法详见 https://doc.oinone.top/frontend/17638.html
这里以实现通用的弹窗添加的多对一组件为例,介绍下 customQueryPage
的用法,其它 api
也是类似的。当我们给一个多对一字段添加数据时,可能不是下拉框的交互,而是打开弹窗,点击数据并提交。平台并没有提供这样的组件,那么这个字段就需要自定义,打开弹窗,并且请求弹窗里的数据。
import {
SPI,
Widget,
FormFieldWidget,
ActiveRecords,
ModelFieldType,
RuntimeRelationField,
ViewType,
buildSelectSearchCondition,
customQueryPage,
IModelField
} from '@kunlun/dependencies';
import Test from './Test.vue';
@SPI.ClassFactory(
FormFieldWidget.Token({
viewType: [ViewType.Form, ViewType.Search],
ttype: [ModelFieldType.ManyToOne]
})
)
export class TestWidget extends FormFieldWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(Test);
return this;
}
// 弹窗表格所属的模型
public get searchDialogModel() {
// 关系字段关联的模型
return (this.field as RuntimeRelationField)?.references;
}
// 弹窗表格展示的字段
public get searchDialogModelFields() {
// 关系字段关联的字段
return (this.field as RuntimeRelationField)?.referencesModel.modelFields;
}
// 转化成 antd table 的 columns 能展示的结构
@Widget.Reactive()
public get columns() {
return (
this.searchDialogModelFields?.map((field: any) => {
return {
key: field.data,
dataIndex: field.data,
title: field.label
};
}) || []
);
}
// 弹窗输入框搜索的字段编码,逗号分隔
@Widget.Reactive()
public get searchFieldCode() {
return this.getDsl().searchFieldCode || 'name';
}
// 弹窗表格数据查询方法名
public get queryPageFunction() {
return (
// 界面设计器配置的查询方法名
this.getDsl().queryPageFunction ||
// 默认查询方法名
'queryPage'
);
}
// 弹窗表格总页数
@Widget.Reactive()
protected totalPages = 10000;
// 弹窗表格数据
@Widget.Reactive()
public searchDialogData: ActiveRecords | undefined;
// 发起查询弹窗表格数据
@Widget.Method()
public async querySearchDialogData(currentPage: number, pageSize: number, searchValue: string) {
if (this.searchDialogModel) {
const condition = buildSelectSearchCondition(
(this.field as RuntimeRelationField).referencesModel,
this.searchFieldCode,
searchValue
);
const result = await customQueryPage(
this.searchDialogModel,
this.queryPageFunction,
{
currentPage,
pageSize,
condition
},
this.searchDialogModelFields as unknown as IModelField[],
this.searchDialogModelFields as unknown as IModelField[]
);
this.totalPages = result.totalPages;
this.searchDialogData = result.content;
}
}
}
<template>
<div class="test-filed-wrapper">
{{ value }}
<a-button class="oio-button" @click="opendialog"> 打开弹窗 </a-button>
<a-modal wrap-class-name="test-dialog" v-model:visible="data.dialogTableVisible" :title="data.title" width="100%">
<a-input-search
v-model:value="data.input3"
placeholder="请输入"
@search="inputSearchButtonClick"
style="width: 20%"
/>
<a-table :dataSource="searchDialogData" :columns="columns" :pagination="false" bordered :customRow="customRow" />
<oio-pagination
v-model:current-page="data.currentPage4"
v-model:page-size="data.pageSize4"
:total="totalPages"
@change="handleChange"
/>
</a-modal>
</div>
</template>
<script lang="ts">
import { OioPagination } from '@kunlun/vue-ui-antd';
import { defineComponent, reactive } from 'vue';
export default defineComponent({
inheritAttrs: false,
name: 'Test',
components: {
OioPagination
},
props: ['value', 'searchDialogData', 'columns', 'totalPages', 'querySearchDialogData', 'change'],
setup(props) {
const data = reactive({
dialogTableVisible: false,
input3: '',
title: '名称',
currentPage4: 1,
pageSize4: 15
});
const customRow = (record: any, index: number) => {
return {
onclick: (event: Event) => {
data.dialogTableVisible = false;
console.log(record, index);
props.change?.(record);
}
};
};
const opendialog = () => {
if (!props.searchDialogData) {
props.querySearchDialogData?.(data.currentPage4, data.pageSize4, data.input3);
}
data.dialogTableVisible = true;
};
const handleChange = (currentPage: number, pageSize: number) => {
props.querySearchDialogData?.(data.currentPage4, data.pageSize4, data.input3);
};
const inputSearchButtonClick = () => {
props.querySearchDialogData?.(data.currentPage4, data.pageSize4, data.input3);
};
return {
data,
opendialog,
handleChange,
customRow,
inputSearchButtonClick
};
}
});
</script>
<style lang="scss">
.test-filed-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
.test-dialog {
.ant-modal-body {
padding: 20px;
display: flex;
flex-direction: column;
row-gap: 16px;
}
}
</style>
可以看到资源应用 -> 省菜单 -> 创建表单 -> 国家/地区 字段被替换了,效果如下:
4. 调用基类方法,实现通用的自定义请求组件
这种方法适用于对于平台组件很熟悉的情况,知道什么基类提供了对应的能力,并继承它,重写几个参数,调用基类的方法就好。
同样以实现通用的弹窗添加的多对一组件为例,方法3继承的是 FormFieldWidget
,把分页、搜索查询、都重写了一遍。其实没必要这么麻烦,我们可以抽象一下,弹窗打开选数据和下拉打开选数据,实际上只有交互上的区别,而没有数据请求上的区别,所以我们完全可以继承平台默认的多对一下拉 FormM2OSelectFieldWidget
,所有的请求、分页都已经做好了,只需要调一下api拿到就行。
import { SPI, Widget, FormFieldWidget, ModelFieldType, ViewType, FormM2OSelectFieldWidget } from '@kunlun/dependencies';
import Test from './Test.vue';
@SPI.ClassFactory(
FormFieldWidget.Token({
viewType: [ViewType.Form, ViewType.Search],
ttype: [ModelFieldType.ManyToOne]
})
)
export class TestWidget extends FormM2OSelectFieldWidget {
public initialize(props) {
super.initialize(props);
this.setComponent(Test);
return this;
}
// 转化成 antd table 的 columns 能展示的结构
@Widget.Reactive()
public get columns() {
return (
this.field.referencesModel.modelFields?.map((field) => {
return {
key: field.data,
dataIndex: field.data,
title: field.label
};
}) || []
);
}
// 弹窗表格总页数,这里重写为响应式的
@Widget.Reactive()
protected totalPages = 10000;
// 弹窗表格数据,这里重写为响应式的
@Widget.Reactive()
protected dataList: Record<string, unknown>[] = [];
// 发起查询弹窗表格数据
@Widget.Method()
public async querySearchDialogData(currentPage: number, pageSize: number, searchValue: string) {
this.currentPage = currentPage;
this.pageSize = pageSize;
this.searchValue = searchValue;
// 只需调用基类的加载数据方法
await this.initLoadOptions();
}
}
<template>
<div class="test-filed-wrapper">
{{ value }}
<a-button class="oio-button" @click="opendialog"> 打开弹窗 </a-button>
<a-modal wrap-class-name="test-dialog" v-model:visible="data.dialogTableVisible" :title="data.title" width="100%">
<a-input-search
v-model:value="data.input"
placeholder="请输入"
@search="inputSearchButtonClick"
style="width: 20%"
/>
<a-table :dataSource="dataList" :columns="columns" :pagination="false" bordered :customRow="customRow" />
<oio-pagination
v-model:current-page="data.currentPage"
v-model:page-size="data.pageSize"
:total="totalPages"
@change="handleChange"
/>
</a-modal>
</div>
</template>
<script lang="ts">
import { OioPagination } from '@kunlun/vue-ui-antd';
import { defineComponent, reactive } from 'vue';
export default defineComponent({
inheritAttrs: false,
name: 'Test',
components: {
OioPagination
},
props: ['value', 'dataList', 'columns', 'totalPages', 'querySearchDialogData', 'change'],
setup(props) {
const data = reactive({
dialogTableVisible: false,
input: '',
title: '名称',
currentPage: 1,
pageSize: 15
});
const customRow = (record: any, index: number) => {
return {
key: record.id || index,
onClick: (event: Event) => {
data.dialogTableVisible = false;
console.log(record, index);
props.change?.(record);
}
};
};
const opendialog = () => {
if (!props.dataList || !props.dataList.length) {
props.querySearchDialogData?.(data.currentPage, data.pageSize, data.input);
}
data.dialogTableVisible = true;
};
const handleChange = (currentPage: number, pageSize: number) => {
props.querySearchDialogData?.(data.currentPage, data.pageSize, data.input);
};
const inputSearchButtonClick = () => {
data.currentPage = 1;
props.querySearchDialogData?.(data.currentPage, data.pageSize, data.input);
};
return {
data,
opendialog,
handleChange,
customRow,
inputSearchButtonClick
};
}
});
</script>
<style lang="scss">
.test-filed-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
.test-dialog {
.ant-modal-body {
padding: 20px;
display: flex;
flex-direction: column;
row-gap: 16px;
}
}
</style>
可以看到不仅代码逻辑变少了,还拥有了更多的能力,例如弹窗表格数据和别的字段联动。
再次去资源应用 -> 省菜单 -> 创建表单 -> 国家/地区 字段,替换效果和方法3一致:
Oinone社区 作者:银时原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/20940.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验