前端自定义请求入门版

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

相关推荐

  • 【路由】浏览器地址栏url参数介绍

    介绍 浏览器地址栏url为路由类型的视图动作(viewAction)的访问url 详情页示例url https://one.oinone.top/page;module=resource;viewType=DETAIL;model=resource.ResourceDistrict;action=redirectDetailPage;scene=redirectDetailPage;target=ROUTER;menu=%7B%22selectedKeys%22:%5B%22%E5%8C%BA%22%5D,%22openKeys%22:%5B%22%E5%9C%B0%E5%9D%80%E5%BA%93%22,%22%E5%9C%B0%E5%8C%BA%22%5D%7D;id=575733837679260950;path=%2Fresource%2F%E5%8C%BA%2FACTION%23resource.ResourceDistrict%23redirectDetailPage 通过调试工具查看解析后的信息 参数介绍 module 动作所在模块名称 viewType 视图类型 model 动作所在模型的编码 action 动作名称 target 动作打开方式,ROUTER为当前路由打开,OPEN_WINDOW为新窗口打开 menu 【选填】菜单栏控制参数,该参数不影响页面的业务逻辑,仅影响菜单栏展开哪些菜单项(通过openKeys属性),选中哪些菜单项(通过selectedKeys属性)),该参数经过JSON.stringify(menu)方式处理过 # 示例参数 { "selectedKeys": ["区"], "openKeys": ["地址库", "地区"] } id 【选填】详情、编辑等单行数据页面的数据id searchBody 列表页搜索区域的搜索条件,该参数在前端经过encodeURIComponent(JSON.stringify(searchBody))方式处理过 # 示例参数 { "code": "11" } searchConditions 列表页高级搜索条件,用于处理searchBody之外的复杂搜索条件,日常开发中无需关心该参数encodeURIComponent(JSON.stringify(searchConditions))方式处理过 # 示例参数 [ { "leftValue":["sourceType"], "operator":"==", "right":"GD" } ] context 上下文参数,该参数经过JSON.stringify(menu)方式处理过 列表页的此参数会填充到搜索区域的字段中作为默认的查询条件,详情 详情页和表单页此参数会作为页面加载函数的入参 # 示例参数 { “cateId”: “61723712399821” } path 权限验证路径,父页面编译的时候自动加上该参数,在父页面点击当前动作的时候会自动拼该参数 scene 【选填】动作场景值 代码中如何获取 这里介绍在组件内如何获取 import { BaseElementWidget } from ‘@kunlun/dependencies’; export class DemoElementWidget extends BaseElementWidget { protected test() { const { module, model, action } = this.urlParameters; } } 推荐阅读相关文档 上下文在字段和动作中的应用 如何实现页面间的跳转

    2024年8月19日
    1.7K00
  • 母版-布局-DSL 渲染基础(v4)

    概述 不论是母版、布局还是DSL,我们统一使用XML进行定义,可以更好的提供结构化表述。 参考文档: XML百度百科 XML语法参考 下面文档中未介绍到的Mask母版和Layout布局,可以去数据库中base库的表base_layout_definition和base_mask_definition的template字段查看 母版 确定了主题、非主内容分发区域所使用组件和主内容分发区域联动方式的页面配置。 母版内容分为主内容分发区域与非主内容分发区域。非主内容分发区域一般包含顶部栏、底部栏和侧边栏。侧边栏可以放置菜单,菜单与主内容分发区域内容进行联动。 默认母板 <mask> <multi-tabs /> <header> <widget widget="app-switcher" /> <block> <widget widget="notification" /> <widget widget="divider" /> <widget widget="language" /> <widget widget="divider" /> <widget widget="user" /> </block> </header> <container> <sidebar> <widget widget="nav-menu" height="100%" /> </sidebar> <content> <breadcrumb /> <block width="100%"> <widget width="100%" widget="main-view" /> </block> </content> </container> </mask> 该模板中包含了如下几个组件: mask:母版根标签 multi-tabs:多选项卡 header:顶部栏 container:容器 sldebar:侧边栏 nav-menu:导航菜单 content:主内容 breadcrumb:面包屑 block:div块 main-view:主视图;用于渲染布局和DSL等相关内容; 母版将整个页面的大体框架进行了描述,接下来将主要介绍布局和DSL是如何在main-view中进行渲染的。关于自定义母版组件的相关内容 点击查看 布局 布局是将页面拆分成一个一个的小单元,按照从上到下、从左到右进行顺序排列 布局主要用于控制页面中元素的展示的相对位置,原则上不建议将元数据相关内容在布局中进行使用,可最大化布局的利用率。 默认表格视图(TABLE) <view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" /> </view> </pack> <pack widget="group" slot="tableGroup"> <element widget="actionBar" slot="actionBar"> <xslot name="actions" /> </element> <element widget="table" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" /> <element widget="rowActions" slot="rowActions" /> </element> </pack> </view> 该模板中包含了如下几个组件: view:视图;用于定义当前视图类型,不同的视图类型会有不同的数据交互,以及渲染不同的组件。 pack:容器类型相关组件。 element:元素组件;包含各种各样的组件,根据组件实现有不同的作用。 xslot:DSL插槽;用于将DSL中定义的模板分别插入到对应的槽中; 特别的,任何XML标签上的slot属性都具备DSL插槽的全部能力。当学习完DSL相关内容后,我们将会对DSL插槽有比较清晰的理解。 PS:在下面的内容中,将使用该布局进行描述。 DSL 准备工作 为了方便描述DSL和元数据之间的关系,我们需要先定义一个简单模型,这个模型里面包含字段和动作。这些通常是服务端定义的。(对服务端不感兴趣的同学可以跳过代码部分) DemoModel.java @Model.model(DemoModel.MODEL_MODEL) @Model(displayName = "演示模型", labelFields = {"name"}) public class DemoModel extends IdModel { private static final long serialVersionUID = -7211802945795866154L; public static final String MODEL_MODEL = "demo.DemoModel"; @Field(displayName = "名称") private String name; @Field(displayName = "是否启用") private Boolean isEnabled; } DemoModelAction.java @Model.model(DemoModel.MODEL_MODEL) @UxRouteButton( action = @UxAction(name = "redirectCreatePage", displayName = "创建", contextType = ActionContextTypeEnum.CONTEXT_FREE), value = @UxRoute(model =…

    2023年11月1日
    1.4K10
  • 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日
    51800
  • 如何通过 Oineone 平台自定义视图

    在 Oineone 平台上,自定义视图允许用户替换默认提供的页面布局,以使用自定义页面。本文将指导您如何利用 Oineone 提供的 API 来实现这一点。 默认视图介绍 Oineone 平台提供了多种默认视图,包括: 表单视图 表格视图 表格视图 (左树右表) 详情视图 画廊视图 树视图 每种视图都有其标准的 layout。自定义视图实际上是替换这些默认 layout 的过程。 默认的表单视图 layout <view type="FORM"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="form" slot="form"> <xslot name="fields" slotSupport="pack,field" /> </element> </view> 内嵌的的表单视图 layout <view type="FORM"> <element widget="form" slot="form"> <xslot name="fields" slotSupport="pack,field" /> </element> </view> 默认的表格 <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="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> 内嵌的的表格 <view type="TABLE"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="table" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" /> </element> </view> 左树右表 <view type="table"> <pack title="" widget="group"> <view type="search"> <element slot="search" widget="search"/> </view> </pack> <pack title="" widget="group"> <pack widget="row" wrap="false"> <pack widget="col" width="257"> <pack title="" widget="group"> <pack widget="col"> <element slot="tree" widget="tree"/> </pack> </pack> </pack> <pack mode="full" widget="col"> <pack widget="row"> <element justify="START" slot="actionBar"…

    2024年4月3日
    61600
  • 如何提高自定义组件的开发效率

    引言 本文将通过前端的开发者模式带领大家提高自定义组件的开发效率 支持2024年9月6日之后用npm i安装的4.7.x之后的所有版本 介绍前端开发者模式 开发者模式的特性 浏览器控制台可以看到更多利于开发的日志输出 页面顶部状态栏消息模块的轮询接口,将只在页面刷新后请求一次,这样会减少开发阶段不必要的请求,以及解决后端断点调试的时候被消息轮询干扰的问题 页面视图的数据将不在走缓存,而是每次实时从base_view表里查询template字段获取,方便需要实时看通过xml修改的页面效果 如何进入开发者模式 通过在浏览器地址栏后追加;mode=dev刷新页面进入开发者模式,页面加载后可以在浏览器开发者工具的应用标签下的本地存储空间(即localStorage)中看到本标识的存储,如果想退出开发者模式可以手动清除该值 开发者模式的应用场景 1.通过控制台跳转到视图动作(ViewAction)的调试页面 页面刷新后,可以在浏览器开发者工具的控制台看到如下图的日志输出,直接点击该链接可以跳转到当前页面的调试链接弹窗视图动作、抽屉视图动作等以弹出层为展示效果的页面,之前的版本是无法进入调试链接查看的,现在可以在弹窗等视图动作点击后在控制台看到该链接点击进入调试页面 2.查看页面母版(mask)、布局(layout)的匹配情况 母版(mask)匹配结果布局(layout)匹配结果 3.查看视图组件(View)、字段组件(Field)、动作组件(Action)的匹配情况 通过浏览器开发者工具的控制台,我们可以看到每个视图、字段、字段匹配到的组件的class类名,这样我们就知道当前用的是那个组件,在自定义的时候可以在源码查看该组件类的实现代码,然后再选择是否继承该父类。另外我们在给组件定义SPI条件的时候,可以参考匹配入参和匹配结果内的属性。 匹配入参是当前元数据SPI的最全量注册条件 匹配结果是当前匹配到的组件的注册条件,一般情况下我们只需要在匹配结果的条件加上当前模型编码(model)、视图名称(viewName)、字段名称(name)、动作名称(actionName)等条件就可以完成自定义组件的覆盖。 在浏览器开发者工具的控制台,通过模型编码、视图名称、字段名称、动作名称等关键字快速搜索定位到需要查找的组件 控制台搜索快捷键:win系统通过ctrl+f,mac系统通过cmd+f 总结 在确保自定义部分代码最终正确被import导入到启动工程main.ts内的情况下,可以通过开发者模式在浏览器控制台打印的日志来排查组件未正确覆盖的问题。

    2024年9月6日
    1.2K00

Leave a Reply

登录后才能评论