前端自定义请求入门版

在开发过程中,为了满足业务场景、增加灵活性,前端自定义请求不可避免。下面将会从——自定义 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低代码应用平台体验

(0)
银时的头像银时数式员工
上一篇 2025年3月31日 pm4:05
下一篇 2025年4月17日 pm8:52

相关推荐

  • 表格主题配置(v4)

    TableThemeConfig /** * 表格主题配置 */ export interface TableThemeConfig { border: boolean | string; stripe: boolean; isCurrent: boolean; isHover: boolean; /** * 表格列主题配置 */ column: Partial<TableColumnThemeConfig>; } /** * 表格列主题配置 */ export interface TableColumnThemeConfig { /** * <h3>最小宽度</h3> * <ul> * <li>boolean: enabled column width auto compute</li> * <li>number: using css width (default: px)</li> * <li>string: using css width</li> * <li> * object: auto compute width for label by default function * <ul> * <li>min: min min width (default: 120)</li> * <li>max: max min width (default: 432)</li> * <li>chineseWidth: chinese width (default: 14 -> fontSize: 14px)</li> * <li>otherWidth: non chinese width (default: 9 -> fontSize: 14px)</li> * <li>sortableFixWidth: sortable handle width (default: 40)</li> * <li>nonSortableFixWidth: non sortable fix width (default: 22)</li> * </ul> * </li> * <li>function: auto compute width for label by function</li> * </ul> */ minWidth: boolean | number | string | Partial<TableColumnMinWidthComputeConfig> | TableColumnMinWidthComputeFunction; /** * 操作列 */ operation: { /** * 宽度 (default: 165) */ width?: number | string; /** * 最小宽度 (default: 120) */ minWidth?: number | string; }; } export interface TableColumnMinWidthComputeConfig { min: number;…

    2023年11月6日
    1.2K00
  • 在前端视图添加自定义的区域块

    添加自定义区域块 平台提供了一系列默认的视图布局,可以帮助开发人员快速构建出复杂的企业应用系统。当然,我们可以使用自定义区域块来扩展表格、表单、画廊、树形等视图。 自定义区域块概述 平台视图布局都是通过XML配置实现的。在视图布局中,我们可以使用一些特定的元素标签来构建视图的表头、表单、搜索区域等部分。而自定义区域块,就是这些元素标签之外的部分。我们可以通过在视图布局的XML中添加自定义区域块,来扩展页面功能。 视图类型及相关元素 视图类型分为表格(TABLE)、表单(FORM)、画廊(GALLERY)、树形(TREE)等。不同类型的视图布局,包含的元素也有所不同。 下面是几种视图类型及其对应的元素: 表格:搜索区域、表格主体,其中表格主体包含了表格上面的动作、表格区域等部分。 表单:表单区域,包含了表单动作、表单区域等部分。 画廊:动作、卡片详细信息。 在表格页面添加自定义区域块 以下是一个示例,演示如何在表格页面顶部添加自定义区域块。 1. 修改视图布局XML 首先,我们需要修改表格视图的XML布局,添加自定义区域块元素标签。 <view type="TABLE"> <!– 这是搜索区域 –> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> </pack> <!– 这是表格主体 –> <pack widget="group" slot="tableGroup"> <!– 在这里添加自定义区域块元素标签 –> <element widget="MyCustomElement"></element> <!– 这是表格上面的动作 –> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <!– 这是表格区域 –> <element widget="table" slot="table" slotSupport="field"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" slotSupport="action" /> </element> </pack> </view> 在上述代码中,我们添加了一个名为MyCustomElement的元素标签。这将作为我们自定义区域块的容器。 2. 创建自定义Vue组件 接下来,我们需要创建一个Vue组件,并将其指定为自定义元素标签MyCustomElement的模板。 <template> <div> <!– 在这里编写自定义区域块的内容 –> <p>Hello, world!</p> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ components: { }, props: [], setup(props) { return {}; } }); </script> 在上述代码中,我们定义了一个非常简单的Vue组件,它在页面上显示一个“Hello, world!”的文本信息。 3. 创建自定义Element Widget 为了使自定义Vue组件与XML布局文件关联起来,我们需要创建一个对应的Element Widget。 import { BaseElementWidget, SPI, BaseElementViewWidget, Widget, ViewMode, FormWidget, BaseElementWidgetProps } from '@kunlun/dependencies'; import MyCustomElement from './MyCustomElement.vue'; @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'MyCustomElementWidget' })) export class MyCustomElementWidget extends BaseElementWidget { public initialize(props: BaseElementWidgetProps): this { super.initialize(props) this.setComponent(MyCustomElement) return this } } 在上述代码中,我们继承了BaseElementWidget类,并在其中指定了Vue组件MyCustomElement。这样,XML布局文件中的元素标签就能够正确地与Vue组件关联起来。 4. 注册视图布局 最后,我们需要将上述代码配置注册。具体而言,我们需要调用registerLayout方法来将XML布局文件、模块名和视图类型进行关联。 import { registerLayout, ViewType } from '@kunlun/dependencies'; registerLayout( `<view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" />…

    2023年11月1日
    2.7K00
  • 【界面设计器】自定义字段组件实战——轮播图

    阅读之前 此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 业务背景 用户需要从【创建/编辑】页面中上传多张图片,并且在【详情】页面将这多张图片进行【轮播】展示。 业务分析 从需求来看,我们需要实现一个【轮播图】组件,并且该组件允许在【详情】视图中使用。在其他视图中,我们可以直接使用平台内置的【图片】组件,实现基础的编辑和展示功能。 名词解释 业务模型:需要进行可视化管理的存储模型或代理模型。 准备工作 你需要在某个业务模型下创建一个【表格视图】用于查看全部数据,创建【表单视图】用于创建/编辑数据,并创建【详情视图】展示必要的信息。(为了方便起见,你可以在所有视图中仅使用编码和名称两个字段) 你需要将【表格视图】绑定到某个菜单上,并通过【跳转动作】将三个视图进行关联,可以完整执行当前模型的全部【增删改查】操作。 业务模型定义 (以下仅展示本文章用到的模型字段,忽略其他无关字段。) DemoModel 名称 API名称 业务类型 是否多值 长度(单值长度) 编码 code 文本 否 128 名称 name 文本 否 128 轮播图 carouselImages 文本 是 512 实现页面效果展示 表格视图 表单视图-创建 表单视图-编辑 详情视图 根据业务背景添加轮播图字段到所有视图 轮播图字段信息: 字段业务类型:文本 多值:是 使用组件:图片 无代码模型 在模型设计器创建轮播图字段,并从【组件库】-【模型】拖放至视图中即可。 PS:这里需要注意的是,在模型设计器中需要切换至专家模式,并确认字段长度为512,否则当URL超长时将无法保存。 低代码模型 与服务端同学确认字段,并从【组件库】-【模型】中拖放至视图中即可。 将上图中的【演示】数据进行【编辑】,并上传三张图片,在【详情视图】查看默认展示效果。 演示图片下载 创建组件、元件 准备工作完成后,我们需要根据【业务背景】确定【组件】以及【元件】相关信息,并在【界面设计器】中进行创建。 以下操作过程将省略详细步骤,仅展示可能需要确认的关键页面。 创建轮播图组件 创建轮播图元件 根据业务背景,我们需要根据模型中的字段确定业务类型,在这个场景中,可以使用如下配置。(暂时可以不进行属性面板的设计) 在【详情视图】中将【轮播图字段】的组件切换为我们新创建的【轮播图组件】 PS:这里会发现组件变成了【输入框】的样式,这是由于我们没有提供对应元件的代码实现,使得SPI找到了默认组件。 启动SDK工程进行组件基本功能开发 (npm相关操作请自行查看SDK工程中内置的README.MD) Carousel.vue <template> <a-carousel class="carousel" effect="fade" autoplay> <div class="carousel-item" v-for="image in images" :key="image"> <img :src="image" :alt="image" /> </div> </a-carousel> </template> <script lang="ts"> import { Carousel as ACarousel } from 'ant-design-vue'; import { computed, defineComponent, PropType } from 'vue'; export default defineComponent({ name: 'Carousel', components: { ACarousel }, props: { value: { type: Array as PropType<string[]> } }, setup(props) { const images = computed(() => props.value || []); return { images }; } }); </script> <style lang="scss"> .carousel { .slick-slide { height: 160px; & > div, .carousel-item { width: 100%; height: 100%; } img { max-width: 100%; max-height: 100%; margin: auto; } } } </style> 效果展示 开发完成后,我们将重新打包生成的JS文件和CSS文件在【界面设计器】的【低无一体】进行上传,就可以在【设计器环境】中正常使用了。 设计轮播图的属性面板 通过我们使用的a-carousel组件,我们发现组件中提供了很多【属性】或【功能】可以进行配置,比如是否自动切换(autoplay)、面板指示点位置(dotPosition)、是否显示面板指示点(dots)等。在这里我们将对这三个属性的配置方式进行演示,其他更多属性可以自行设计并开发。 我们可以在【界面设计器】的【属性面板设计】中根据这三个属性的字段类型确定以下信息: 功能 API名称 业务类型 选用组件 可选项 是否自动切换 autoplay 布尔 开关 -…

    2023年11月1日
    4.0K00
  • 如何在表格的字段内添加动作

    介绍 在日常的业务中,我们经常需要在表格内直接点击动作完成一些操作,而不是只能在操作栏中,例如:订单的表格内点击商品名称或者里面的按钮跳转到商品详情页面,这里我们将带来大家来通过自定义表格字段来实现这个功能。 1.编写表格字段组件 组件ts文件TableBtnFieldWidget.ts import { ActionWidget, ActiveRecordExtendKeys, BaseFieldWidget, BaseListView, ModelFieldType, queryDslWidget, queryRowActionBar, RowContext, SPI, TableStringFieldWidget, BaseElementListViewWidget, ViewType, Widget } from '@kunlun/dependencies'; import { createVNode, VNode } from 'vue'; import { toString } from 'lodash-es'; import TableBtnField from './TableBtnField.vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: ViewType.Table, ttype: [ModelFieldType.String, ModelFieldType.Text], // widget: 'StringLink', // 以下3行配置代码测试用,生产建议在界面设计器自定义组件,widget填自定义组件的api名称 model: 'resource.k2.Model0000000109', name: 'name' }) ) export class TableBtnFieldWidget extends TableStringFieldWidget { @Widget.Reactive() private get triggerActionLabel() { // 界面设计器组件内设计该属性 return this.getDsl().triggerActionLabel || '更新'; } private getTriggerAction() { return this.model.modelActions.find((a) => a.label === this.triggerActionLabel); } private getTriggerActionWidget(widgetHandle: string, draftId: string, triggerActionLabel: string): ActionWidget | undefined { const listView = Widget.select(widgetHandle) as unknown as BaseListView; const listWidget = queryDslWidget(listView?.getChildrenInstance(), BaseElementListViewWidget); if (!listWidget) { return undefined; } const rowActionBar = queryRowActionBar(listWidget.getChildrenInstance(), draftId); const actionWidget = rowActionBar?.getChildrenInstance().find((a) => (a as ActionWidget).action.label === triggerActionLabel); return actionWidget as ActionWidget; } protected clickAction(context: RowContext) { const draftId = context.data[ActiveRecordExtendKeys.DRAFT_ID] as unknown as string; const actionWidget = this.getTriggerActionWidget(this.getRootHandle()!, draftId, this.triggerActionLabel); if (!actionWidget) { console.error('未找到action', this.triggerActionLabel); return; } actionWidget.click(); } @Widget.Method() public renderDefaultSlot(context: RowContext): VNode[] | string { const value = toString(this.compute(context)); if (value) { return [ createVNode(TableBtnField,…

    2024年5月16日
    1.3K00
  • 列表页内上下文无关的动作如何添加自定义上下文

    场景 在界面设计器,可以配置当前列表页从上个页面带的上下文参数,现在需要传递这个上下文到下个页面,设计器没有配置入口,我们可以通过自定义改动作来解决 示例代码 import { ActionType, ActionWidget, RouterViewActionWidget, SPI, ViewActionTarget } from '@kunlun/dependencies'; @SPI.ClassFactory( ActionWidget.Token({ actionType: [ActionType.View], target: [ViewActionTarget.Router], // 模型编码 model: 'module.model', // 动作名称 name: 'actionName' }) ) export class DemoRouterViewActionWidget extends RouterViewActionWidget { protected async clickAction(): Promise<void> { // initialContext内是上个页面传来的上下文,手动将值传递到下个页面的上下文 // 这里假设需要传递的字段名为type this.action.context = { type: this.initialContext.type }; return super.clickAction(); } }

    2024年8月20日
    1.5K00

Leave a Reply

登录后才能评论