前端自定义请求入门版

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

    环境配置 前端环境配置包含.env编译时配置和RuntimeConfig运行时配置 编译时配置 .env 属性 类型 默认值 作用 BASE_PATH [String] 统一配置URL请求路径前缀 STATIC_IMG [String] 静态资源路径 RUNTIME_CONFIG_BASE_URL [String] 运行时配置文件URL请求路径前缀 RUNTIME_CONFIG_FILENAME [String] manifest 运行时配置文件名 MESSAGE_LEVEL DEBUG、SUCCESS、INFO、WARN、ERROR 消息级别 BASE_PATH 当配置BASE_PATH=/test时,前端所有请求就将添加该前缀。 如/login登录页,将自动修改为/test/login MESSAGE_LEVEL 当配置消息级别时,全局的消息通知将根据消息级别进行过滤。 DEBUG:允许全部级别的消息通知。 SUCCESS:仅允许ERROR、WARN、INFO、SUCCESS级别的消息通知。 INFO:仅允许ERROR、WARN、INFO级别的消息通知。 WARN:仅允许ERROR、WARN级别的消息通知。 ERROR:仅允许ERROR级别的消息通知。 运行时配置 RuntimeConfig 运行时配置是作为编译时配置的补充。 manifest.js runtimeConfigResolve({ …… }); 开发时使用运行时配置 创建{PROJECT_NAME}/public/manifest.js文件。 如kunlun-boot/public/manifest.js 生产环境使用运行时配置 在nginx中配置的dist访问路径下,创建manifest.js文件。 若将/opt/pamirs/frontend/dist作为访问路径,则创建/opt/pamirs/frontend/dist/manifest.js文件。 注意事项 编译时可能使用编译时配置改变运行时配置文件的路径和文件名,注意文件名和访问路径必须匹配。 在浏览器中访问时,nginx或者浏览器会对js文件会进行缓存处理,会导致运行时配置不能及时生效。最好的办法是配置nginx时,禁用manifest.js文件缓存。 在manifest.js文件中建议不要包含任何与配置项无关的字符(包括注释),该文件在通过网络传输过程中,无法处理这些无效内容。 面包屑配置 breadcrumb 配置方式 runtimeConfigResolve({ breadcrumb?: boolean | BreadcrumbConfig }); BreadcrumbConfig /** * 面包屑配置 */ export interface BreadcrumbConfig extends RuntimeConfigOptions, EnabledConfig { /** * <h3>是否启用</h3> * <p>启用时,需配合mask渲染对应的面包屑组件</p> * <p>默认: 开启</p> */ enabled?: boolean; /** * <h3>首页配置</h3> * <p>boolean值与{@link BreadcrumbHomepageConfig#enabled}等效</p> */ homepage?: boolean | BreadcrumbHomepageConfig; } /** * 首页配置 */ export interface BreadcrumbHomepageConfig extends RuntimeConfigOptions, EnabledConfig { /** * <h3>首项显示首页</h3> * <p>默认: 开启</p> */ enabled?: boolean; /** * <h3>首页类型</h3> * <p>默认: 'application'</p> */ type?: 'application' | 'module'; } 多选项卡配置 multiTabs 配置方式 runtimeConfigResolve({ multiTabs?: boolean | MultiTabsConfig }); MultiTabsConfig /** * 多标签页配置 */ export interface MultiTabsConfig extends RuntimeConfigOptions, EnabledConfig { /** * <h3>是否启用(非运行时)</h3> * <p>启用时,需配合mask渲染对应的多标签页管理组件</p> * <p>默认: 开启</p> */ enabled?: boolean; /** * <h3>是否使用内联多标签页</h3> * <p>仅在使用默认mask时生效</p> */ inline?: boolean; /** * <h3>显示模块Logo</h3> * <p>默认: 开启</p> */ showModuleLogo?: boolean; /** * <h3>最多标签页数量</h3> *…

    前端 2023年11月1日
    91800
  • 「前端」关闭源码依赖

    问题背景 在 5.x 版本的 oinone 前端架构中,我们开放了部分源码,但是导致以下性能问题: 1.构建耗时增加​​:Webpack 需要编译源码文件 2.​​加载性能下降​​:页面需加载全部编译产物 3.​​冷启动缓慢​​:开发服务器启动时间延长 以下方案通过关闭源码依赖。 操作步骤 1. 添加路径修正脚本 1: 在当前项目工程根目录中的scripts目录添加patch-package-entry.js脚本,内容如下: const fs = require('fs'); const path = require('path'); const targetPackages = [ '@kunlun+vue-admin-base', '@kunlun+vue-admin-layout', '@kunlun+vue-router', '@kunlun+vue-ui', '@kunlun+vue-ui-antd', '@kunlun+vue-ui-common', '@kunlun+vue-ui-el', '@kunlun+vue-widget' ]; // 递归查找目标包的 package.json function findPackageJson(rootDir, pkgName) { const entries = fs.readdirSync(rootDir); for (const entry of entries) { const entryPath = path.join(rootDir, entry); const stat = fs.statSync(entryPath); if (stat.isDirectory()) { if (entry.startsWith(pkgName)) { const [pkGroupName, name] = pkgName.split('+'); const pkgDir = path.join(entryPath, 'node_modules', pkGroupName, name); const pkgJsonPath = path.join(pkgDir, 'package.json'); if (fs.existsSync(pkgJsonPath)) { return pkgJsonPath; } } // 递归查找子目录 const found = findPackageJson(entryPath, pkgName); if (found) return found; } } return null; } // 从 node_modules/.pnpm 开始查找 const pnpmDir = path.join(__dirname, '../', 'node_modules', '.pnpm'); for (const pkgName of targetPackages) { const packageJsonPath = findPackageJson(pnpmDir, pkgName); if (packageJsonPath) { try { const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (packageJSON.main === 'index.ts') { const libName = packageJSON.name.replace('@', '').replace('/', '-'); packageJSON.main = `dist/${libName}.esm.js`; packageJSON.module = `dist/${libName}.esm.js`; const typings = 'dist/types/index.d.ts'; packageJSON.typings = typings; const [pwd] = packageJsonPath.split('package.json'); const typingsUrl = path.resolve(pwd, typings); const dir = fs.existsSync(typingsUrl); if (!dir)…

    2025年4月17日
    28000
  • 动作API

    ActionWidget 动作组件的基类,包含了动作组件的通用属性和方法 示例 class MyActionWidget extends ActionWidget { } 动作属性 属性名 说明 类型 可选值 默认值 label 动作的名称 String – 当前动作的displayName action 当前动作的元数据 RuntimeAction – model 运行时模型 RuntimeModel – viewAction 运行时视图动作 RuntimeViewAction – view 运行时视图 RuntimeViewAction – initialValue 视图初始值 ActiveRecord[] – initialContext 视图初始上下文 Object – urlParameters 获取url参数 UrlQueryParameters – scene 场景 String – loading 动作加载状态 Boolean – false disabled 是否禁用 Boolean – false disabledTitle 禁用时的按钮名称 String – – invisible 当前字段是否不可见 Boolean – false validateForm 点击动作后是否校验表单 Boolean – false actionDomain 动作的domain查询条件 String – undefined goBack 点击动作后是否返回上一页 Boolean – false isDialog 是否为弹窗内动作 Boolean – 弹窗下的动作默认为true closeDialog 点击动作后是否关闭弹窗 Boolean – 默认为isDialog的值 isDrawer 是否为抽屉内动作 Boolean – 抽屉下的动作默认为true closeDrawer 点击动作后是否关闭抽屉 Boolean – 默认为isDrawer的值 isInnerPopup 是否为页内弹出层动作 Boolean – 页内弹出层下的动作默认为true isAsync 是否为异步动作 Boolean – true refreshRoot 是否刷新根视图 Boolean – false refreshData 是否刷新数据 Boolean – true type 动作的类型 ButtonType – 行内动作默认为ButtonType.link,其他动作为ButtonType.primary bizStyle 动作的业务类型 ButtonBizStyle – ButtonBizStyle.default icon 动作的图标 String – – enableConfirm 是否开启二次确认 Boolean – true confirmType 二次确认的类型 ConfirmType – – confirm 二次确认的内容 String – – confirmText 二次确认的提示内容 String – – confirmPosition 二次确认提示的展示位置 PopconfirmPlacement – PopconfirmPlacement.BM enterText 二次确认的确定按钮文字 String – – cancelText 二次确认的取消按钮文字 String – – searchBody 列表页的动作可以拿到搜索区域的搜索条件 ActiveRecord…

    2024年3月8日
    1.0K00
  • 如果让表单支持单选 ?

    本文将介绍在代码和XML配置中的修改,支持表格的单选功能。 先自定义一个widget,继承平台默认的table @SPI.ClassFactory( BaseElementWidget.Token({ viewType: ViewType.Table, widget: 'MyRadioTable' }) ) export class MyRadioTable extends TableWidget { @Widget.Reactive() protected get selectMode(): ListSelectMode { return ListSelectMode.radio; } } 注册layout 如果当前是主表格,那么xml配置如下 const 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="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="MyRadioTable" slot="table" slotSupport="field"> <element widget="expandColumn" slot="expandRow" /> <element widget="RadioColumn" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" slotSupport="action" /> </element> </pack> </view>` registerLayout(xml, { moduleName: '模块名称', model: '模型', viewType: ViewType.Table }) 如果当前的表格是form表单页面的表格,那么xml配置如下 const xml = `<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="MyRadioTable" slot="table"> <element widget="expandColumn" slot="expandRow" /> <element widget="RadioColumn" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" /> </element> </view>` registerLayout(xml, { moduleName: '模块名称', model: '模型', viewType: ViewType.Table })

    2023年11月27日
    66200
  • TS 结合 Vue 实现动态注册和响应式管理

    基础知识 1: 面向对象 面向对象编程是 JavaScript 中一种重要的编程范式,它帮助开发者通过类和对象组织代码,提高代码的复用性。 2: 装饰器 装饰器在 JavaScript 中是用于包装 class 或方法的高阶函数 为了统一术语,下面的内容会把装饰器讲成注解 在 oinone 平台中,无论是字段还是动作,都是通过 ts + vue 来实现的,ts 中是面向对象的写法,所有的属性、方法建议都在对应的 class 中,如果有通用的属性跟方法,可以放在一个公共的 class 中,然后通过继承来实现,这样便于维护。 <!– FormString.vue –> <template> <div> <p>用户名: {{userName}}</p> <button @click="updateUser({name: '王五'})">修改用户名</button> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: { userName: { type: String, default: '' }, userInfo: { type: Object, default: () => ({}) }, updateUser: { type: Function, default: () => () => ({}) } } }); </script> import FormString from './FormString.vue' @SPI.ClassFactory( FormFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String }) ) export class FormCustomStringFieldWidget extends FormFieldWidget { public initialize(props) { super.initialize(props); // 调用父类方法,确保继承的属性和方法正常初始化 this.setComponent(FormString); // 注册 Vue 组件,这样该 Widget 就会渲染 FormString 组件 return this; } public otherInfo = { name:'张三' } @Widget.Reactive() public userInfo = { name:'李四' } @Widget.Reactive() public get userName() { return this.userInfo.name } @Widget.Method() public updateUser userName(user) { this.userInfo = user } public updateOtherUser userName(user) { this.otherUser = user } } 这段代码定义了一个 FormCustomStringFieldWidget 类,用于处理表单中 String 类型字段的展示和交互。该类继承自 FormFieldWidget,并使用了多种注解和特性来实现不同功能。下面是对代码的详细讲解。 SPI 讲解 @SPI.ClassFactory() 无论是自定义字段还是动作,或者是自定义 mask、layout,都会用到@SPI.ClassFactory来注册,@SPI.ClassFactory 是一个注解,它标记了该类是通过工厂模式注册的。 在前端中,所有的注解(装饰器)本质上还是高阶函数,下面是一段伪代码。 const SPI = { ClassFactory(token) { return (targetClass) => {…

    2024年9月21日
    1.5K00

Leave a Reply

登录后才能评论