Commit ac6e7e82 by pangchong

docs(rules): 规范XML节点解析与渲染规则与Git提交限制

- 删除README中的Ifar XML编辑器项目描述和解析渲染规范
- 在.gemini/.rules.md中新增集中化配置和复合结构特殊容器规则说明
- 明确禁止在业务代码中硬编码XML标签,强制从配置文件导入常量
- 详细定义COMPOSITE_CONTAINER_MAP及其子节点动态渲染和高亮定位要求
- 新增Git提交流程限制,禁止AI助手自动执行git commit和git push操作
- 移除示例工作卡XML文件,保持仓库纯净和规范
parent bae2f4d0
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
- AI 助手在分析问题、编写规划、输出日志及用户沟通的整个生命周期中,**必须**统一使用**中文** - AI 助手在分析问题、编写规划、输出日志及用户沟通的整个生命周期中,**必须**统一使用**中文**
- 自动生成的 git Commit Message 或文件注释也必须使用中文。 - 自动生成的 git Commit Message 或文件注释也必须使用中文。
- **禁止提交代码至 Git 仓库**:AI 助手仅负责编写和修改代码,**绝对禁止自动执行 `git commit` 或 `git push` 等提交操作**,所有修改在完成并验证正确后必须保持在未提交状态,交由用户手动审核后执行提交。
--- ---
...@@ -241,3 +242,44 @@ ...@@ -241,3 +242,44 @@
- **唯一 ID 约束 (强制)**:在业务代码中不管是通过对话框交互、还是通过特定逻辑手动拼装并向 XML 树中插入自定义节点(如 `TABLE``SIGNOFF` `customNode` 传入 `insertNode`)时,**必须**确保该自定义节点(以及其包含的所有下属子节点)在构建时被显式分配了一个全局唯一的 UUID(使用浏览器原生的 **`crypto.randomUUID()`** 生成),**严禁** `id` 初始化为空字符串 `''``null` 或未定义。 - **唯一 ID 约束 (强制)**:在业务代码中不管是通过对话框交互、还是通过特定逻辑手动拼装并向 XML 树中插入自定义节点(如 `TABLE``SIGNOFF` `customNode` 传入 `insertNode`)时,**必须**确保该自定义节点(以及其包含的所有下属子节点)在构建时被显式分配了一个全局唯一的 UUID(使用浏览器原生的 **`crypto.randomUUID()`** 生成),**严禁** `id` 初始化为空字符串 `''``null` 或未定义。
- **定位与映射绑定**:未配置有效唯一 UUID 的新插入节点,将导致编辑器内的 `nodeMap` 混乱、大纲树选中状态失效,以及右侧编辑面板的 `scrollIntoView` 滚动定位由于 `data-node-id` 缺失或为空而彻底崩溃。 - **定位与映射绑定**:未配置有效唯一 UUID 的新插入节点,将导致编辑器内的 `nodeMap` 混乱、大纲树选中状态失效,以及右侧编辑面板的 `scrollIntoView` 滚动定位由于 `data-node-id` 缺失或为空而彻底崩溃。
---
## 20. XML 节点解析与渲染规则规范 (源自 README.md)
为了保证编辑器的高内聚、低耦合与易维护性,所有开发人员(及 AI 助手)在新增、修改 XML 节点渲染时,**必须严格遵守以下规则**
### 20.1 配置集中化原则
- **禁止硬编码**:所有 XML 标签集合、分类常量、特殊标签匹配,必须定义在 `src/configs/xmlTags.ts` 文件中。
- **禁止局部私有定义**:所有业务组件需要进行标签判定时,必须从该配置文件中导入对应的常量,严禁在业务代码中出现局部私有的硬编码判断。
### 20.2 复合结构特殊容器规则 (`COMPOSITE_CONTAINER_MAP`)
当某些节点不是普通的块级节点,而是需要自定义布局、拼装表格或组合行内方式渲染其子节点(但其子节点又必须支持独立被选中、就地编辑和精确定位)时,它们被称为**复合结构特殊容器**
这些映射关系定义在 `src/configs/xmlTags.ts` `COMPOSITE_CONTAINER_MAP` 对象中:
```typescript
export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = {
CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'], // 电路断路器行与四列子元素
TED: ['TOOLNAME', 'TOOLNBR'], // 工具名称与工具件号
CON: ['CONNAME', 'CONNBR'], // 消耗品名称与消耗品件号
GRAPHIC: ['TITLE'], // 附图与附图标题
EINDATA: ['EIN'], // 适用性组与具体功能号
EXPD: ['EXPDNAME', 'CSN', 'ITEMNBR']
}
```
#### 📌 渲染与解析规范:
1. **容器排除**:在判断一个节点是否为普通列表容器(`isContainer`)时,必须通过 `!COMPOSITE_CONTAINER_MAP[tagName]` 将这些复合容器排除,防止它们被错误渲染为普通块列表。
2. **动态渲染(禁止写死)**
在渲染这些容器下的子节点时,**必须使用 `v-for` 遍历 `COMPOSITE_CONTAINER_MAP[node.tagName]` 进行动态渲染**,严禁使用 `v-if="node.children.find(c => c.tagName === 'SPECIFIC_TAG')"` 这种死代码。
3. **高亮与定位联动**
- 必须在每一个可被编辑或选中的子节点 DOM 元素上挂载 `:data-node-id="child.id"`,以确保搜索及左侧树点击时能精确定位到该节点。
- 父级包装器(如表格的 `<tr>`)如需在子节点被选中时联动高亮,必须通过遍历 `COMPOSITE_CONTAINER_MAP[parentTag]` 并判断 `child.id === selectedNodeId` 来动态激活高亮样式,确保高亮状态同步。
### 20.3 Git 提交限制原则
- **禁止提交到本地 Git 仓库**:在开发过程中,所有代码的修改和新增操作,**严禁使用 git commit、git push 等任何 Git 提交或推送命令**。所有修改仅保留在本地工作区中。
# Ifar XML 标注重构编辑器 (Ifar-Xml-Editor)
本项目是一款针对航空工业 XML 结构化工卡的专业编辑器,支持实时渲染、交互定位、XML 就地修改等功能。
---
## 🛠️ XML 节点解析与渲染规则规范
为了保证编辑器的高内聚、低耦合与易维护性,所有开发人员(及 AI 助手)在新增、修改 XML 节点渲染时,**必须严格遵守以下规则**
### 1. 配置集中化原则
- **禁止硬编码**:所有 XML 标签集合、分类常量、特殊标签匹配,必须定义在 `src/configs/xmlTags.ts` 文件中。
- **禁止局部私有定义**:所有业务组件需要进行标签判定时,必须从该配置文件中导入对应的常量,严禁在业务代码中出现局部私有的硬编码判断。
### 2. 复合结构特殊容器规则 (`COMPOSITE_CONTAINER_MAP`)
当某些节点不是普通的块级节点,而是需要自定义布局、拼装表格或组合行内方式渲染其子节点(但其子节点又必须支持独立被选中、就地编辑和精确定位)时,它们被称为**复合结构特殊容器**
这些映射关系定义在 `src/configs/xmlTags.ts``COMPOSITE_CONTAINER_MAP` 对象中:
```typescript
export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = {
CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'], // 电路断路器行与四列子元素
TED: ['TOOLNAME', 'TOOLNBR'], // 工具名称与工具件号
CON: ['CONNAME', 'CONNBR'], // 消耗品名称与消耗品件号
GRAPHIC: ['TITLE'], // 附图与附图标题
EINDATA: ['EIN'] // 适用性组与具体功能号
}
```
#### 📌 渲染与解析规范:
1. **容器排除**:在判断一个节点是否为普通列表容器(`isContainer`)时,必须通过 `!COMPOSITE_CONTAINER_MAP[tagName]` 将这些复合容器排除,防止它们被错误渲染为普通块列表。
2. **动态渲染(禁止写死)**
在渲染这些容器下的子节点时,**必须使用 `v-for` 遍历 `COMPOSITE_CONTAINER_MAP[node.tagName]` 进行动态渲染**,严禁使用 `v-if="node.children.find(c => c.tagName === 'SPECIFIC_TAG')"` 这种死代码。
3. **高亮与定位联动**
- 必须在每一个可被编辑或选中的子节点 DOM 元素上挂载 `:data-node-id="child.id"`,以确保搜索及左侧树点击时能精确定位到该节点。
- 父级包装器(如表格的 `<tr>`)如需在子节点被选中时联动高亮,必须通过遍历 `COMPOSITE_CONTAINER_MAP[parentTag]` 并判断 `child.id === selectedNodeId` 来动态激活高亮样式,确保高亮状态同步。
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -46,6 +46,9 @@ export const INLINE_ELEMENTS = [ ...@@ -46,6 +46,9 @@ export const INLINE_ELEMENTS = [
'STDNBR', 'STDNBR',
// 区域 // 区域
'ZONE', 'ZONE',
// 扭矩值
'TOR',
'TORVALUE',
// 适用性标记 // 适用性标记
'EFFECT', 'EFFECT',
'CONEFFECT', 'CONEFFECT',
...@@ -174,7 +177,7 @@ export const ROMAN_LOOKUP: Array<[string, number]> = [ ...@@ -174,7 +177,7 @@ export const ROMAN_LOOKUP: Array<[string, number]> = [
// ─── 派生组合标签集(由以上原子集合组合而成)──────────────────────────────── // ─── 派生组合标签集(由以上原子集合组合而成)────────────────────────────────
// 文档型叶子节点(用于在左侧树提取文本副标题展示) // 文档型叶子节点(用于在左侧树提取文本副标题展示)
export const DOCUMENT_LIKE_TAGS = ['PARA', 'PARAC', 'TITLE', 'TITLEC', 'REGULATION', 'REFBLOCK', 'GRPHCREF', 'REFINT', 'EQU'] export const DOCUMENT_LIKE_TAGS = ['PARA', 'PARAC', 'TITLE', 'TITLEC', 'REGULATION', 'REFBLOCK', 'GRPHCREF', 'REFINT', 'EQU', 'TXTLINE']
// 所有列表容器标签(有序 LIST1-7 + 无序 UNLIST/NUMLIST) // 所有列表容器标签(有序 LIST1-7 + 无序 UNLIST/NUMLIST)
export const ALL_LIST_TAGS = [...ORDERED_LIST_TAGS, 'UNLIST', 'NUMLIST'] export const ALL_LIST_TAGS = [...ORDERED_LIST_TAGS, 'UNLIST', 'NUMLIST']
...@@ -207,7 +210,8 @@ export const STRUCTURAL_TAGS = [ ...@@ -207,7 +210,8 @@ export const STRUCTURAL_TAGS = [
'CBSUBLST', 'CBSUBLST',
'GRAPHIC', 'GRAPHIC',
'FTNOTE', 'FTNOTE',
'APPEND' 'APPEND',
'TXTGRPHC'
] ]
// 中文段落优先的标签排序列表 // 中文段落优先的标签排序列表
......
...@@ -57,6 +57,7 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -57,6 +57,7 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
} }
const renderInline = computed(() => { const renderInline = computed(() => {
if (props.node.tagName === 'TXTGRPHC') return false
return props.isInline || INLINE_ELEMENTS_SET.has(props.node.tagName) return props.isInline || INLINE_ELEMENTS_SET.has(props.node.tagName)
}) })
...@@ -419,10 +420,21 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -419,10 +420,21 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
return child ? child.textContent || '' : '' return child ? child.textContent || '' : ''
} }
const formatTorvalue = (min: string | undefined, max: string | undefined, unit: string | undefined): string => {
const minVal = min ? min.trim() : ''
const maxVal = max ? max.trim() : ''
const unitVal = unit ? unit.trim() : ''
if (minVal && maxVal && minVal !== maxVal) {
return `${minVal} - ${maxVal} ${unitVal}`
}
return `${minVal || maxVal} ${unitVal}`
}
return { return {
editorStore, editorStore,
isSelected, isSelected,
isContainer, isContainer,
formatTorvalue,
renderInline, renderInline,
isListItem, isListItem,
isHeaderTag, isHeaderTag,
......
...@@ -452,6 +452,42 @@ ...@@ -452,6 +452,42 @@
></span> ></span>
</template> </template>
<!-- 12.5 TOR (力矩组合) 渲染 -->
<template v-else-if="node.tagName === 'TOR'">
<span
class="inline-flex items-baseline text-color1 cursor-pointer select-none"
:data-node-id="node.id"
:class="[
editorStore.selectedNodeId === node.id
? 'ring-2 ring-primary ring-offset-1 rounded-sm bg-primary/20 px-0.5'
: ''
]"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
<template v-for="(child, idx) in node.children.filter((c) => c.tagName === 'TORVALUE')" :key="child.id">
<span v-if="idx > 0" class="text-color3 mx-0.5 select-none">(</span>
<DocNodeRenderer :node="child" :parent="node" is-inline />
<span v-if="idx > 0" class="text-color3 select-none">)</span>
</template>
</span>
</template>
<!-- 12.6 TORVALUE (具体力矩值) 渲染 -->
<template v-else-if="node.tagName === 'TORVALUE'">
<span
class="inline-flex items-center text-color1 cursor-pointer select-none font-mono underline hover:text-primary transition-all"
:data-node-id="node.id"
:class="[
editorStore.selectedNodeId === node.id
? 'ring-2 ring-primary ring-offset-1 rounded-sm bg-primary/20 px-0.5'
: ''
]"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
{{ formatTorvalue(node.attributes.MIN, node.attributes.MAX, node.attributes.UNIT) }}
</span>
</template>
<!-- 13. TED / CON (工具/消耗品数据组合) 统一配置化渲染 --> <!-- 13. TED / CON (工具/消耗品数据组合) 统一配置化渲染 -->
<template v-else-if="node.tagName === 'TED' || node.tagName === 'CON'"> <template v-else-if="node.tagName === 'TED' || node.tagName === 'CON'">
<span <span
...@@ -670,10 +706,37 @@ ...@@ -670,10 +706,37 @@
</div> </div>
</template> </template>
<!-- 19.5 TXTGRPHC (文本图形块) 处理 -->
<template v-else-if="node.tagName === 'TXTGRPHC'">
<div
class="my-2 font-mono text-sm leading-relaxed overflow-x-auto flex flex-col space-y-0.5"
style="font-family: 'NSimSun', '新宋体', 'SimSun', '宋体', monospace !important; white-space: pre !important;"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
<DocNodeRenderer
v-for="child in node.children"
:key="child.id"
:node="child"
:parent="node"
/>
</div>
</template>
<!-- 19.6 TXTLINE (文本图形行) 处理 -->
<template v-else-if="node.tagName === 'TXTLINE'">
<div
contenteditable="true"
class="focus:outline-none focus:bg-fill-3 px-1 rounded min-h-[1.5em] transition-colors"
style="font-family: 'NSimSun', '新宋体', 'SimSun', '宋体', monospace !important; white-space: pre !important;"
@blur="handleTextBlur"
v-text="node.textContent"
></div>
</template>
<!-- 20. RECORD-LINE (记录项) 处理 --> <!-- 20. RECORD-LINE (记录项) 处理 -->
<template v-else-if="node.tagName === 'RECORD-LINE'"> <template v-else-if="node.tagName === 'RECORD-LINE'">
<div class="my-1 py-1"> <div class="my-1 py-1">
<div class="flex flex-wrap items-center gap-1.5 text-sm text-color2"> <div class="flex flex-wrap items-center gap-1.5 text-sm text-color2" :style="{ justifyContent: 'var(--cell-align, flex-start)' }">
<template v-if="node.children && node.children.length > 0"> <template v-if="node.children && node.children.length > 0">
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" :parent="node" is-inline /> <DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" :parent="node" is-inline />
</template> </template>
...@@ -1041,6 +1104,7 @@ const { ...@@ -1041,6 +1104,7 @@ const {
editorStore, editorStore,
isSelected, isSelected,
isContainer, isContainer,
formatTorvalue,
renderInline, renderInline,
isListItem, isListItem,
isHeaderTag, isHeaderTag,
...@@ -1072,4 +1136,12 @@ const { ...@@ -1072,4 +1136,12 @@ const {
content: ', '; content: ', ';
color: inherit; color: inherit;
} }
/* 保护 TXTGRPHC 的严格等宽与中英文 2:1 比例 */
[data-tag-name="TXTGRPHC"],
[data-tag-name="TXTGRPHC"] *,
[data-tag-name="TXTLINE"],
[data-tag-name="TXTLINE"] * {
font-family: 'NSimSun', '新宋体', 'SimSun', '宋体', monospace !important;
white-space: pre !important;
}
</style> </style>
...@@ -257,6 +257,32 @@ export function useNodeTree( ...@@ -257,6 +257,32 @@ export function useNodeTree(
const tag = node.attributes.TAG || '签字' const tag = node.attributes.TAG || '签字'
const level = node.attributes['CK-LEVEL'] const level = node.attributes['CK-LEVEL']
subtitle = level ? `Tag: ${tag} (${level}级)` : `Tag: ${tag}` subtitle = level ? `Tag: ${tag} (${level}级)` : `Tag: ${tag}`
} else if (node.tagName === 'TOR') {
const formatTorValueStr = (valNode: XmlNode): string => {
const min = (valNode.attributes.MIN || '').trim()
const max = (valNode.attributes.MAX || '').trim()
const unit = (valNode.attributes.UNIT || '').trim()
if (min && max && min !== max) {
return `${min} - ${max} ${unit}`
}
return `${min || max} ${unit}`
}
const torvalues = node.children.filter((c) => c.tagName === 'TORVALUE')
subtitle = torvalues
.map((c, idx) => {
const str = formatTorValueStr(c)
return idx > 0 ? `(${str})` : str
})
.join(' ')
} else if (node.tagName === 'TORVALUE') {
const min = (node.attributes.MIN || '').trim()
const max = (node.attributes.MAX || '').trim()
const unit = (node.attributes.UNIT || '').trim()
if (min && max && min !== max) {
subtitle = `${min} - ${max} ${unit}`
} else {
subtitle = `${min || max} ${unit}`
}
} else if (node.attributes.ID) { } else if (node.attributes.ID) {
subtitle = node.attributes.ID subtitle = node.attributes.ID
} else if (node.attributes.EFFRG) { } else if (node.attributes.EFFRG) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment