软件开发模板化很香,但真到复杂业务就崩了——Oinone 有它的答案


很多人第一次接触 Oinone,总会好奇地问: “你们有没有 ERP 模板?有没有现成的业务场景?”

我们常常笑着摇头。 不是因为没有,而是因为不想。

从第一天起,Oinone 就不是一个“提供现成答案”的产品。 我们更像是一把锋利的工具。 要造船?还是造飞机?我们不替你决定。我们只把最坚实的钢铁交到你手里,让你去发挥想象力。

这种选择,让我们的边界变得清晰: ——我们不去套 ERP 的模板,因为业务像潮水,永远在变化; ——我们只专注打磨工具,因为自由,才是最珍贵的价值。

我们憧憬的未来,也不是一个摆满“数式 Oinone 官方应用”的商店,而是一个热闹的集市: 无数合作伙伴把自己独特的应用摆上来,彼此碰撞,生态由此生长。

我们一直坚信: • 业务不会停留在今天,模板永远追不上变化 • 真正的价值,不是给你预设答案,而是给你自由选择 • 生态不是一家厂商的独角戏,而是伙伴们的共舞

这一理念,早已渗透进 Oinone 的每一行代码里。 比如最近上线的 自定义表格:不仅能合并单元格、分组表头,更重要的是,它证明了平台的开放性——当原生功能还未覆盖到你的业务时,你依旧能自由扩展,完成独属于自己的定制化实现。

这,才是 Oinone 一直想做的事。

⚡ 直达演示环境 ☕ 账号:admin ☕ 密码:admin

本文将讲解如何通过自定义实现表格支持单元格合并和表头分组。

点击下载对应的代码

在学习该文章之前,你需要先了解:

1: 自定义视图 2: 自定义视图、字段只修改 UI,不修改数据和逻辑 3: 自定义视图动态渲染界面设计器配置的视图、动作


1. 自定义 widget

创建自定义的 MergeTableWidget,用于支持合并单元格和表头分组。

// MergeTableWidget.ts
import { BaseElementWidget, SPI, ViewType, TableWidget, Widget, DslRender } from '@kunlun/dependencies';
import MergeTable from './MergeTable.vue';

@SPI.ClassFactory(
  BaseElementWidget.Token({
    viewType: ViewType.Table,
    widget: 'MergeTableWidget'
  })
)
export class MergeTableWidget extends TableWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(MergeTable);
    return this;
  }

  /**
   * 表格展示字段
   */
  @Widget.Reactive()
  public get currentModelFields() {
    return this.metadataRuntimeContext.model.modelFields.filter((f) => !f.invisible);
  }

  /**
   * 渲染行内动作VNode
   */
  @Widget.Method()
  protected renderRowActionVNodes() {
    const table = this.metadataRuntimeContext.viewDsl!;

    const rowAction = table?.widgets.find((w) => w.slot === 'rowActions');
    if (rowAction) {
      return rowAction.widgets.map((w) => DslRender.render(w));
    }

    return null;
  }
}

2. 创建对应的 Vue 组件

定义一个支持合并单元格与表头分组的 Vue 组件。

<!-- MergeTable.vue -->
<template>
  <vxe-table
    border
    height="500"
    :column-config="{ resizable: true }"
    :merge-cells="mergeCells"
    :data="showDataSource"
    @checkbox-change="checkboxChange"
    @checkbox-all="checkedAllChange"
  >
    <vxe-column type="checkbox" width="50"></vxe-column>
    <!-- 渲染界面设计器配置的字段 -->
    <vxe-column
      v-for="field in currentModelFields"
      :key="field.name"
      :field="field.name"
      :title="field.label"
    ></vxe-column>

    <!-- 表头分组  https://vxetable.cn/v4.6/#/table/base/group -->
    <vxe-colgroup title="更多信息">
      <vxe-column field="role" title="Role"></vxe-column>
      <vxe-colgroup title="详细信息">
        <vxe-column field="sex" title="Sex"></vxe-column>
        <vxe-column field="age" title="Age"></vxe-column>
      </vxe-colgroup>
    </vxe-colgroup>

    <vxe-column title="操作" width="120">
      <template #default="{ row, $rowIndex }">
        <!-- 渲染界面设计器配置的行内动作 -->
        <row-action-render
          :renderRowActionVNodes="renderRowActionVNodes"
          :row="row"
          :rowIndex="$rowIndex"
          :parentHandle="currentHandle"
        ></row-action-render>
      </template>
    </vxe-column>
  </vxe-table>

  <!-- 分页 -->
  <oio-pagination
    :pageSizeOptions="pageSizeOptions"
    :currentPage="pagination.current"
    :pageSize="pagination.pageSize"
    :total="pagination.total"
    show-total
    :showJumper="paginationStyle != ListPaginationStyle.SIMPLE"
    :showLastPage="paginationStyle != ListPaginationStyle.SIMPLE"
    :onChange="onPaginationChange"
  ></oio-pagination>
</template>

<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import { CheckedChangeEvent } from '@kunlun/vue-ui';
import { ActiveRecord, ActiveRecords, ManualWidget, Pagination, RuntimeModelField } from '@kunlun/dependencies';
import { ListPaginationStyle, OioPagination, OioSpin, ReturnPromise } from '@kunlun/vue-ui-antd';
import RowActionRender from './RowActionRender.vue';

export default defineComponent({
  mixins: [ManualWidget],
  components: {
    OioSpin,
    OioPagination,
    RowActionRender
  },
  inheritAttrs: false,
  props: {
    currentHandle: {
      type: String,
      required: true
    },
    // loading
    loading: {
      type: Boolean,
      default: undefined
    },
    // 表格展示的数据
    showDataSource: {
      type: Array as PropType<ActiveRecord[]>
    },

    // 分页
    pagination: {
      type: Object as PropType<Pagination>,
      required: true
    },

    pageSizeOptions: {
      type: Array as PropType<(number | string)[]>,
      required: true
    },

    paginationStyle: {
      type: String as PropType<ListPaginationStyle>
    },

    // 修改分页
    onPaginationChange: {
      type: Function as PropType<(currentPage: number, pageSize: number) => ReturnPromise<void>>
    },

    // 表格选中
    onCheckedChange: {
      type: Function as PropType<(data: ActiveRecords, event?: CheckedChangeEvent) => void>
    },

    // 表格全选
    onCheckedAllChange: {
      type: Function as PropType<(selected: boolean, data: ActiveRecord[], event?: CheckedChangeEvent) => void>
    },

    // 展示字段
    currentModelFields: {
      type: Array as PropType<RuntimeModelField[]>
    },

    // 渲染行内动作
    renderRowActionVNodes: {
      type: Function as PropType<(row: any) => any>,
      required: true
    }
  },
  setup(props, ctx) {
    /**
     * 单元格合并
     * https://vxetable.cn/v4.6/#/table/advanced/span
     */
    const mergeCells = ref([
      { row: 1, col: 1, rowspan: 3, colspan: 3 },
      { row: 5, col: 0, rowspan: 2, colspan: 2 }
    ]);

    // 单选
    const checkboxChange = (e) => {
      const { checked, record, records } = e;
      const event: CheckedChangeEvent = {
        checked,
        record,
        records,
        origin: e
      };

      props.onCheckedChange?.(records, event);
    };

    // 全选
    const checkedAllChange = (e) => {
      const { checked, record, records } = e;
      const event: CheckedChangeEvent = {
        checked,
        record,
        records,
        origin: e
      };

      props.onCheckedAllChange?.(checked, records, event);
    };

    return {
      mergeCells,
      ListPaginationStyle,
      checkboxChange,
      checkedAllChange
    };
  }
});
</script>

<style lang="scss"></style>

3. 创建行内动作

<script lang="ts">
import { ActionBar, RowActionBarWidget } from '@kunlun/dependencies';
import { debounce } from 'lodash-es';
import { createVNode, defineComponent } from 'vue';

export default defineComponent({
  inheritAttrs: false,
  props: {
    row: {
      type: Object,
      required: true
    },
    rowIndex: {
      type: Number,
      required: true
    },
    renderRowActionVNodes: {
      type: Function,
      required: true
    },
    parentHandle: {
      type: String,
      required: true
    }
  },
  render() {
    const vnode = this.renderRowActionVNodes();

    return createVNode(
      ActionBar,
      {
        widget: 'rowAction',
        parentHandle: this.parentHandle,
        inline: true,
        activeRecords: this.row,
        rowIndex: this.rowIndex,
        key: this.rowIndex,
        refreshWidgetRecord: debounce((widget?: RowActionBarWidget) => {
          if (widget) {
            widget.setCurrentActiveRecords(this.row);
          }
        })
      },
      {
        default: () => vnode
      }
    );
  }
});
</script>

4. 注册布局

// registry.ts

import { registerLayout, ViewType } from '@kunlun/dependencies';

registerLayout(
  `<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" slotSupport="field">
                <xslot name="searchFields" slotSupport="field" />
            </element>
        </view>
    </pack>
    <pack widget="group" slot="tableGroup">
        <element widget="actionBar" slot="actionBar" slotSupport="action">
            <xslot name="actions" slotSupport="action" />
        </element>
        <element widget="MergeTableWidget" slot="table" slotSupport="field">
            <element widget="expandColumn" slot="expandRow" />
            <xslot name="fields" slotSupport="field" />
            <element widget="rowActions" slot="rowActions" slotSupport="action" />
        </element>
    </pack>
</view>`,
  {
    model: '模型',
    viewType: ViewType.Table,
    actionName: '动作名称'
  }
);

通过上述步骤,自定义表格可以实现单元格合并和表头分组功能,同时支持动态渲染界面设计器配置的字段和动作。

这个布局定义包含了:

  1. 顶部搜索区域
  2. 表格操作栏
  3. 我们的自定义MergeTableWidget
  4. 行内动作插槽

技术解析

通过这个案例,我们可以看到Oinone的几个核心技术特点:

  1. 扩展性:通过SPI机制和Widget体系,可以轻松扩展原生组件功能
  2. 组合性:各个组件通过插槽(slot)机制灵活组合
  3. 动态性:视图和字段可以通过DSL动态配置
  4. 一致性:保持UI交互和数据流管理的统一模式

结语

回到最初的问题:为什么Oinone不提供现成的ERP模板?因为我们认为,真正的企业级应用需要的是可以自由组合的构建块,而非固化的模板。本文展示的自定义表格实现,只是Oinone强大扩展能力的一个缩影。

在Oinone的生态中,每个开发者都可以: • 创建自己的业务组件

• 定义领域特定的DSL

• 贡献可复用的解决方案

这才是Oinone想要构建的未来——不是由我们提供所有答案,而是提供一个让所有开发者都能自由创造的工具平台。

                                                                                </div>



Source link

未经允许不得转载:紫竹林-程序员中文网 » 软件开发模板化很香,但真到复杂业务就崩了——Oinone 有它的答案

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
关于我们 免责申明 意见反馈 隐私政策
程序员中文网:公益在线网站,帮助学习者快速成长!
关注微信 技术交流
推荐文章
每天精选资源文章推送
推荐文章
随时随地碎片化学习
推荐文章
发现有趣的