前端自定义请求入门版

在开发过程中,为了满足业务场景、增加灵活性,前端自定义请求不可避免。下面将会从——自定义 mask、自定义表格(表单等)、自定义字段三个实际场景的角度,介绍自定义请求。这篇文章把请求都写在了 ts 中,这样便于继承重写,如果不习惯 ts 的写法,把请求写在 vue 里也是可以的。

1. 自定义 mask

mask 组件通常会有一个特点:在不同页面不同模型或不同应用下都展示,与业务模型无关,且往往只需要请求一次。同时可能有精确控制请求体大小的需求,这就很适合采取手写 GraphQL 的方式。

例如,我要重写顶部 mask 中的用户组件,展示用户信息。这个请求就只需请求一次,而且不需要复用,就很适合手写 GraphQL

这里继承平台的用户组件,然后在代码中写死 GraphQL 发起请求。但是 GraphQL 语句怎么拼呢?我们可以去默认页面,打开浏览器控制台,找到相应的请求,把 GraphQL 语句复制出来,这里复制下默认的用户请求。
前端自定义请求入门版

http.query 参数的构造、相应结果的获取都能从请求中得到。可以看到我这里精简了请求,只取了用户名。
前端自定义请求入门版

TS

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

VUE

<template>
  <div class="Test">
    {{ testUserInfo }}
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  name: 'Test',
  props: ['testUserInfo']
});
</script>

效果如下:
前端自定义请求入门版

2. 自定义表格(表单)等视图元素组件

2-1. 自定义表格

2-1-1. 自定义表格自动获取数据

Oinone 提供了前端组件的默认实现。所以生成默认页面的时候,请求数据都是通的,可以看到表格、表单、表单里的字段等组件数据都是能回填的。
所以这里继承平台的表格组件,就有了平台表格自动获取数据的能力。

TS

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>

效果如下:
前端自定义请求入门版

2-1-2. 自定义表格重写 fetchData,发起自定义请求

TS

import {
  ActiveRecord,
  BaseElementWidget,
  Condition,
  customQueryPage,
  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;
  }

  /**
   * 获取数据
   */
  public async fetchData(condition?: Condition): Promise<ActiveRecord[]> {
    // load 方法会激活 spin 转圈组件
    return await this.load(async () => {
      const pagination = this.generatorPagination();
      // 生成表格的查询条件,会把搜索里的条件拼上
      // const finalCondition = this.generatorCondition(condition);
      // 这里也可以手拼,模糊匹配名称带 'a' 的记录
      const finalCondition = new Condition('name').like('a');

      /**
       * this.model.model 是模型的编码
       * 'queryPage' 是模型中定义的查询方法名,可以和后端约定,甚至配置。这里使用默认的 queryPage
       * 第三个参数是查询条件,可以携带分页参数等信息
       * 第四个参数是请求字段,可以配置请求字段,默认是所有字段,可以配置成需要的字段,可以减少请求字段,提高性能
       * 第五个参数是返回字段,默认是所有字段
       *  */
      const result = await customQueryPage(
        this.model.model,
        'queryPage',
        {
          pageSize: pagination.pageSize,
          currentPage: pagination.current,
          condition: finalCondition
        },
        // 拿到当时视图中的字段
        this.rootRuntimeContext.getRequestFields(),
        this.rootRuntimeContext.getRequestFields()
      );

      pagination.total = result.totalElements;
      pagination.totalPageSize = result.totalPages;

      // 这里 return 出去的值会赋给 dataSource,同2-1-1,在 vue 的 props 里接一下就能使用
      return result.content;
    });
  }
}

vue 同 2-1-1 中的 vue,用 props 接一下 dataSource,就能获取数据。

2-2. 自定义表单

2-2-1. 自定义表单自动获取数据

这里继承平台的表单组件,就有了平台表单自动获取数据的能力

TS

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>

效果如下:
前端自定义请求入门版

2-2-2. 自定义表单重写 fetchData,发起自定义请求

TS

import {
  BaseElementWidget,
  SPI,
  FORM_WIDGET,
  FormWidget,
  ViewType,
  ActiveRecord,
  Condition,
  queryOne,
  constructOne,
  IModel,
  customQuery
} 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;
  }

  /**
   * 获取数据
   * 这里只考虑单条数据,非内联表单页的情况
   */
  public async fetchData(condition?: Condition): Promise<ActiveRecord> {
    return await this.load(async () => {
      let result;

     // 拿到当前视图的字段
      const requestFields = this.rootRuntimeContext.getRequestFields();
      // 获取 url 中的 id
      const id = this.urlParameters.id;
      // 有 id 根据 id 查数据
      if (id) {
        /**
         * 可以调封装好的 queryOne 方法
         * 传入模型编码,请求参数,请求字段
         *  */
        result = await queryOne(this.model.model, { id }, requestFields);
        // 如果不调 queryOne,也可以自定义传入方法名
        // result = await customQuery(this.model.model, 'xxxMethodName', { id }, requestFields, requestFields);
      }
      // 没 id 初始化构造一条数据
      else {
        result = await constructOne({
          modelModel: this.model.model,
          model: this.model as unknown as IModel,
          record: { name: 'xxx' },
          fields: requestFields,
          variables: {
            anyKey: 'anyValue'
          },
          context: {}
        });
      }

      // 这里 return 出去的值会赋给 formData,同2-2-1,在 vue 的 props 里接一下就能使用
      return result;
    });
  }
}

vue 同 2-2-1 中的 vue,用 props 接一下 formData,就能获取数据。

3. 自定义字段

3-1 自定义普通字段

3-1-1 自定义普通字段自动获取数据

字段的数据默认是依靠表单或表格组件请求的,可以通过 value 快捷地拿到字段值,这里以单行文本字段为例。

TS

import { SPI, ViewType, FormFieldWidget, 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 FormFieldWidget {
  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>

效果如下:
前端自定义请求入门版

3-1-2 自定义普通字段发起请求

我们可能会遇到这么一种场景:当字段的值修改后,需要调用后端的函数,修改表单的值。同样以单行文本字段为例。

TS

import {
  SPI,
  ViewType,
  FormFieldWidget,
  BaseFieldWidget,
  ModelFieldType,
  Widget,
  customQuery,
  ObjectUtils
} from '@kunlun/dependencies';
import Test from './Test.vue';

@SPI.ClassFactory(
  BaseFieldWidget.Token({
    viewType: [ViewType.Form, ViewType.Search],
    ttype: ModelFieldType.String
  })
)
export class TestWidget extends FormFieldWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(Test);
    return this;
  }

  // 请求的方法名,可配置
  public get methodName() {
    return this.getDsl().methodName || 'construct';
  }

  @Widget.Method()
  public async onCalculate() {
    // 拿到表单的数据
    const formData = this.formData || {};
    // 拿到当前视图的字段
    const requestFields = this.rootViewRuntimeContext.runtimeContext.getRequestFields();

    /**
     * 调用自定义方法
     * @param modelModel 模型的编码
     * @param method 方法名称
     * @param record 请求数据,这里把整个表单数据都带上
     * @param requestFields 请求字段,默认是所有字段,可以配置成需要的字段,以减少请求体积,提高性能
     * @param responseFields 第五个参数是返回字段,默认是所有字段
     */
    const result = await customQuery(this.model.model, this.methodName, formData, requestFields, requestFields);

    // 合并返回的数据到表单
    if (result) {
      ObjectUtils.shallowMerge(formData, result as Object);
    }

    // 重新加载表单数据
    this.reloadFormData$.subject.next(true);
  }
}

vue 中用 props 接一下 value,就能获取数据

<template>
  <div class="Test">
    <a-input class="oio-input oio-input-number" v-model:value="realValue" @change="onChange" />
    <a-button class="oio-button" type="primary" @click="onCalculate">请求</a-button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from 'vue';

export default defineComponent({
  name: 'Test',
  props: ['value', 'change', 'onCalculate'],
  setup(props) {
    const realValue = ref<string>(props.value);
    watch(
      () => props.value,
      (newValue) => {
        realValue.value = newValue;
      }
    );

    const onChange = () => {
      props.change?.(realValue.value);
    };

    return {
      realValue,
      onChange
    };
  }
});
</script>

<style lang="scss">
.Test {
  display: flex;
  align-items: center;
  gap: 16px;

  .ant-input {
    flex: auto;
  }
}
</style>

3-2 自定义关系字段

3-2-1 自定义关系字段自动获取数据

字段的数据默认是依靠表单或表格组件请求的,可以通过 value 快捷地拿到字段值,这里以多对一字段为例。

TS

import { SPI, ViewType, FormFieldWidget, BaseFieldWidget, ModelFieldType } from '@kunlun/dependencies';
import Test from './Test.vue';

@SPI.ClassFactory(
  BaseFieldWidget.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;
  }
}

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>

3-2-2 自定义关系字段手写 GraphQL 获取数据

手写 GraphQL 的方式,优点是符合 RESTful 请求的直觉,方便理解,能精确控制请求体大小。但缺点也很明显,写得代码又多,又不通用,一旦换一个模型,请求体又得重新改造,也很容易拼错。

这里以实现通用的弹窗添加的多对一组件为例,介绍手写 GraphQL 的请求方式。当我们给一个多对一字段添加数据时,可能不是下拉框的交互,而是打开弹窗,点击数据并提交。平台并没有提供这样的组件,那么这个字段就需要自定义,打开弹窗,并且请求弹窗里的数据。

TS

import {
  SPI,
  Widget,
  FormFieldWidget,
  ActiveRecords,
  ModelFieldType,
  RuntimeRelationField,
  ViewType,
  http,
  Condition,
  DEFAULT_TRUE_CONDITION
} 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 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) {
    // 根据配置的弹窗输入框搜索的字段编码,构建查询条件
    let condition = new Condition(DEFAULT_TRUE_CONDITION);
    if (searchValue) {
      this.searchFieldCode.split(',').forEach((fieldCode) => {
        // like 模糊匹配
        condition.and(new Condition(fieldCode).like(searchValue));
      });
    }

    // 手拼 gql
    const query = `
      {
        resourceCountryQuery {
          queryPage(
            page: {currentPage: ${currentPage}, size: ${pageSize}}
            queryWrapper: {rsql: "${condition?.toString()}"}
          ) {
            content {
              code
              name
              id
            }
            totalPages
            totalElements
          }
        }
      }
      `;
    const r = await http.query('resource', query);
    const result = r.data['resourceCountryQuery']['queryPage'];

    this.totalPages = result.totalPages as number;
    this.searchDialogData = result.content as ActiveRecords;
  }
}

VUE

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

3-2-3 自定义关系字段调平台 api 获取数据

调用平台封装的请求 api 可以解决手写 GraphQL 带来的问题。这种方法适合对于平台基类还不熟悉的情况,我们不清楚基类有没有提供对应的能力,所以把请求相关的功能全量自定义了。如果对于平台已经很熟悉了,可以参考 3-2-4。

平台请求相关的api用法详见 https://doc.oinone.top/frontend/17638.html

这里同样以实现通用的弹窗添加的多对一组件为例,介绍下 customQueryPage 的用法,其它 api也是类似的。

TS

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
      );
      // 这样把模型和方法写死,效果就相当于手写 GraphQL,不能通用了
      // const result = await customQueryPage(
      //   "resource.ResourceCountry",
      //   "queryPage",
      //   {
      //     currentPage,
      //     pageSize,
      //     condition
      //   },
      //   this.searchDialogModelFields as unknown as IModelField[],
      //   this.searchDialogModelFields as unknown as IModelField[]
      // );
      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;
    }
  }
}

vue 同 3-2-2 的 vue

可以看到资源应用 -> 省菜单 -> 创建表单 -> 国家/地区 字段被替换了,效果如下:

3-2-4 自定义关系字段调基类方法获取数据

这种方法适用于对于平台组件很熟悉的情况,知道什么基类提供了对应的能力,并继承它,重写几个参数,调用基类的方法就好。

同样以实现通用的弹窗添加的多对一组件为例,方法 3-2-3 继承的是 FormFieldWidget,把分页、搜索查询、都重写了一遍。其实没必要这么麻烦,我们可以抽象一下,弹窗打开选数据和下拉打开选数据,实际上只有交互上的区别,而没有数据请求上的区别,所以我们完全可以继承平台默认的多对一下拉 FormM2OSelectFieldWidget ,所有的请求、分页都已经做好了,只需要调一下api拿到就行。

TS

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

VUE

<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低代码应用平台体验

Like (0)
银时's avatar银时数式员工
Previous 2025年3月31日 pm4:05
Next 2025年4月17日 pm8:52

相关推荐

  • 表单字段API

    FormFieldWidget 表单字段的基类,包含了表单字段通用的属性跟方法 示例 class MyFieldClass extends FormFieldWidget{ } 字段属性 属性名 说明 类型 可选值 默认值 value 当前字段的值 any – null formData 当前表单视图的数据 Object – {} rootData 跟视图的数据,如果当前只有一个视图,那么与formData是一样的 Array – [] metadataRuntimeContext 当前视图运行时的上下文,可以获取当前模型、字段、动作、视图等所有的数据 Object – – urlParameters 当前url参数 Object – – field 当前字段的元数据 Object – – model 当前模型 Object – – view 当前视图 Object – – disabled 是否禁用 Boolean – false invisible 当前字段是否不可见 Boolean – false required 当前字段是否必填,如果当前字段是在详情页,那么是false Boolean – false readonly 当前字段是否只读,如果当前字段是在详情页、搜索,那么是false Boolean – false placeholder 占位符 String – 当前字段的displayName label 字段的标题 String – 当前字段的displayName 方法 方法名 说明 参数 例子 getDsl 获取当前字段所有的配置 – change 修改当前字段的值 any focus 获取焦点触发的方法 – blur 失去焦点触发的方法 – executeValidator 执行当前字段的校验,异步的 – submit 重写当前字段的提交逻辑 – submit() { return ‘value’ } reloadActiveRecords 替换当前视图的数据 Array this.reloadActiveRecords([{code: xxx, name: 111}]) reloadRootData 替换根视图的数据 Array this.reloadRootData([{code: xxx, name: 111}])

    2023年11月15日
    2.2K00
  • oio-grid 栅格

    24 栅格系统。 <oio-row :gutter="24"> <oio-col :span="12"></oio-col> <oio-col :span="12"></oio-col> </oio-row> 概述 布局的栅格化系统,我们是基于行(row)和列(col)来定义信息区块的外部框架,以保证页面的每个区域能够稳健地排布起来。下面简单介绍一下它的工作原理: 通过\row\在水平方向建立一组\column\(简写 col) 你的内容应当放置于\col\内,并且,只有\col\可以作为\row\的直接元素 栅格系统中的列是指 1 到 24 的值来表示其跨越的范围。例如,三个等宽的列可以使用 \<a-col :span="8" />\ 来创建 如果一个\row\中的\col\总和超过 24,那么多余的\col\会作为一个整体另起一行排列 Flex 布局 我们的栅格化系统支持 Flex 布局,允许子元素在父节点内的水平对齐方式 – 居左、居中、居右、等宽排列、分散排列。子元素与子元素之间,支持顶部对齐、垂直居中对齐、底部对齐的方式。同时,支持使用 order 来定义元素的排列顺序。 Flex 布局是基于 24 栅格来定义每一个『盒子』的宽度,但不拘泥于栅格。 API Row 成员 说明 类型 默认值 align flex 布局下的垂直对齐方式:top middle bottom string top gutter 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 [水平间距, 垂直间距](1.5.0 后支持)。 number/object/array 0 justify flex 布局下的水平排列方式:start end center space-around space-between string start wrap 是否自动换行 boolean false Col 成员 说明 类型 默认值 版本 flex flex 布局填充 string|number – offset 栅格左侧的间隔格数,间隔内不可以有栅格 number 0 order 栅格顺序,flex 布局模式下有效 number 0 pull 栅格向左移动格数 number 0 push 栅格向右移动格数 number 0 span 栅格占位格数,为 0 时相当于 display: none number – xxxl ≥2000px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xs <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – sm ≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – md ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – lg ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xl ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xxl ≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object –

    2023年12月18日
    1.2K00
  • 前端自定义组件之单页面步骤条

    本文将讲解如何通过自定义,实现单页面的步骤条组件。其中每个步骤的元素里都是界面设计器拖出来的。 实现路径 整体的实现思路是界面设计器拖个选项卡组件,自定义这个选项卡,里面的每个选项页都当成一步渲染出来,每一步的名称是选项页的标题。 1. 界面设计器拖出页面 我们界面设计器拖个选项卡组件,然后在每个选项页里拖拽任意元素。完成后点击右上角九宫格,选中选项卡,填入组件 api 名称,作用是把选项卡切换成我们自定义的步骤条组件,这里的 api 名称和自定义组件的 widget 对应。最后发布页面,并绑定菜单。 2. 组件实现 widget 组件重写了选项卡,核心函数 renderStep,通过 DslRender.render 方法渲染界面设计器拖拽的元素,每一步的 step 又是解析选卡页得到的。 import { SPI, Widget, DefaultTabsWidget, BasePackWidget, DslDefinition, DslRender, DslDefinitionType, CallChaining, customMutation } from '@oinone/kunlun-dependencies'; import { VNode } from 'vue'; import NextStepSinglePage from './NextStepSinglePage.vue'; @SPI.ClassFactory(BasePackWidget.Token({ widget: 'NextStepSinglePage' })) export class NextStepSinglePageWidget extends DefaultTabsWidget { public initialize(props) { super.initialize(props); this.setComponent(NextStepSinglePage); return this; } @Widget.Reactive() public get invisible() { return false; } // 配置的每一步名称,解析选项页的标题 @Widget.Reactive() public get titles() { return this.template?.widgets?.map((item) => item.title) || []; } // region 上一步下一步配置 // 步骤数组,数组里的元素即步骤要渲染的内容 @Widget.Reactive() public get steps(): DslDefinition[] { // 每个 tab 是一个步骤,这里会有多个步骤 // 每个步骤里有多个元素,又是数组 // 所以这里是二维数组 const tabDsls: DslDefinition[][] = this.template?.widgets.map((item) => item.widgets) || []; // 对每个步骤的子元素们,外侧包一层 row 布局,所以变回了一维数组 return tabDsls.map((tabDsl) => { return { …(this.template || {}), dslNodeType: DslDefinitionType.PACK, widgets: tabDsl, widget: 'row', resolveOptions: { mode: 1 } }; }); } // 渲染步骤,每个步骤有多个子元素 @Widget.Method() public renderStep(step: DslDefinition): VNode | undefined { return DslRender.render(step); } // region 校验相关 // 校验的钩子 @Widget.Reactive() @Widget.Inject('validatorCallChaining') protected parentValidatorCallChaining: CallChaining<boolean> | undefined; // 校验步骤表单 @Widget.Method() public async onValidator(): Promise<boolean> { const res = await this.parentValidatorCallChaining?.syncCall(); return res…

    2025年7月8日
    98000
  • TableWidget 与 FormWidget 浅析

    前言:本文主要聚集于 TableWidget和 FormWidget,对两者以外的内容不做赘述。 TableWidget和 FormWidget作为一个基本的渲染模块与 Mask等不同,TableWidget与 FormWidget有着明确的目的,比如 TableWidget就是作为一个表格视图,这种名称中可以看得出来。其将表格的一系列能力聚拢,如单元格,行列选择排序,翻页等等; TableWidget 我们使用表格到底是在使用什么? 在我们讲述渲染流程前,我们需要提一个问题,在我们使用表格组件时,我们在使用什么,换而言之,我们对表格组件中关注的是什么?应该说我们更关注的是表格所展示的数据。TableWidget或者说整个低代码其实解决的就是这个问题,让我们可以很方便的展示数据。不用关心一些细枝末节 TableWidget 的渲染 TableWidget在整个渲染流程中的负责组装各种 Vue 组件所需要的核心数据或事件回调并传递给其绑定的 Vue 组件即 DefaultTable,如 allowRowClick,rowClickMode等等,这些值会作为 Props 传递给 DefaultTable,DefaultTable则一定意义上充当了一个桥接层,主要做了两件事,处理 Props, 处理 Slot,TableWidget传递给 DefaultTable的 Props 会经过 DefaultTable再次组合,创建新的 Props,同时根据当前的 Props 判断是否有必要新增一些 slot,比如 OioPagination组件是否需要渲染就取决于 Props.showPagination 的值,经过 DefaultTable的处理后, props与 slot会进一步交给 OioTable进行渲染,OioTable则会进一步聚合处理,比如如果没有 defaultSlot则进行空值的渲染,如果存在 footSlot则会构建一个包裹层去包裹它。这些组件协同完成了一个表格的结构,而我们真正关心的数据则由一个个 BaseTableColumnWidget渲染。BaseTableColumnWidget的渲染过程类似于 TableWidget,其负责组装 DefaultTableColumn渲染所需要的 props,然后交给 OioTableColumn进行实际渲染。并且会有多种基于 BaseTableColumnWidget的 widget通过重写 renderDefaultSlot,renderEditSlot,renderContentSlot,renderHeaderSlot等几个 props 实现不同状态,不同类型等组件的渲染。通过 TableWidget与 BaseTableColumnWidget相结合, Table 页面完成了主体框架与内容的渲染。而当数据完成渲染后,不可避免的会有数据交互,比如分页,排序,过滤等,这些交互都由 TableWidget与 BaseTableColumnWidget共同完成。比如排序翻页等,TableWidget会将事件作为 props 向下注入,当事件被调用,TableWidget会进行处理,比如发起请求,或者对内部数据排序等。而除了数据的展示,还有一些动作,即 Action ,Action 被触发时会按照内部的配置进行工作,比如编辑时,将获取 activeRecord,随后触发 Table 的编辑。 TableWidget就是这样去渲染出了一个完整的表格页面。能够完成数据的增删改查等操作 FormWidget FormWidget与 TableWidget一样,也是作为一个渲染模块,但是它与 TableWidget不同的是,FormWidget是作为一个表单视图,其将表单的一系列能力聚拢,如表单提交,表单校验,表单重置等等。其重点在于对数据的增改。所以在提供的能力上也有区别,比如 FormWidget没有提供翻页,排序,过滤等能力,因为这些能力属于表格的能力,而 FormWidget则更关注于表单的能力。提供了数据的存储,提交,校验等能力 Form 在渲染时流程与 Table 大同小异,其同样为三层结构 FormWidget => DefaultForm => FormFieldWidget 一层层向下渲染,不同的在于 FormWidget 更多的关注点在于维护其内部的 FormData 这是整个表单页面所围绕的东西,当页面上的控件发生变化,其变更的值会被收集到 FormData 中,并在后续中使用。同时在编辑已有数据场景下,Form 会将数据加载到 FormData,随后下放给 FormFiledWidget。 异同之处 从介绍中可以看出,Table 侧重于数据的查询展示,Form 则侧重于数据的变动处理,但是抽象的看其核心其实是同一套逻辑,即数据的存储与展示,中间或许会有对数据的某些处理,但是并不是本质上的区别,两者在核心理念上是一致的,即让使用者只需要关心数据本身,而不需要关注于繁琐的视图构造,这是整个低代码甚至前端的发展方向。

    2025年3月25日
    1.1K00
  • 「前端」动作API

    概述 在 oinone 前端平台中,提供了四种动作 跳转动作(页面跳转、打开弹窗、抽屉) 服务端动作(调用接口) 客户端动作(返回上一页、关闭弹窗等) 链接动作(打开执行的链接) 快速开始 // 基础使用示例 import { executeViewAction, executeServerAction, executeUrlAction } from '@kunlun/dependencies'; // 示例 1: 基础页面跳转(去创建页面) executeViewAction(action); // 示例 2: 带参数的页面跳转(查询ID为123的数据),去编辑、详情页 executeViewAction(action, undefined, undefined, { id: '123' }); // 示例 3: 页面跳转的参数,用最新的,防止当前页面的参数被带到下一个页面 executeViewAction(action, undefined, undefined, { id: '123' , preserveParameter: true}); // 示例 4: 调用服务端接口 const params = { id: 'xxx', name: 'xxx' }; await executeServerAction(action, params); await executeServerAction(action, params, { maxDepth: 2 }); // 接口数据返回的数据层级是3层 -> 从0开始计算, 默认是2层 // 执行链接动作 executeUrlAction(action); API 详解 executeViewAction 参数名 描述 类型 必填 默认值 — action 视图动作 RuntimeViewAction true router 路由实例 Router false undefined matched 路由匹配参数 Matched false undefined extra 扩展参数 object false {} target 规定在何处打开被链接文档(可参考 a 标签的 target) string false undefined executeServerAction 参数名 描述 类型 必填 默认值 ​action 服务端动作 RuntimeServerAction true param 传递给后端的参数 object true context 配置接口返回的数据层级(默认是两层) {maxDepth: number} false executeUrlAction 参数名 描述 类型 必填 默认值 ​action 链接动作 IURLAction true

    2025年3月21日
    1.8K00

Leave a Reply

Please Login to Comment