--- name: jeecg-codegen description: Use when user asks to generate JeecgBoot CRUD code, create a new module, add/modify fields on existing module, or says "代码生成", "生成代码", "创建模块", "新增功能", "建表", "加字段", "加一个字段", "增加字段", "新增字段", "修改字段", "删除字段", "generate code", "new entity", "add field" --- # JeecgBoot 代码生成器 将自然语言需求转换为 JeecgBoot 全套 CRUD 代码(后端 Java + 前端 Vue3 + 菜单权限 SQL),并支持对已生成模块的增量字段修改。 ## 主数据复用规则 > **重要:** 生成代码涉及的字典、角色、用户、部门等主数据,必须遵循"先查后建"原则。 > 使用 `jeecg-system` skill 的 `system_utils.py` 查询和管理主数据。 > 详见 `../jeecg-system/SKILL.md`。 ### ⛔ 字典创建必须写入 Flyway SQL,禁止直接走 API 创建 > **代码生成场景下,新建字典的"建"必须落到 Flyway SQL 文件,禁止调用 `find_or_create_dict()` / `create_dict()` 等 API 在远程服务器上直接创建。** > > **Why:** 代码生成产物(Entity、前端、Flyway SQL)会通过 git 提交并部署到测试/预发/生产环境。如果字典只通过 API 在当前开发环境创建,**部署到线上时线上数据库没有该字典**,前端下拉框会空白、列表 `_dictText` 翻译失效。Flyway SQL 跟着代码走,所有环境拉到代码后执行迁移都会自动建上,是唯一能保证环境一致性的方式。 > > **How to apply:** > - 查询字典 → **走 API 或 MySQL**(`jeecg-system` skill 的 `query-dicts` / `query-dict`),用于"先查后建"中的"查" > - 创建字典 → **写入当次的 Flyway SQL 文件**(`sys_dict` INSERT + `sys_dict_item` 批量 INSERT),用于"先查后建"中的"建" > - **禁用**:`find_or_create_dict()`、`create_dict()` 等 system_utils 中的字典创建函数(在代码生成 skill 中不能调用) > - 同理适用于:分类字典 `sys_category` 节点新建 — 也必须写入 Flyway SQL,禁止走 `/sys/category/add` API > > **例外:** 角色、审批角色、用户绑定关系等"运行时主数据",由于跨业务可复用,可走 API 创建(按 jeecg-system 原流程)。**字典与分类字典是"配置数据",必须走 SQL。** ## ⛔ 接口禁止猜测规则 > **严格禁止猜测任何 API 接口路径或参数。** AI 不得根据命名惯例、框架约定或已知路径拼凑接口地址后直接调用。 > > 所有接口调用必须来源于以下之一: > 1. 用户明确提供的接口文档或地址 > 2. `jeecg-system` skill 中已记录的接口 > 3. 通过 `jeecg-system` skill 查询后确认的接口 > > 违反此规则即使偶然成功也视为错误操作,因为猜测成功不代表行为合规。 ## ⛔ 写文件前的强制自检清单(高频翻车点) > **以下两条是 AI 凭"框架直觉"最常犯错的地方,文件写出去几乎必现 bug,调试成本极高。每次执行 Step 4 写后端/前端文件之前,必须逐条 self-check。** > > ### 翻车点 1:每个文件的路径必须与 SKILL/reference 描述完全一致 > > **写每一个文件之前,必须先在 `codegen-reference.md` 顶部"文件清单"章节中找到对应文件的路径模板,逐字符比对后再写入。禁止凭"Spring Boot/JeecgBoot 框架直觉"猜测路径。** > > ### 翻车点 2:FormSchema 必含隐藏 id 字段 > > **所有** FormSchema(主表 Modal 表单、一对一子表 Form、ERP 风格子表 Form、树表 Modal 表单)**首位**必须包含: > > ```typescript > { label: '', field: 'id', component: 'Input', show: false }, > ``` > > **为什么这是铁律:** `BasicForm` 的 `getFieldsValue()` 只返回 schema 中声明过的字段。即使 Modal 打开时通过 `setFieldsValue({ ...data.record })` 把 `id` 写入了表单状态,schema 没声明,提交时 `getFieldsValue()` 也会丢弃它。最终后端收到 `entity.id == null`,`getById(null)` 返回 null,Controller 返回 `Result.error("未找到对应数据")`。**编辑功能直接报错。** > > **位置统一规定:放在 FormSchema 数组首位**(不要纠结"最后还是最前",统一首位)。 > > 这两条规则不需要用户询问、不需要场景判断、不需要选项确认。**100% 强制,100% 一致。** ## 生成模式 > **进入交互流程之前,必须先与用户确认本次使用的生成模式。** 任何场景下都不要默默选择,必须显式告知用户当前模式;用户回复"确认"即采用默认。 本 skill 提供两种生成模式: | 模式 | 状态 | 默认 | 说明 | |------|------|------|------| | **串行生成(Serial)** | Stable | ✅ 默认 | 主 Agent 顺序生成后端 → 前端 → SQL,全程单线执行,稳定可靠 | | **并行生成(Parallel)** | ⚠️ **Beta — 可能不稳定** | ❌ | 派发两个 SubAgent 并行生成前端 / 后端代码,主 Agent 负责契约冻结与跨端校验。详见同目录下 `parallel-generation-mode.md` | ### 模式确认话术(必须执行) 进入 Step 0 之前,主 Agent **必须**先输出类似以下消息,等用户回复后再继续: ``` 本次代码生成将使用【串行生成模式】(默认,稳定)。 如需使用【并行生成模式(Beta)】以缩短耗时,请明确告知。 注意:并行模式当前为 Beta 版本,可能出现前后端字段命名漂移、API URL 不一致、 字典编码错位、FormSchema 隐藏 id 字段遗漏等问题,不确定时建议使用默认串行模式。 ``` ### 模式选择规则 - 用户未明确要求并行 → 一律走 **串行模式**,不要主动建议并行。 - 用户明确要求并行("并行"、"分头生成"、"前后端同时来"、"用 subagent 并行"等关键词) → 进入 **并行模式**,但必须先复述一遍 Beta 风险并等用户**再次确认**后才正式启动。 - **增量字段修改场景(场景 C)** → 强制串行,即使用户要求并行也要拒绝并解释(SubAgent 双重压缩会丢失已有代码细节)。 - 一对多 + ERP / vue3Native / 自定义增强等复杂场景 → 强烈建议串行,需向用户说明风险后由用户决定。 ### 并行模式启动条件(全部满足才进入) 1. 用户明确选择了并行模式。 2. 用户已被告知 Beta 风险并**再次确认**。 3. 操作类型是"全量生成"(场景 A 或 B),不是"增量修改"(场景 C)。 4. 主数据复用前置条件已就绪(字典已查/已建,目标数据库已确认)。 满足后,主 Agent **必须读取** `parallel-generation-mode.md` 并严格按其规范执行(契约冻结 → 派发 SubAgent → 跨端校验 → 输出清单)。任一环节失败 → 按该文档第 6 节"回退策略"切回串行从头来过。 > **铁律不变:** 即使选择并行模式,本章上方的"⛔ 接口禁止猜测"、"⛔ 字典创建必须写入 Flyway SQL"、"⛔ 写文件前的强制自检清单(路径 + FormSchema id)"、以及"⛔ 铁律:Step 2 + Step 3 是不可跳过的硬性停止门" 全部仍然 100% 强制 —— 通过派发 prompt 传达给 SubAgent。 ## 交互流程 > ### ⛔ 铁律:Step 2 + Step 3 是不可跳过的硬性停止门 > > **全量生成必须严格按顺序执行 Step 0 → Step 1 → Step 2 → 等用户回复 → Step 3 → 等用户确认 → Step 4。** > 在用户明确回复"确认"(或等价表述)之前,**绝对禁止**开始生成任何代码、创建任何文件、执行任何 SQL。 > > **以下念头出现时立刻停下,它们都是合理化跳过确认的借口:** > > | 借口 | 现实 | > |------|------| > | "需求描述足够清楚,可以直接推断" | 用户没有确认 ≠ 用户已认可。字段类型、路径、风格都可能偏差。 | > | "选项都是默认值,不需要问" | 默认值是否适用由用户决定,不由 AI 决定。 | > | "先生成再改很方便" | 用户不得不事后检查所有文件,浪费双方时间。 | > | "用户说'Tab风格'已经隐含了风格选择" | 只说明了一个选项,其他9项仍需展示给用户确认。 | > | "Skill 加载太慢,直接生成更高效" | 效率不是跳过确认的理由。 | > > **违反此铁律的代价:** 用户发现问题后,所有已生成文件都需要重新生成或逐一修改。 ### Step 0 前置:判断前端目标(PC 端 / 移动端 / 两者都要) > **此步骤必须在 Step 0 之前执行。前端目标直接决定 Step 2 中需要询问哪些选项。** **识别移动端关键词:** "移动端"、"手机端"、"UniApp"、"uniapp"、"APP端"、"小程序"、"H5"、"移动页面"、"APP页面" 根据用户描述判断: | 用户意图 | 判定结果 | Step 2 调整 | |---------|---------|------------| | 明确只要移动端(含以上关键词,无 PC 相关词) | **仅移动端** | 跳过 PC 前端选项(选项2、3、6、8、9),改为询问 UniApp3 项目根路径 | | 明确只要 PC 端(含 "vue3"、"PC端"、"web端" 等词,无移动端词) | **仅 PC 端** | 按原流程 | | 两者都提到,或描述模糊(如"前后端代码"、"CRUD代码") | **不确定** | 在 Step 2 前先询问用户:**"请问需要生成哪端的前端代码?① 仅 PC 端(Vue3) ② 仅移动端(UniApp3) ③ 两者都要"** | **仅移动端时的 Step 2 选项调整:** - 删除:前端风格(vue3/vue3Native) - 删除:PC 前端视图目录 - 删除:PC 前端项目根路径 - 删除:一对多布局风格(PC 端特有) - 删除:表单列数(PC 端特有) - 新增:**UniApp3 项目根路径**(必填) - 保留:后端模块、是否读取系统字典、后端项目根路径、数据库名称 **两者都要时:** 同时展示 PC 端和移动端的选项,分两组列出。 > ⚠️ **禁止默认跳过任何一端!** 无法判断时必须询问用户,不得自行假设"用户可能只要 PC"或"用户只要移动端"。 --- ### Step 0: 判断操作类型 — 全量生成 or 增量修改? **识别增量修改的关键词:** "加字段"、"增加字段"、"新增字段"、"加一个XX字段"、"删除字段"、"修改字段"、"改一下XX"、"给XX模块加"、"给XX表加" 如果是增量修改 → 进入 **场景C** 如果是全量生成 → 进入 **场景A** 或 **场景B** ### Step 1: 全量生成 — 判断场景 **场景A — 已有表(用户给了表名):** 1. 通过数据库查询获取精确 DDL(见"数据库连接"章节) 2. 从 DDL 中解析:主键类型、全部字段(名称/类型/注释/是否nullable)、是否有系统字段 3. 根据字段类型和注释自动推导前端控件类型 4. 用户无需描述字段,AI 全部自动推导 **场景B — 新建表(用户用自然语言描述需求):** 1. 从用户描述中提取:表名、实体名、功能描述、字段列表 2. 用"智能字段推导"规则推导 DB 类型和前端控件 3. 默认添加全部系统字段(create_by/create_time/update_by/update_time/sys_org_code) 4. 生成建表 DDL 写入 Flyway SQL **场景C — 增量修改(给已有模块加/改/删字段):** 1. **定位目标模块**:从用户提到的表名、模块名、实体名中识别目标 2. **扫描已有代码文件**:在后端和前端目录中搜索已生成的文件 ```bash # /:后端/前端项目根目录,使用前需向用户确认 # 搜索后端 Entity 文件 find -name "{EntityName}.java" -path "*/entity/*" # 搜索前端 data.ts 文件 find /src/views -name "{EntityName}.data.ts" ``` 3. **读取全部已有文件**:Entity.java、*.data.ts、*List.vue、*Modal.vue(如有 Form.vue 也读取) 4. **解析当前字段列表**:从 Entity.java 解析已有字段 5. **推导新字段属性**:用"智能字段推导"规则推导 DB 类型、Java 类型、前端控件 6. **展示修改摘要**,等待用户确认后再修改 **增量修改的操作类型:** - **加字段**:在所有文件中追加新字段定义 - **删字段**:从所有文件中移除指定字段定义 - **改字段**:修改指定字段的类型、控件、注释等 **判断表类型:** - 提到"分类/层级/树/上下级" → **树表** - 提到"主子表/明细/一对多/订单+商品" → **一对多** - 默认 → **单表** **全控件生成模式("全控件"关键词触发):** 当用户说"全控件"、"覆盖所有控件类型"时,触发全覆盖枚举模式,**每张表都必须包含该场景支持的所有组件类型**,不得只生成代表性字段: - **主表**:枚举全部 FormSchema 组件 — Input/InputPassword/InputTextArea/InputNumber(整数+金额)/JDictSelectTag(下拉+radio)/JCheckbox/JSelectMultiple/JSwitch/DatePicker(5个picker变体)/TimePicker/JSelectUser/JSelectDept/JCategorySelect/JTreeSelect/JImageUpload/JUpload/JPopup+回填/JPopupDict/JAreaLinkage - **一对一子表**:在主表全部控件基础上额外加 JEditor/JMarkdownEditor/联动组件(多级)/关联记录+他表字段/表字典各变体(radio/checkbox/multi/带条件) - **一对多子表**:枚举全部 JVxeTypes — input/textarea/inputNumber/select(系统字典+表字典)/selectSearch/selectMultiple/checkbox(开关)/date/datetime/time/image/file/popup/departSelect/userSelect/pca - **标准触发词**:`全控件`、`覆盖所有 FormSchema 控件`、`覆盖所有 JVxeTypes` - **标准提示语**(用户可直接复制使用): > 生成全控件主子表,主表+一对一子表覆盖所有 FormSchema 控件,一对多子表覆盖所有 JVxeTypes(含pca),Tab-in-Modal 风格(radio-group 切换) **一对多表的前端布局风格:** > ⚠️ **严禁假设布局风格!** 必须在 Step 2 询问用户,用户未回答前不得擅自选择非默认风格(如 Tab-in-Modal)。 > 过去曾犯错:用户未说明风格,却错误地选了 Tab-in-Modal (C9),导致用户反馈后需要重新生成 Modal.vue。 一对多表有三种前端布局风格,用户未指定时**默认使用原始布局风格**。 > **重要:vue3 封装风格和 vue3Native 原生风格的一对多架构完全不同!** vue3 封装风格使用 `useJvxeMethod`,vue3Native 原生风格使用 `useValidateAntFormAndTable`。详见 `codegen-reference.md` 的 C9-C12(vue3)和 **C13(vue3Native)**。 **vue3 封装风格布局选项:** | 风格 | 关键词 | 列表页 | Modal 布局 | |------|--------|--------|-----------| | **默认/原始布局** | "默认风格"、"默认"、未指定风格 | 标准列表(无 expandedRowRender) | 上面主表 BasicForm + 下面 a-tabs 子表 | | **Tab-in-Modal (C9)** | "tab风格"、"tab切换"、"radio切换"、"标题栏切换" | 标准列表(同默认,**无** expandedRowRender) | radio-group 标题栏切换主表/子表,`wrapClassName="j-cgform-tab-modal"` | | **内嵌子表 (C12)** | "内嵌子表"、"行展开"、"expandedRowRender" | 行展开显示子表(expandedRowRender) | 上面主表 BasicForm + 下面 a-tabs 子表(同默认) | | **ERP (C11)** | "ERP风格"、"独立编辑" | 主表单选 + 子表独立 CRUD Tab | 仅主表 BasicForm(子表独立 Modal) | > ⚠️ **子表外键字段名必须读实体确认,严禁猜测!** > 生成子表 FormSchema 的隐藏外键字段前,**必须先 Read 子表 Entity.java**,以实体中的 Java 字段名为准。 > 外键字段名因开发者习惯差异很大(`companyId` / `bizCompanyId` / `mainId` / `headerId`), > 根据主表实体名推断必然出错,会导致 MySQL `Field 'xxx' doesn't have a default value` 异常。 > 同样,Modal 中 `values.xxx = unref(mainId)` 的 `xxx` 也必须与实体字段名一致。 **vue3Native 原生风格(C13)— 架构完全不同:** - **Modal 是薄包装器**(BasicModal + useModalInner),只调 `formComponent.submitForm()/edit()/add()` - **Form.vue 是核心组件**,包含主表 a-form + 子表 a-tabs + 提交逻辑 - 使用 **`useValidateAntFormAndTable`** hook(不是 `useJvxeMethod`) - 子表 API 导出为**函数**(不是 URL 字符串) - `saveOrUpdate` **不用** `isTransformResponse: false` - 一对一子表用原生 `a-form` + `Form.useForm`,暴露 `isForm = true` - 一对一子表 `initFormData(mainId)` 直接传主表 ID(不传 URL 字符串) - 一对一子表 `getFormData()` 返回对象(不是数组) - 需要额外的 `queryDataById` API 函数 - List.vue 使用 `useModal` + `openModal(true, {...})` 模式 **vue3 封装风格 — 默认/原始布局的关键特征:** - **Modal 结构**:BasicForm(主表)始终显示在上方 + `` 包裹子表在下方 - **无** `wrapClassName="j-cgform-tab-modal"`,**无** `#title` 插槽的 radio-group - **`refKeys` 只包含子表 key**(不包含主表 key),如 `['subMany', 'subOne']` - 一对多子表用 ``,一对一子表抽成独立 Form.vue 组件(**必须用 `defineComponent`,不能用 ` ``` **规则24.4:内嵌子表风格 — Modal 必须使用 `useJvxeMethod` 6参数模式** 内嵌子表 (C12) 的 Modal 与默认/原始布局风格完全一致,同样使用 `useJvxeMethod` 的 6 参数模式。**不要自己编写 `handleSubmit`**,`useJvxeMethod` 返回的 `handleSubmit` 已内置表单校验、子表数据收集、`validateSubForm` 调用等完整逻辑。 ```typescript // Modal.vue 关键结构 const refKeys = ref(['demoProjBudget', 'demoProjTask']); // 只有子表 key,不含主表 const activeKey = ref('demoProjBudget'); const demoProjBudgetForm = ref(); // 一对一子表 Form ref const demoProjTask = ref(); // 一对多子表 JVxeTable ref const tableRefs = { demoProjTask }; // ⚠️ 只包含 JVxeTable ref! const demoProjTaskTable = reactive({ loading: false, dataSource: [], columns: demoProjTaskJVxeColumns }); // ✅ 6参数调用 useJvxeMethod const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod( requestAddOrEdit, classifyIntoFormData, tableRefs, activeKey, refKeys, validateSubForm ); // classifyIntoFormData — 组装提交数据 function classifyIntoFormData(allValues) { let main = Object.assign({}, allValues.formValue) return { ...main, demoProjBudgetList: demoProjBudgetForm.value.getFormData(), // 一对一:调用 Form 的 getFormData() demoProjTaskList: allValues.tablesValue[0].tableData, // 一对多:从 tablesValue 取(index 对应 tableRefs 中的顺序) } } // validateSubForm — 校验所有一对一子表 function validateSubForm(allValues) { return new Promise((resolve, reject) => { Promise.all([ demoProjBudgetForm.value.validateForm(0), // index 对应 refKeys 中的位置 ]).then(() => { resolve(allValues) }).catch(e => { if (e.error === VALIDATE_FAILED) { activeKey.value = e.index == null ? unref(activeKey) : refKeys.value[e.index] if (e.errorFields) { const firstField = e.errorFields[0]; if (firstField) { e.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' }); } } } else { console.error(e) } }) }) } // Modal 打开时加载子表数据 const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => { await reset(); setModalProps({ confirmLoading: false, showCancelBtn: data?.showFooter, showOkBtn: data?.showFooter }); isUpdate.value = !!data?.isUpdate; formDisabled.value = !data?.showFooter; if (unref(isUpdate)) { await setFieldsValue({ ...data.record }); // 一对一子表:调用 Form 的 initFormData(传 URL 字符串 + 主表 id) demoProjBudgetForm.value.initFormData(queryDemoProjBudgetByMainId, data?.record?.id); // 一对多子表:调用 requestSubTableData(传 URL 字符串 + 参数 + reactive 表对象) requestSubTableData(queryDemoProjTaskByMainId, { id: data?.record?.id }, demoProjTaskTable); } setProps({ disabled: !data?.showFooter }); }); // reset — 重置所有表单和子表数据 async function reset() { await resetFields(); activeKey.value = 'demoProjBudget'; demoProjBudgetForm.value.resetFields(); // 一对一子表重置 demoProjTaskTable.dataSource = []; // 一对多子表清空数据 } ``` **规则24.5:内嵌子表风格 — api.ts 双导出模式(URL字符串 + API函数)** 每个子表需要两种导出方式,分别供 Modal 和 SubTable 使用: ```typescript // api.ts enum Api { // ... demoProjBudgetList = '/demo/demoProj/queryDemoProjBudgetByMainId', demoProjTaskList = '/demo/demoProj/queryDemoProjTaskByMainId', } // 1. URL 字符串导出 — 供 Modal 中 requestSubTableData / initFormData 使用 // 这些函数内部用 defHttp.get 默认 isTransformResponse:true,拿到的是 result 部分 export const queryDemoProjBudgetByMainId = Api.demoProjBudgetList; export const queryDemoProjTaskByMainId = Api.demoProjTaskList; // 2. 函数导出 — 供 SubTable 组件使用,isTransformResponse:false 返回完整 {success, result, ...} export const demoProjBudgetListApi = (params) => defHttp.get({ url: Api.demoProjBudgetList, params }, { isTransformResponse: false }); export const demoProjTaskListApi = (params) => defHttp.get({ url: Api.demoProjTaskList, params }, { isTransformResponse: false }); ``` **为什么需要两种?** - Modal 中 `requestSubTableData(url, params, tableObj)` 内部执行 `defHttp.get({url, params})` 默认 `isTransformResponse:true`,拿到的直接是 `result` 对象(即 IPage),然后取 `res.records || res` 赋值给 `tableObj.dataSource` - Modal 中 `initFormData(url, id)` 内部执行 `defHttp.get({url,params:{id}},{isTransformResponse:false})`,拿到完整响应,通过 `res.result.records[0]` 取第一条数据 - SubTable 中 `xxxListApi({id})` 用 `isTransformResponse:false`,通过 `res.result.records` 取数据数组 **规则25:Tab-in-Modal 风格 — Modal 必须用 radio-group 标题栏,不是 a-tabs** Tab-in-Modal 风格的 Modal 使用 `a-radio-group` + `a-radio-button` 在标题栏切换主表/子表区域,各区域用 `v-show` 显隐(不是 `a-tab-pane`)。必须设置 `wrapClassName="j-cgform-tab-modal"` 启用专属样式,`contentArea` div 包裹所有表单区域。 ```html
``` **规则26:Tab-in-Modal 风格 — 一对一子表必须抽成独立 Form.vue 组件** 一对一子表不能用内联 BasicForm(`useForm` + `registerDetailForm`),必须抽成独立的 Vue 组件(如 `XxxForm.vue`),使用 Options API(`defineComponent`)暴露以下方法: - `initFormData(url, id)` — 加载数据,内部用 `defHttp.get({url,params:{id}},{isTransformResponse:false})` - `getFormData()` — 返回 `[formData]` 数组(后端用 `List` 接收) - `validateForm(index)` — 校验表单,`index` 对应 `refKeys` 数组位置 - `resetFields()` — 重置表单 **规则27:Tab-in-Modal 风格 — useJvxeMethod 第6个参数 validateSubForm** 有一对一子表时,`useJvxeMethod` 必须传入第6个参数 `validateSubForm`,该函数在 `handleSubmit` 中自动调用。不需要自写 `handleSubmit`: ```typescript const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod( requestAddOrEdit, classifyIntoFormData, tableRefs, activeKey, refKeys, validateSubForm ); // validateSubForm 内部调用所有一对一子表的 validateForm(index) ``` **规则28:Tab-in-Modal 风格 — refKeys 和 JVxeTable ref 名必须一致** `refKeys` 数组的值必须与 `v-show` 条件、JVxeTable 的 `ref` 名、`activeKey` 的值完全一致。`refKeys[0]` 固定为主表 key。 ```typescript const refKeys = ref(['mainKey', 'subManyKey', 'subOneKey']); // JVxeTable: ref="subManyKey" v-show="activeKey == 'subManyKey'" // Form: ref="subOneForm" v-show="activeKey == 'subOneKey'" ``` **规则29:树表 — Mapper 接口参数名必须与 XML 参数名一致** 树表 Mapper 接口和 XML 的参数命名必须严格对齐,参考 JeecgBoot 代码生成器的标准输出: ```java // Mapper 接口 void updateTreeNodeStatus(@Param("id") String id, @Param("status") String status); List queryListByPid(@Param("pid") String pid, @Param("query") Map query); ``` ```xml update {{tableName}} set has_child = #{status} where id = #{id} ``` **规则30:树表 — Modal 的 updateSchema 禁止传递 treeData** JTreeSelect 组件通过 `dict` 配置(如 `"demo_category,category_name,id"`)自行从后端加载树数据。`updateSchema` 只需传递 `hiddenNodeKey`,**禁止**传递 `treeData` 到 componentProps,否则会导致下拉框显示异常(label 变 value、内容不全)。 ```typescript // ❌ 错误 — 传递 treeData 会干扰 JTreeSelect 内部数据管理 updateSchema([{ field: 'pid', componentProps: { treeData, hiddenNodeKey: data.record.id } }]); // ✅ 正确 — 只传 hiddenNodeKey updateSchema([{ field: 'pid', componentProps: { hiddenNodeKey: data.record.id } }]); ``` `treeData` 变量仅用于 Modal 内部的 `getExpandKeysByPid` 函数(计算展开路径),不用于 JTreeSelect 组件。 **规则31:树表 — handleSuccess 必须遵循参考代码的刷新策略** 树表 `handleSuccess` 回调接收 `{isUpdate, values, expandedArr, changeParent}` 四个参数,刷新策略如下: ```typescript async function handleSuccess({isUpdate, values, expandedArr, changeParent}) { if (isUpdate) { if (changeParent) { reload(); // 父节点变更,全量刷新 } else { // 父节点未变,重新查询单条记录(含 _dictText 翻译) let data = await list({ id: values.id, pageSize: 1, pageNo: 1, pid: values['pid'] }); if (data && data.records && data.records.length > 0) { updateTableDataRecord(values.id, data.records[0]); } else { updateTableDataRecord(values.id, values); } } } else { if (!values['id'] || !values['pid']) { reload(); // 新增根节点,全量刷新 } else { // 新增子节点,逐级展开 expandedRowKeys.value = []; // ← 必须先清空! for (let key of unref(expandedArr)) { await expandTreeNode(key); } } } } ``` 关键点: 1. 编辑后必须**重新查询**单条数据(`list({id,...})`),不能直接用 form values 更新,否则字典列不显示翻译文本 2. 新增子节点前必须**先清空 `expandedRowKeys`**(`expandedRowKeys.value = []`),否则展开逻辑出错 3. 新增根节点判断条件是 `!values['id'] || !values['pid']`(form 提交后 id 为空因为是后端自动生成) **规则32:树表 — getDataByResult 的 loading 占位节点必须使用正确的显示字段** `getDataByResult` 为有子节点的数据添加 loading 占位,占位节点的显示字段名必须与表的树节点主显示列一致: ```typescript // 如果 columns 第一列 dataIndex 是 'categoryName' let loadChild = { id: item.id + '_loadChild', categoryName: 'loading...', isLoading: true } // 如果 columns 第一列 dataIndex 是 'name' let loadChild = { id: item.id + '_loadChild', name: 'loading...', isLoading: true } ``` 使用错误的字段名(如统一用 `name`)会导致 loading 文本不显示。 **规则33:树表 — 前端代码必须完全对齐参考代码结构** 树表前端与单表有根本性差异,生成时必须严格按以下清单: **List.vue 必须包含:** - `isTreeTable: true` in tableProps - `expandedRowKeys` ref + `@expand="handleExpand"` + `@fetch-success="onFetchSuccess"` - `beforeFetch` 必须添加 `params.hasQuery = "true"` - `queryParam` reactive + `superQueryConfig` + 高级查询组件 - `v-auth` 权限指令在所有操作按钮上 - `getTableAction` 主操作(编辑 + 添加下级)+ `getDropDownAction` 下拉操作(详情 + 删除) - `actionColumn` 必须设置 `width: 240` 和 `fixed: 'right'` - `handleExpand` 中必须处理 `result.records`:`result = result.records ? result.records : result` - `handleAddSub` 传递 `{pidField: record.id}` 作为 record(pidField 即 pid 字段名) - `batchHandleDelete` 必须过滤 loadChild 占位:`ids.filter(item => !item.includes('loadChild'))` **Modal.vue 必须包含:** - `let model: Nullable = null` 保存编辑前数据 - `treeData` ref 用于 `getExpandKeysByPid`(不传给 JTreeSelect) - `setModalProps({ showOkBtn: !!!data?.hideFooter })` 详情模式隐藏确认按钮 - `setProps({ disabled: !!data?.hideFooter })` 详情模式禁用表单 - `isDetail.value = !!data?.showFooter`(注意是 showFooter 不是 hideFooter) - 编辑时 `updateSchema` 只传 `hiddenNodeKey` - `handleSubmit` 中 `emit('success', {isUpdate, values, expandedArr, changeParent})` - `changeParent` 判断:`model != null && (model['pid'] != values['pid'])` - `scrollToField` 在 catch 块中处理校验错误定位 - `baseRowStyle: { padding: "0 20px" }` 表单行内边距 **data.ts 必须包含:** - `id` 隐藏字段放在 formSchema **最后** - `pid` 字段用 `JTreeSelect` 组件,配置 `dict/pidField/pidValue/hasChildField` - 树表主显示列的 `align` 必须是 `'left'`(便于显示层级缩进) - `superQuerySchema` 每项需包含 `type` 字段 **api.ts 必须包含:** - `rootList`(不是 `list`)、`childList`、`getChildListBatch`、`loadTreeData` 四个树表专用接口 - `getChildListBatch` 必须使用 `{isTransformResponse: false}` 返回完整 Result 对象 **规则33.1:树表 — pid 字段改名后,前端所有 `pid` 引用必须同步替换为实际 camelCase 字段名** 树表模板默认以 `pid` 作为父节点字段名。**当 DB 字段不叫 `pid`(如 `parent_id`、`parent_node` 等)时,前端所有硬编码的 `'pid'` 必须替换为对应的 Java camelCase 名称(如 `parentId`)**。否则 `QueryGenerator` 找不到匹配字段,过滤条件被忽略,接口返回全表数据。 **必须替换的位置(以 `parent_id` → `parentId` 为例):** | 文件 | 原代码 | 替换为 | |------|--------|--------| | List.vue `handleExpand` | `getChildList({ pid: record.id })` | `getChildList({ parentId: record.id })` | | List.vue `expandTreeNode` | `getChildList({ pid: key })` | `getChildList({ parentId: key })` | | List.vue `handleAddChild` | `{ pid: record.id }` | `{ parentId: record.id }` | | List.vue `handleSuccess` | `pid: values['pid']` / `!values['pid']` | `parentId: values['parentId']` / `!values['parentId']` | | Modal.vue `changeParent` | `model['pid'] != values['pid']` | `model['parentId'] != values['parentId']` | **根本原因:** `childList` 使用 `QueryGenerator.initQueryWrapper(entity, req.getParameterMap())`,参数名必须与 Entity 字段名(camelCase)完全匹配才能生成 WHERE 条件。传 `pid` 但 Entity 字段叫 `parentId`,条件被静默忽略,返回全表数据。 **生成后自查:** 搜索 List.vue、Modal.vue 中所有字面量 `'pid'`,逐一确认是否需要替换为实际父节点字段的 camelCase 名。 --- **规则34:vue3Native 单表 Form 默认单列布局,用户指定时按指定列数** 生成 vue3Native 单表的 `Form.vue` 时,**默认使用单列布局**: ```html ... ``` 仅当用户**明确说明列数**(如"双列"、"两列"、"2列"、"三列"等)时,才改变布局: | 用户指定 | `` | Modal 宽度 | |---------|----------------|-----------| | 单列(默认) | `:span="24"` | 800 | | 双列 / 两列 / 2列 | `:span="12"` | 1000 | | 三列 / 3列 | `:span="8"` | 1200 | | 四列 / 4列 | `:span="6"` | 1280 | **注意:** 同一表单中通常有例外字段用全宽(如多行文本 `remark`、富文本 `content`),这类字段无论整体几列都用 `:span="24"`。 --- **规则35:生成默认值时 formSchema 的 defaultValue 必须有实际意义** 当用户要求"生成默认值"时,formSchema 中每个控件的 `defaultValue` 必须是有意义的示例数据,**不能统一写 `''`(空字符串)**。空字符串在表单打开时什么都不显示,不能体现"默认值"的效果。 **各控件类型的默认值规则:** | 控件类型 | 默认值写法 | 示例 | |---------|-----------|------| | Input | 示例文本 | `'示例节点'` | | InputPassword | 示例密码 | `'Abc@12345'` | | InputTextArea | 示例文本 | `'这是一条默认备注内容'` | | InputNumber(金额/BigDecimal) | 示例金额 | `100.00` | | InputNumber(整数/排序) | 示例整数 | `1` | | JDictSelectTag(下拉) | 取查询到的字典**第一个选项**的值 | `'1'` | | JDictSelectTag(radio) | 同上,取第一个选项值 | `'1'` | | JCheckbox(多选) | 取前两个选项值逗号拼接 | `'reading,music'` | | JSelectMultiple(下拉多选) | 取第一个选项值 | `'1'` | | JSwitch(开关) | 开启状态 | `'1'` | | DatePicker(日期) | 当天日期字符串 | `'2026-04-28'` | | DatePicker(showTime/日期时间) | 当天日期 + 上班时间 | `'2026-04-28 09:00:00'` | | TimePicker(时间) | 上班时间 | `'09:00:00'` | | DatePicker(picker=quarter) | 当年 Q1 第一天 | `'2026-01-01'` | | DatePicker(picker=year) | 当年 1 月 1 日 | `'2026-01-01'` | | DatePicker(picker=month) | 当月 1 日 | `'2026-04-01'` | | DatePicker(picker=week) | 本周一日期 | `'2026-04-27'` | | JEditor(富文本) | 示例 HTML 片段 | `'

这里是默认的富文本内容示例

'` | | JMarkdownEditor | 示例 Markdown 文本 | `'## 默认标题\n\n这里是默认的 **Markdown** 内容示例。'` | **以下控件依赖系统实际数据,无法预设固定默认值,保持 `defaultValue: ''`:** JSelectDept、JTreeSelect(关联他表)、JImageUpload、JUpload **JPopup 例外:** 默认值设为 `'admin'`(系统内置管理员账号) **JPopupDict 例外:** 默认值设为 `'e9ca23d68d884d4ebb19d07889727dae'`(系统内置数据 ID) **JCategorySelect 例外:** 默认值设为 `'f39a06bf9f390ba4a53d11bc4e0018d7'`(系统内置分类节点 ID) **JSelectUser 例外:** 默认值设为 `'admin'`(系统内置管理员账号,必定存在) **JAreaLinkage 例外:** 使用北京市的 GB 区划码作为默认值:`defaultValue: '110105'`(北京市朝阳区) **日期字段的当天日期**:通过 `date +%Y-%m-%d` 获取真实日期后写入,不要硬编码过去的日期。 --- ### 智能字段推导中的字典关键词 当用户描述字段时,按以下关键词自动推导使用哪种字典: | 用户描述关键词 | 推导字典类型 | 推荐控件 | |--------------|------------|---------| | "状态"、"类型"、"级别"、"优先级" | 系统字典(先搜索 sys_dict 匹配) | JDictSelectTag | | "区域"、"地区"、"分类"、"类目"、"树形选择" | 分类字典(搜索 sys_category 匹配) | JCategorySelect | | "部门"、"组织"、"归属" | 表字典(关联 sys_depart) | JDictSelectTag(表字典) | | "用户"、"负责人"、"经办人" | 用户选择组件(非字典) | JSelectUser | ## 项目路径 > **重要:禁止通过 Glob/Bash/Grep 等工具主动搜索 CLAUDE.md 或项目目录!** > > 路径获取优先级(按顺序执行,不得跳步): > 1. **memory 中已有记录**:直接使用,无需任何文件读取 > 2. **当前目录下有 `CLAUDE.md`**:用 Read 工具直接读取该文件获取路径(不搜索,不 Glob) > 3. **以上均无**:在 Step 2 的选项表格中追加"后端项目根路径"和"前端项目根路径"两项,让用户手动填写 | 类别 | 路径 | |------|------| | 后端根 | 由 memory / CLAUDE.md / 用户提供 | | 前端根 | 由 memory / CLAUDE.md / 用户提供 | | 后端代码 | `{后端根}/{module}/src/main/java/org/jeecg/modules/{entityPackage}/` | | 前端代码 | `{前端根}/src/views/{viewDir}/` | | Flyway SQL | `{后端根}/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/` | ## 命名约定 - **表名**:snake_case(如 `biz_goods`) - **实体名**:表名转 PascalCase(如 `BizGoods`) - **entityPackage**:表名前缀或用户指定(如 `biz`) - **bussiPackage** 固定:`org.jeecg.modules` - **权限编码**:`{entityPackage}:{tableName}:add/edit/delete/deleteBatch/exportXls/importExcel` ## 智能字段推导 **用于新建表场景(从自然语言推导),或已有表但字段无注释时的补充推导:** | 语义关键词 | dbType | Java 类型 | vue3 组件 | vue3Native 组件 | |-----------|--------|----------|----------|----------------| | 名称/标题/编码 | varchar(100) | String | Input | a-input | | 金额/价格/费用 | decimal(10,2) | BigDecimal | InputNumber | a-input-number | | 数量/数目/个数 | int | Integer | InputNumber | a-input-number | | 状态/类型/级别 | varchar(10) | String | JDictSelectTag | JDictSelectTag | | 是否/开关 | varchar(2) | String | Switch | a-switch | | 日期/生日 | date | Date | DatePicker | a-date-picker | | 时间/日期时间 | datetime | Date | DatePicker(showTime) | a-date-picker(showTime) | | 备注/描述/说明 | text | String | InputTextArea | a-textarea | | 内容/富文本 | text | String | JEditor | JEditor | | 图片/头像/照片 | varchar(1000) | String | JImageUpload | JImageUpload | | 文件/附件 | varchar(1000) | String | JUpload | JUpload | | 用户/负责人 | varchar(32) | String | JSelectUser | JSelectUser | | 部门/组织 | varchar(32) | String | JSelectDept | JSelectDept | | 排序/序号 | int | Integer | InputNumber | a-input-number | **已有表场景的 DB类型→控件 映射(当字段无注释时使用):** | DB列类型 | Java类型 | 默认前端控件 | |---------|---------|-----------| | varchar(n) n<=200 | String | Input | | varchar(n) n>200 | String | InputTextArea | | text / longtext | String | InputTextArea | | int / tinyint | Integer | InputNumber | | bigint | Long | InputNumber | | decimal / double / float | BigDecimal | InputNumber | | date | Date | DatePicker | | datetime / timestamp | Date | DatePicker(showTime) | ## 主键策略(根据已有表结构自适应) | 表DDL中的主键定义 | Java类型 | @TableId | 说明 | |------------------|---------|----------|------| | `int AUTO_INCREMENT` | Integer | `@TableId(type = IdType.AUTO)` | int自增主键 | | `bigint AUTO_INCREMENT` | Long | `@TableId(type = IdType.AUTO)` | bigint自增主键 | | `varchar(36)` / `varchar(32)` 无AUTO_INCREMENT | String | `@TableId(type = IdType.ASSIGN_ID)` | JeecgBoot标准字符串主键 | | `bigint` 无AUTO_INCREMENT | Long | `@TableId(type = IdType.ASSIGN_ID)` | 雪花ID | **注意:** 当主键为 Integer/Long 类型时,Controller 中 `delete` 和 `queryById` 的参数类型也要对应调整。 ## 系统字段(按实际表结构判断) **不是所有表都有系统字段!** 生成前必须检查表是否实际包含这些字段,**只生成表中存在的字段**: | 字段 | 说明 | 不存在时的处理 | |------|------|--------------| | `create_by` | 创建人 | 不生成该属性 | | `create_time` | 创建时间 | 不生成该属性 | | `update_by` | 更新人 | 不生成该属性 | | `update_time` | 更新时间 | 不生成该属性 | | `sys_org_code` | 所属部门 | 不生成该属性 | 如果是**新建表**(用户自然语言描述需求),则默认添加全部系统字段。 如果是**已有表**(用户指定了表名且数据库中已存在),则必须根据实际 DDL 来决定。 树表额外字段:`pid`、`has_child`(同样需检查是否实际存在)。 ## 参考文件 生成代码前,**必须读取** 同目录下的 `codegen-reference.md` 获取完整代码模板骨架。 - `codegen-reference.md`:后端 Java + 前端 Vue3 完整代码模板骨架,生成代码前必须读取 - `references/ref-menu-sql.md`:菜单权限 SQL 模板(sys_permission + sys_role_permission),第 3 轮写入前必须读取 - `parallel-generation-mode.md`:并行生成模式(Beta)的契约冻结清单、SubAgent 派发 prompt 模板、跨端校验流程、回退策略。**仅当用户在"生成模式"章节选择并行模式后才需要读取**