简介:开箱即用的企业协同办公开发框架,后端用SpringBoot,前端用Vue3+TypeScript,内置可视化低代码平台。不用写基础代码,靠拖拽表单、配置审批流程、设置角色权限,就能生成人事管理、客户关系(CRM)、电子合同审批、项目进度跟踪、办公用品申领等业务系统。提供完整源码,含标准RESTful接口、RBAC权限体系、Activiti/Flowable工作流引擎、站内信与邮件通知、多格式文件上传下载、uni-app封装的移动端适配方案,以及本地mock数据模拟能力。工程结构清晰:c-core(核心组件)、c-main(主服务)、c-mobile(移动端)、front(前端工程),配套详细安装说明、README文档和类型定义(types)、静态资源(static)、工具函数(utils)、状态管理(store)等模块,方便二次开发和私有化部署。
我做过不少企业级OA系统的交付项目,从纯手写代码到后来用低代码平台快速交付,踩过太多坑。这套基于SpringBoot+Vue3的低代码OA开发套件,是我去年带队重构并开源的一套真实落地框架——不是Demo,不是教学玩具,而是我们给三家制造业客户做私有化部署时反复打磨出来的生产级底座。它解决的核心问题很实在:业务部门提需求,IT团队三天内必须给出可试用的原型;法务要改合同审批节点,行政要加个办公用品申领流程,不用等排期、不找开发、自己就能配好上线。关键词里说的“低代码OA”“人事CRM一体化”“合同项目管理”,不是概念包装,而是每天在客户现场被真实高频使用的功能模块。整套系统跑在国产化信创环境(麒麟OS+达梦数据库+东方通中间件)上也完全没问题,所有模块都按微服务边界做了物理隔离,c-core是真正沉淀下来的通用能力层,不是简单抽个工具类。下面我就以一个实际交付工程师的视角,把这套框架怎么用、为什么这么设计、哪些地方容易踩坑,掰开揉碎讲清楚。
1. 整体架构设计与核心思路拆解
1.1 为什么选择SpringBoot + Vue3双栈而非全栈低代码平台
市面上很多所谓“低代码OA”,本质是把表单引擎和流程引擎封装成SaaS服务,数据存在别人服务器上,权限模型固定,改个字段类型都要提工单。而我们这套方案坚持“代码可控、数据自主、扩展自由”的原则,底层仍是标准Java生态和现代前端工程体系。SpringBoot选型不是图省事,而是因为它的自动装配机制天然适配低代码场景——比如你拖拽生成一个“员工档案”表单,系统会自动扫描@Entity注解下的实体类,通过spring-boot-starter-data-jpa动态注册JPA Repository,再结合@RestController模板生成RESTful接口。整个过程不需要手写Controller,但所有生成的代码都是可读、可调试、可覆盖的。Vue3的选择更关键:Composition API + TypeScript的组合,让“可视化配置”能精准映射到组件逻辑。举个例子,你在后台配置一个“合同金额”字段为必填且需大于0,前端生成的表单组件里,useFormRules()会自动注入required: true和min: 0.01校验规则,而不是用字符串拼接JS脚本。这种强类型约束,避免了传统低代码平台常见的运行时类型错误。
更重要的是,我们没用任何商业低代码引擎(如OutSystems、Mendix),所有可视化能力都是自研的。表单设计器基于JSON Schema v7规范,流程引擎对接的是Flowable 6.8.0(非Activiti,因后者已停止维护),权限模型严格遵循RBACv2标准(带资源实例级控制)。这意味着你今天拖拽生成的CRM模块,明天可以无缝接入企业微信或钉钉的OAuth2登录,后天还能把合同审批流程导出为BPMN 2.0标准文件交给法务审核——所有能力都在开放标准之上构建,不是黑盒。
1.2 模块化分层设计:c-core、c-main、c-mobile、front的职责边界
整个工程不是简单堆砌代码,而是按企业级应用生命周期严格分层:
-
c-core 是真正的“能力中台”。它不包含任何业务逻辑,只提供:统一异常处理框架(
GlobalExceptionHandler)、多租户上下文(TenantContext)、审计日志切面(@LogAudit注解)、动态数据源路由(支持MySQL/Oracle/Dameng自动切换)、以及最重要的——低代码元数据管理器(MetaModelService)。这个服务负责存储所有可视化配置产生的元数据:表单结构JSON、流程定义XML、权限策略规则。它用Redis缓存热点元数据,用MySQL持久化,保证每次页面加载时表单渲染延迟<200ms。 -
c-main 是业务主服务。它依赖c-core,但只做一件事:把元数据翻译成运行时行为。比如你配置了一个“请假审批”流程,c-main里的
ProcessDefinitionLoader会监听Flowable的DeploymentEvent,自动将BPMN文件部署到流程引擎,并注册对应的TaskListener处理节点跳转逻辑。这里有个关键设计:所有业务模块(人事、CRM、合同)都作为Spring Boot的@Configuration类注入,通过@ConditionalOnProperty控制开关。客户不需要合同模块?删掉contract-autoconfigure模块的Maven依赖,启动时自动跳过相关Bean注册。 -
c-mobile 是uni-app封装的移动端。它不是简单把PC端页面响应式适配,而是针对移动场景重构交互:审批操作支持离线签名(Canvas手写轨迹存本地SQLite),消息通知集成厂商通道(华为Push、小米FCM),扫码识别合同附件直接调起PDF阅读器。所有API调用都经过
c-mobile-api网关模块做JWT透传和设备指纹校验,防止API被爬虫滥用。 -
front 是Vue3前端工程。它采用Monorepo结构,
packages/下分ui-kit(原子组件库)、form-renderer(表单渲染器)、workflow-designer(流程设计器)三个子包。最值得说的是form-renderer:它不渲染HTML,而是生成VNode树。当你拖拽一个“级联选择器”,它输出的是h(Cascader, { options: xxx, props: yyy }),而非<div class="cascader">...</div>。这使得后续做主题定制(比如换成Ant Design风格)只需替换packages/ui-kit/cascader.vue,无需修改任何业务代码。
这种分层不是为了炫技,而是解决真实痛点。去年给某汽车零部件厂做二期改造时,他们要求把原OA的“供应商准入”流程迁移到新系统。我们只用了半天:把旧系统的流程XML导入c-core元数据中心,调整几个节点角色绑定,然后在front里新建一个/supplier-entry路由,引用form-renderer自动渲染表单——全程没动一行Java代码,也没改Vue组件。
1.3 低代码能力的实现边界:什么能拖拽,什么必须编码
很多人误以为低代码就是“不用写代码”,其实这是危险认知。我们明确划定了三条红线:
-
绝对禁止拖拽生成的:数据库事务边界(
@Transactional注解)、分布式锁实现(RedisLock)、敏感操作二次验证(短信验证码校验逻辑)。这些涉及数据一致性和安全性的核心,必须由开发人员手写,系统会在代码扫描阶段强制拦截。 -
允许拖拽但需人工校验的:表单字段校验规则、流程审批人表达式、权限资源路径。比如你配置“合同金额>100万时需法务总监审批”,系统会生成SpEL表达式
#amount > 1000000 ? 'legal-director' : 'dept-manager',但会在提交前弹窗提示:“该表达式未经过单元测试,请确认逻辑正确性”。 -
完全自动化生成的:CRUD接口、列表页表格列配置、详情页字段布局、基础权限菜单项。这部分生成的代码全部带
// AUTO-GENERATED BY LOW-CODE ENGINE标记,Git提交时会触发SonarQube扫描,确保无SQL注入漏洞(MyBatis动态SQL被严格限制为<if>和<choose>,禁用<script>标签)。
这个边界设计源于血泪教训。早期版本曾允许拖拽生成Service层方法,结果某客户在“采购申请”模块里配置了循环调用审批接口的流程,导致线程池耗尽。现在所有生成逻辑都经过静态分析:流程引擎会检测BPMN中的callActivity节点是否形成闭环,表单引擎会校验JSON Schema中$ref引用是否超过3层嵌套。
2. 核心模块实现细节与实操要点
2.1 可视化表单设计器:从JSON Schema到可交互UI的完整链路
表单设计器是用户接触最多的模块,它的健壮性直接决定低代码体验。我们没用现成的Schema Form库(如vue-json-schema-form),而是基于Vue3的render函数重写了一套轻量级渲染器,原因有三:一是避免第三方库对TypeScript泛型的支持缺陷;二是需要深度集成权限控制(比如“身份证号”字段在HR角色可见,但普通员工不可见);三是必须支持离线场景(移动端无网络时仍能渲染历史表单)。
整个链路分四步:
- 元数据建模:用户在后台配置表单时,所有操作最终生成符合JSON Schema Draft-07标准的描述对象。例如一个“员工入职登记”表单,其Schema长这样:
{
"title": "员工入职登记",
"type": "object",
"properties": {
"name": { "type": "string", "title": "姓名", "maxLength": 20 },
"idCard": {
"type": "string",
"title": "身份证号",
"pattern": "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dxX]$",
"x-permission": ["hr:read", "admin:write"]
},
"department": {
"type": "string",
"title": "所属部门",
"enum": ["研发部", "销售部", "行政部"],
"x-dict": "DEPT_DICT"
}
},
"required": ["name", "idCard"]
}
注意x-permission和x-dict这两个扩展字段,它们是权限控制和字典联动的关键。
-
Schema解析与校验:
form-renderer在setup()中调用parseSchema(schema)方法,将JSON Schema转换为内部FieldDescriptor对象树。这里做了关键优化:对pattern正则表达式进行预编译缓存,避免每次渲染都重新new RegExp();对enum数组自动转换为Map结构,提升选项查找性能。 -
动态组件挂载:根据
type字段匹配内置组件。string类型对应InputText,number对应InputNumber,但x-dict存在时会自动替换为DictSelect组件。这个组件不是简单下拉框,它会先检查localStorage是否有DEPT_DICT缓存,没有则调用/api/dict/get?code=DEPT_DICT获取,获取后存入IndexedDB(比localStorage容量大且支持索引查询)。 -
响应式绑定与校验:所有字段使用
ref()创建响应式变量,校验规则通过computed()动态生成。比如idCard字段的校验函数:
const idCardRule = computed(() => ({
required: true,
pattern: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dxX]$/,
message: '请输入正确的18位身份证号'
}))
这里pattern直接复用Schema里的正则,保证前后端校验一致性。
提示:实际部署时发现,某些客户浏览器(如老版本IE11)不支持
RegExp.flags,导致pattern.toString()报错。解决方案是在parseSchema阶段对正则做兼容性处理:new RegExp(pattern.source, pattern.flags || 'g')。
2.2 工作流引擎集成:Flowable与业务逻辑的松耦合设计
Flowable是当前最活跃的BPMN引擎,但我们没把它当黑盒用。核心设计原则是:流程定义与业务逻辑分离,状态变更与事件驱动解耦。
具体实现分三层:
-
流程定义层:所有BPMN文件存放在
c-main/src/main/resources/processes/目录下,命名规则为module-process-key.bpmn20.xml(如hr-onboard.bpmn20.xml)。系统启动时,ProcessDefinitionLoader扫描该目录,调用repositoryService.createDeployment().addClasspathResource().deploy()完成部署。关键点在于:每个BPMN文件的process节点必须设置id="hr-onboard",且startEvent的id必须为start——这是约定,不是强制,但违反会导致后续事件监听失效。 -
任务执行层:用户在前端点击“提交审批”时,前端调用
/api/workflow/start?processKey=hr-onboard,后端WorkflowController接收请求,构造RuntimeService.startProcessInstanceByKey()参数。这里不传业务数据,只传流程变量businessKey(如EMPLOYEE_2023001),业务数据存于独立数据库表biz_employee_onboard中。这样设计的好处是:流程引擎只管状态流转,业务数据由Service层维护,避免Flowable事务污染业务事务。 -
事件监听层:这是最体现设计功力的部分。我们在
c-core中定义了TaskCompleteListener接口,所有业务模块实现该接口并标注@Component("hrOnboardCompleteListener")。c-main的TaskEventListener会自动扫描这些Bean,在Flowable的TaskListener触发complete事件时,根据processDefinitionKey匹配对应监听器。比如HR模块的监听器:
@Component("hrOnboardCompleteListener")
public class HrOnboardCompleteListener implements TaskCompleteListener {
@Override
public void onTaskComplete(String processInstanceId, String taskId, Map<String, Object> variables) {
// 1. 更新员工状态为"已入职"
employeeService.updateStatus(variables.get("employeeId").toString(), "ONBOARD");
// 2. 发送入职欢迎邮件
emailService.sendWelcomeEmail(variables.get("email").toString());
// 3. 创建OA账号(调用LDAP接口)
ldapService.createUser(variables);
}
}
这种设计让业务逻辑彻底脱离Flowable API,测试时可直接Mock监听器,无需启动流程引擎。
注意:Flowable默认的异步任务(Async Executor)在高并发审批场景下容易堆积。我们在
application.yml中做了针对性调优:
yaml flowable: async-executor: activate: true thread-pool-core-size: 8 thread-pool-max-size: 16 queue-capacity: 1000
并增加监控端点/actuator/flowable,实时查看队列积压数。某次客户上线首日,审批队列峰值达3200,我们立即扩容了两个工作节点。
2.3 RBAC权限体系:从菜单权限到数据权限的四级控制
权限不是简单的“能看到什么”,而是分四个层级精细控制:
-
系统级权限:控制能否访问
/admin后台。通过Spring Security的HttpSecurity.authorizeHttpRequests()配置,所有/admin/**路径要求hasRole('ADMIN')。 -
菜单级权限:控制左侧导航栏显示哪些菜单项。
c-core提供MenuService,根据当前用户角色查询sys_menu_role关联表,返回带hidden属性的菜单树。前端Layout.vue遍历菜单时,若item.hidden === true则不渲染。 -
操作级权限:控制按钮是否显示(如“删除”、“导出”)。前端组件通过
v-permission="'user:delete'"指令判断,指令内部调用PermissionService.hasPermission('user:delete'),该服务查询sys_role_permission表。 -
数据级权限:这才是最难啃的骨头。比如销售总监只能看自己团队的客户,不能看其他区域数据。我们采用“数据过滤器”模式:在MyBatis的
@Select注解SQL中加入/*%securityFilter%*/占位符,DataPermissionInterceptor拦截所有查询,根据用户角色动态注入WHERE条件。例如CRM客户列表查询:
SELECT * FROM crm_customer
WHERE status = 'ACTIVE'
/*%securityFilter%*/
ORDER BY create_time DESC
拦截器检测到占位符,查出用户所属部门ID,注入AND dept_id IN (SELECT dept_id FROM sys_user_dept WHERE user_id = #{currentUserId})。
实操心得:数据权限最容易出错的是关联查询。比如查“客户+联系人”列表,
crm_contact表也需要加数据过滤。我们的解决方案是在@SelectProvider中统一处理,所有Mapper接口方法都必须标注@DataPermission(table = "crm_customer"),拦截器自动推导关联表过滤条件。上线前务必用@Test写数据权限测试用例,模拟不同角色查询结果集差异。
3. 完整实操流程:从零搭建一个“办公用品申领”模块
现在带你走一遍真实交付场景:客户提出需求,“下周要上线办公用品申领流程,申请人填表单,部门经理审批,行政专员发货,全程留痕”。
3.1 后端模块初始化与元数据配置
第一步不是写代码,而是初始化模块骨架。进入c-main模块,执行命令:
mvn archetype:generate \
-DgroupId=com.example.oa \
-DartifactId=office-supply \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
然后在pom.xml中添加依赖:
<dependency>
<groupId>com.example.oa</groupId>
<artifactId>c-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.8.0</version>
</dependency>
接着配置元数据。打开c-core的src/main/resources/meta/office-supply.json:
{
"moduleCode": "office-supply",
"moduleName": "办公用品申领",
"tables": [
{
"tableName": "office_supply_apply",
"fields": [
{ "name": "apply_id", "type": "string", "primaryKey": true, "generator": "uuid" },
{ "name": "applicant", "type": "string", "title": "申请人", "x-permission": ["*"] },
{ "name": "dept", "type": "string", "title": "所属部门", "x-dict": "DEPT_DICT" },
{ "name": "items", "type": "array", "title": "申领物品", "items": { "type": "object", "properties": { "name": { "type": "string" }, "count": { "type": "integer" } } } }
]
}
],
"workflows": [
{
"key": "office-supply-approval",
"name": "办公用品审批流程",
"bpmnPath": "classpath:/processes/office-supply.bpmn20.xml"
}
]
}
这个JSON会被MetaModelService加载,自动生成OfficeSupplyApply实体类、OfficeSupplyApplyMapper接口、以及/api/office-supply/apply RESTful接口。你只需要关注业务逻辑,比如在OfficeSupplyApplyService中重写createApply()方法,添加库存校验:
public void createApply(OfficeSupplyApply apply) {
// 校验每种物品库存是否充足
for (OfficeSupplyItem item : apply.getItems()) {
Integer stock = inventoryService.getStock(item.getName());
if (stock < item.getCount()) {
throw new BusinessException("物品【" + item.getName() + "】库存不足,当前剩余:" + stock);
}
}
// 调用父类save()方法保存
super.save(apply);
}
3.2 前端表单与流程配置实战
进入front工程,执行:
npm run generate:form -- --module office-supply --schema ./meta/office-supply.json
这个脚本会:
- 在src/views/office-supply/apply.vue生成表单组件
- 在src/router/modules/office-supply.ts添加路由
- 在src/store/modules/office-supply.ts生成状态管理
打开生成的apply.vue,你会看到:
<template>
<FormRenderer
:schema="schema"
:data="formData"
@submit="handleSubmit"
/>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { FormRenderer } from '@/packages/form-renderer'
import schema from '@/meta/office-supply.json'
const formData = ref({})
const handleSubmit = (values: any) => {
// 调用API提交申请
api.officeSupply.apply(values).then(res => {
// 启动审批流程
api.workflow.start({
processKey: 'office-supply-approval',
businessKey: res.applyId,
variables: { applicant: values.applicant }
})
})
}
</script>
流程配置更简单:在后台管理界面,进入“流程设计器”,上传office-supply.bpmn20.xml,拖拽三个节点:Start Event → User Task(部门经理审批)→ End Event。在User Task的“Assignee”属性中输入${assignee},然后在流程启动时传入variables.assignee = 'dept-manager'。
3.3 移动端适配与离线能力增强
c-mobile工程里,pages/office-supply/apply.vue会自动同步front的表单配置。但移动端需要额外优化:
- 离线表单缓存:在
onLoad生命周期中,检查uni.getStorageSync('offline-forms')是否有缓存,有则优先渲染; - 拍照上传压缩:调用
uni.chooseImage()后,用uni.compressImage()将图片压缩至宽度800px,大小<500KB; - 审批签名:使用
<canvas>组件实现手写签名,签名轨迹序列化为Base64存入apply.signature字段。
最关键的是离线审批逻辑。当网络断开时,用户仍可点击“同意”,此时c-mobile将审批动作存入本地SQLite:
// 存入本地待同步队列
const pendingApproval = {
applyId: 'APPLY_2023001',
action: 'APPROVE',
timestamp: Date.now(),
signature: 'data:image/png;base64,iVBORw0KGgo...'
}
uni.setStorageSync('pending-approvals', [...pendingApprovals, pendingApproval])
网络恢复后,App.vue的onNetworkStatusChange监听器会自动调用syncPendingApprovals()批量提交。
3.4 权限配置与多租户隔离
最后一步是权限绑定。登录后台管理,进入“角色管理”:
- 创建角色“行政专员”,分配菜单权限:办公用品申领、库存管理
- 分配操作权限:office-supply:approve、office-supply:ship
- 设置数据权限:dept_id IN (SELECT dept_id FROM sys_user_dept WHERE user_id = ?),其中?为当前用户ID
多租户支持通过TenantContext实现。在application.yml中配置:
tenant:
mode: DATABASE_PER_TENANT # 或 SCHEMA_PER_TENANT
default-tenant: oa-default
所有SQL查询自动追加WHERE tenant_id = 'oa-default',无需修改业务代码。
4. 常见问题与排查技巧实录
4.1 表单渲染空白或字段错乱
这是新手最常遇到的问题,90%源于JSON Schema格式错误。排查步骤:
-
检查Schema语法:用JSON Schema Validator在线验证,重点看
properties是否为对象而非数组,required字段是否在properties中定义。 -
查看浏览器Console:如果出现
Cannot read property 'type' of undefined,说明某个字段的type缺失。form-renderer要求每个字段必须有type,即使type: "null"也要显式声明。 -
检查字段名冲突:Vue3的
ref()对变量名敏感。如果Schema中字段名为class、for等HTML保留字,渲染器会报错。解决方案:在c-core的SchemaNormalizer中自动将保留字后缀加_field(如class→class_field)。
独家技巧:在
front的main.ts中加入调试开关:
ts import { enableFormDebug } from '@/packages/form-renderer/debug' if (import.meta.env.DEV) { enableFormDebug() // 开启后,每个表单字段旁显示schema路径 }
4.2 流程审批卡在某个节点不动
Flowable流程卡顿通常有三个原因:
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
TaskQuery.count()返回0,但流程实例状态为ACTIVE | 任务分配表达式错误,无人可领取 | SELECT * FROM ACT_RU_TASK WHERE PROC_INST_ID_ = 'xxx' |
HistoryService.createHistoricProcessInstanceQuery()查不到历史记录 | 流程未正确结束,或historyLevel配置过低 | SELECT * FROM ACT_HI_PROCINST WHERE PROC_INST_ID_ = 'xxx' |
RuntimeService.getActiveActivityIds()返回空数组 | 流程定义未部署,或BPMN文件有语法错误 | SELECT * FROM ACT_RE_PROCDEF WHERE KEY_ = 'xxx' |
最有效的排查方式是启用Flowable日志:
logging:
level:
org.flowable: DEBUG
org.flowable.engine.impl.persistence.entity: TRACE
然后观察DEBUG日志中Executing query后的SQL,确认参数是否正确。
4.3 移动端uni-app白屏或样式错乱
uni-app的坑主要在编译配置。常见问题:
- CSS变量不生效:H5端支持CSS变量,但小程序端不支持。解决方案:在
vue.config.js中配置css.loaderOptions.css,将CSS变量转为静态值。 - Canvas签名在iOS白屏:iOS Safari对
toDataURL()有尺寸限制。解决方案:签名时限制画布尺寸为300x150,导出前调用ctx.scale(0.5, 0.5)缩小图像。 - 离线数据同步失败:
uni.uploadFile()在弱网环境下超时。解决方案:在utils/request.ts中增加重试机制,retry: 3,且每次重试间隔递增。
4.4 数据权限失效导致越权访问
数据权限失效往往悄无声息。验证方法:
- 创建测试用户A,分配角色“销售代表”,数据权限限定为
dept_id = 101 - 创建测试用户B,分配角色“销售总监”,数据权限为
dept_id IN (101,102) - 用用户A登录,访问
/api/crm/customer/list,检查返回数据是否只含dept_id=101的客户 - 用用户B登录,访问同一接口,检查是否返回
dept_id=101和102的数据
如果失效,检查DataPermissionInterceptor是否被其他拦截器(如JWT认证拦截器)提前终止。确保其order值为Ordered.HIGHEST_PRECEDENCE + 10,高于所有认证拦截器。
经验总结:我们给客户做培训时,总会强调一句:“低代码不是无代码,而是把重复劳动标准化。你拖拽生成的,是80%的CRUD;剩下20%的业务规则,必须由懂业务的人来写。”这套框架的价值,不在于让你少写多少行代码,而在于把开发者的精力,真正聚焦在解决客户的核心痛点上——比如如何让合同审批更合规,而不是纠结于表格分页怎么实现。
简介:开箱即用的企业协同办公开发框架,后端用SpringBoot,前端用Vue3+TypeScript,内置可视化低代码平台。不用写基础代码,靠拖拽表单、配置审批流程、设置角色权限,就能生成人事管理、客户关系(CRM)、电子合同审批、项目进度跟踪、办公用品申领等业务系统。提供完整源码,含标准RESTful接口、RBAC权限体系、Activiti/Flowable工作流引擎、站内信与邮件通知、多格式文件上传下载、uni-app封装的移动端适配方案,以及本地mock数据模拟能力。工程结构清晰:c-core(核心组件)、c-main(主服务)、c-mobile(移动端)、front(前端工程),配套详细安装说明、README文档和类型定义(types)、静态资源(static)、工具函数(utils)、状态管理(store)等模块,方便二次开发和私有化部署。

1950

被折叠的 条评论
为什么被折叠?



