软件开发模板化很香,但真到复杂业务就崩了——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: '动作名称'
}
);
通过上述步骤,自定义表格可以实现单元格合并和表头分组功能,同时支持动态渲染界面设计器配置的字段和动作。
这个布局定义包含了:
- 顶部搜索区域
- 表格操作栏
- 我们的自定义MergeTableWidget
- 行内动作插槽
技术解析
通过这个案例,我们可以看到Oinone的几个核心技术特点:
- 扩展性:通过SPI机制和Widget体系,可以轻松扩展原生组件功能
- 组合性:各个组件通过插槽(slot)机制灵活组合
- 动态性:视图和字段可以通过DSL动态配置
- 一致性:保持UI交互和数据流管理的统一模式
结语
回到最初的问题:为什么Oinone不提供现成的ERP模板?因为我们认为,真正的企业级应用需要的是可以自由组合的构建块,而非固化的模板。本文展示的自定义表格实现,只是Oinone强大扩展能力的一个缩影。
在Oinone的生态中,每个开发者都可以: • 创建自己的业务组件
• 定义领域特定的DSL
• 贡献可复用的解决方案
这才是Oinone想要构建的未来——不是由我们提供所有答案,而是提供一个让所有开发者都能自由创造的工具平台。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
企业级报表平台:表格产品提供的全方案解析
在数据驱动的商业环境中,企业级报表平台已成为组织决策和运营的核心支撑,用户对报表平台的交互体验期望也不断提升。现代企业需要的不再只是静态报表,而是能够整合多源数据、支持自主分析、提供更加简易交互方式的智能报表平台。 SpreadJS作为一款基于HTML5的纯前端表格控件,以其类Excel的界面和功能、强大的数据处理能力和灵活的扩展性,为企业构建下一代报表平台提供了全面的解决方案。 1. 类Excel报表:传统需求的现代实现 类Excel报表是SpreadJS最基础也是最强大的功能之一。它通过在前端完美复刻Excel界面、操作方式和公式引擎等,让用户无需学习成本即可上手学习,企业可以利用这一特性快速构建与线下Excel高度类似的报表填写、编辑和展示环境。 技术实现深度解析 界面交互层:SpeadJS采取类Excel设计模式,实现了超过500种与Excel兼容的公式函数,支持跨工作表计算、数组公式和动态数组、图表图形等等,确保了与Excel的兼容性。前端渲染引擎采用Canvas渲染,叠加葡萄城专利技术,保障百万级数据流畅滚动,可广泛应用于企业台账、财务、金融、事务所、实验室等对Excel功...
-
下一篇
深入浅出了解 PSI:隐私求交的原理与应用场景全解析
打开链接即可点亮社区Star,照亮技术的前进之路。 Github 地址:https://github.com/secretflow/secretflow The Problem of Private Set Intersection PSI 全称为 Private Set Intersection,直观的翻译名字为"隐私求交"。 从场景来看,隐私求交: 有许多个参与方,每个参与方持有各自的隐私数据 希望通过协议求到所有数据的交集 但是不泄漏除交集外的任何信息 目前常用的 PSI 算法有: ECDH [1] KKRT [2] PSTY [3] 1.1 ECDH 如果我们假设哈希函数 这里是入计算安全参数,通常我们可以取 128。 基于 DH 的 PSI 协议如下所示: 1.2 KKRT 结合 Cuckoo Hash 以及 Batched OPRF,可以构造出一个比较高效的基于 OT 的 PSI 协议。 由于 Batched OPRF 的构建基于 OT,因此我们可以认为 KKRT16 的 PSI 协议是基于 OT 构建的。 协议具体内容如下图所示,我们将左边参与方叫做 Alice ,右边参...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL数据库在高并发下的优化方案
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果