Commit 68090924 by pangchong

feat(config): 补充 XML 标签配置及复合容器规范

- 新增 `.env.xm` 环境变量配置支持 xm 模式构建与运行
- 在 package.json 中添加 dev:xm 和 build:xm 脚本命令
- 详细补充了 xmlTags.ts 中 XML 节点标签分类,如 HEADER_TAGS、INLINE_ELEMENTS
- 新增 EMPTY_MARKER_TAGS、NOTE_VARIANT_TAGS、HIDDEN_STRUCTURAL_TAGS 等节点类型定义
- 完善 COMPOSITE_CONTAINER_MAP 复合结构特殊容器的注释说明和规范
- 在 README.md 里新增复合结构特殊容器的使用规则及渲染原则说明
- 精细化规范文档规则及格式调整,添加多条项目开发规范说明
- 修正 API 请求中对响应 code 判断逻辑,统一以 200 作为成功标识
parent 66bf1bb7
VITE_PARTITION_NAME = 'from_project_001'
VITE_CUSTOM_CODE = 'xm'
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npm run format
echo '***************************************************' echo '***************************************************'
echo '********************注意提交格式*******************' echo '********************注意提交格式*******************'
echo '***************************************************' echo '***************************************************'
......
...@@ -9,13 +9,16 @@ ...@@ -9,13 +9,16 @@
为了保证编辑器的高内聚、低耦合与易维护性,所有开发人员(及 AI 助手)在新增、修改 XML 节点渲染时,**必须严格遵守以下规则** 为了保证编辑器的高内聚、低耦合与易维护性,所有开发人员(及 AI 助手)在新增、修改 XML 节点渲染时,**必须严格遵守以下规则**
### 1. 配置集中化原则 ### 1. 配置集中化原则
* **禁止硬编码**:所有 XML 标签集合、分类常量、特殊标签匹配,必须定义在 `src/configs/xmlTags.ts` 文件中。
* **禁止局部私有定义**:所有业务组件需要进行标签判定时,必须从该配置文件中导入对应的常量,严禁在业务代码中出现局部私有的硬编码判断。 - **禁止硬编码**:所有 XML 标签集合、分类常量、特殊标签匹配,必须定义在 `src/configs/xmlTags.ts` 文件中。
- **禁止局部私有定义**:所有业务组件需要进行标签判定时,必须从该配置文件中导入对应的常量,严禁在业务代码中出现局部私有的硬编码判断。
### 2. 复合结构特殊容器规则 (`COMPOSITE_CONTAINER_MAP`) ### 2. 复合结构特殊容器规则 (`COMPOSITE_CONTAINER_MAP`)
当某些节点不是普通的块级节点,而是需要自定义布局、拼装表格或组合行内方式渲染其子节点(但其子节点又必须支持独立被选中、就地编辑和精确定位)时,它们被称为**复合结构特殊容器** 当某些节点不是普通的块级节点,而是需要自定义布局、拼装表格或组合行内方式渲染其子节点(但其子节点又必须支持独立被选中、就地编辑和精确定位)时,它们被称为**复合结构特殊容器**
这些映射关系定义在 `src/configs/xmlTags.ts``COMPOSITE_CONTAINER_MAP` 对象中: 这些映射关系定义在 `src/configs/xmlTags.ts``COMPOSITE_CONTAINER_MAP` 对象中:
```typescript ```typescript
export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = { export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = {
CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'], // 电路断路器行与四列子元素 CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'], // 电路断路器行与四列子元素
...@@ -27,9 +30,10 @@ export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = { ...@@ -27,9 +30,10 @@ export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = {
``` ```
#### 📌 渲染与解析规范: #### 📌 渲染与解析规范:
1. **容器排除**:在判断一个节点是否为普通列表容器(`isContainer`)时,必须通过 `!COMPOSITE_CONTAINER_MAP[tagName]` 将这些复合容器排除,防止它们被错误渲染为普通块列表。 1. **容器排除**:在判断一个节点是否为普通列表容器(`isContainer`)时,必须通过 `!COMPOSITE_CONTAINER_MAP[tagName]` 将这些复合容器排除,防止它们被错误渲染为普通块列表。
2. **动态渲染(禁止写死)** 2. **动态渲染(禁止写死)**
在渲染这些容器下的子节点时,**必须使用 `v-for` 遍历 `COMPOSITE_CONTAINER_MAP[node.tagName]` 进行动态渲染**,严禁使用 `v-if="node.children.find(c => c.tagName === 'SPECIFIC_TAG')"` 这种死代码。 在渲染这些容器下的子节点时,**必须使用 `v-for` 遍历 `COMPOSITE_CONTAINER_MAP[node.tagName]` 进行动态渲染**,严禁使用 `v-if="node.children.find(c => c.tagName === 'SPECIFIC_TAG')"` 这种死代码。
3. **高亮与定位联动** 3. **高亮与定位联动**
* 必须在每一个可被编辑或选中的子节点 DOM 元素上挂载 `:data-node-id="child.id"`,以确保搜索及左侧树点击时能精确定位到该节点。 - 必须在每一个可被编辑或选中的子节点 DOM 元素上挂载 `:data-node-id="child.id"`,以确保搜索及左侧树点击时能精确定位到该节点。
* 父级包装器(如表格的 `<tr>`)如需在子节点被选中时联动高亮,必须通过遍历 `COMPOSITE_CONTAINER_MAP[parentTag]` 并判断 `child.id === selectedNodeId` 来动态激活高亮样式,确保高亮状态同步。 - 父级包装器(如表格的 `<tr>`)如需在子节点被选中时联动高亮,必须通过遍历 `COMPOSITE_CONTAINER_MAP[parentTag]` 并判断 `child.id === selectedNodeId` 来动态激活高亮样式,确保高亮状态同步。
...@@ -7,8 +7,10 @@ ...@@ -7,8 +7,10 @@
"dev": "vite", "dev": "vite",
"dev:prod": "vite --mode prod", "dev:prod": "vite --mode prod",
"dev:test": "vite --mode test", "dev:test": "vite --mode test",
"dev:xm": "vite --mode xm",
"build": "vue-tsc -b && vite build", "build": "vue-tsc -b && vite build",
"build:prod": "vue-tsc -b && vite build --mode prod", "build:prod": "vue-tsc -b && vite build --mode prod",
"build:xm": "vue-tsc -b && vite build --mode xm",
"preview": "vite preview", "preview": "vite preview",
"prepare": "husky install", "prepare": "husky install",
"commitlint": "commitlint --config commitlint.config.cjs -e -V", "commitlint": "commitlint --config commitlint.config.cjs -e -V",
......
...@@ -50,11 +50,7 @@ const createService = (baseURL: string) => { ...@@ -50,11 +50,7 @@ const createService = (baseURL: string) => {
if (isJson) { if (isJson) {
json = (await response.json()) as ResponseData json = (await response.json()) as ResponseData
if (json) { if (json) {
if ( if (json.code === 200 || json.code === '200' || (json.code === undefined && json.success === undefined)) {
json.code === 200 ||
json.code === '200' ||
(json.code === undefined && json.success === undefined)
) {
json.code = 200 json.code = 200
return json return json
} }
...@@ -80,11 +76,7 @@ const createService = (baseURL: string) => { ...@@ -80,11 +76,7 @@ const createService = (baseURL: string) => {
} }
if (json) { if (json) {
if ( if (json.code === 200 || json.code === '200' || (json.code === undefined && json.success === undefined)) {
json.code === 200 ||
json.code === '200' ||
(json.code === undefined && json.success === undefined)
) {
json.code = 200 json.code = 200
} }
} }
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -12,9 +12,7 @@ ...@@ -12,9 +12,7 @@
content-style="padding: 0; background: transparent;" content-style="padding: 0; background: transparent;"
class="download-progress-modal" class="download-progress-modal"
> >
<div <div class="relative overflow-hidden rounded-2xl bg-fill-1/80 backdrop-blur-xl p-4 border border-color1/20">
class="relative overflow-hidden rounded-2xl bg-fill-1/80 backdrop-blur-xl p-4 border border-color1/20"
>
<!-- 装饰性背景光效 --> <!-- 装饰性背景光效 -->
<div class="absolute -top-24 -right-24 w-48 h-48 bg-primary/10 rounded-full blur-3xl animate-pulse"></div> <div class="absolute -top-24 -right-24 w-48 h-48 bg-primary/10 rounded-full blur-3xl animate-pulse"></div>
<div class="absolute -bottom-24 -left-24 w-48 h-48 bg-primary/5 rounded-full blur-3xl animate-pulse" style="animation-delay: 1s"></div> <div class="absolute -bottom-24 -left-24 w-48 h-48 bg-primary/5 rounded-full blur-3xl animate-pulse" style="animation-delay: 1s"></div>
......
...@@ -28,8 +28,6 @@ ...@@ -28,8 +28,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const show = ref(false) const show = ref(false)
const loading = ref(false) const loading = ref(false)
const exportProgress = ref(0) const exportProgress = ref(0)
......
...@@ -55,7 +55,6 @@ ...@@ -55,7 +55,6 @@
import { CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5' import { CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
import type { UploadFileInfo } from 'naive-ui' import type { UploadFileInfo } from 'naive-ui'
interface ImportOptions { interface ImportOptions {
title?: string title?: string
api: string api: string
......
...@@ -86,7 +86,6 @@ ...@@ -86,7 +86,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
......
...@@ -36,8 +36,7 @@ ...@@ -36,8 +36,7 @@
:style="{ backgroundColor: themeVars.actionColor, color: themeVars.textColor2 }" :style="{ backgroundColor: themeVars.actionColor, color: themeVars.textColor2 }"
> >
<!-- 左侧:占位 --> <!-- 左侧:占位 -->
<div class="mr-auto flex items-center"> <div class="mr-auto flex items-center"></div>
</div>
<div v-if="!compact" class="flex items-center space-x-3"> <div v-if="!compact" class="flex items-center space-x-3">
<span>显示 {{ pageStart }}{{ pageEnd }}, 共 {{ displayTotal }} 记录</span> <span>显示 {{ pageStart }}{{ pageEnd }}, 共 {{ displayTotal }} 记录</span>
...@@ -128,7 +127,6 @@ ...@@ -128,7 +127,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui' import type { DataTableColumns } from 'naive-ui'
import { PlayBackOutline, ChevronBackOutline, ChevronForwardOutline, PlayForwardOutline, RefreshOutline } from '@vicons/ionicons5' import { PlayBackOutline, ChevronBackOutline, ChevronForwardOutline, PlayForwardOutline, RefreshOutline } from '@vicons/ionicons5'
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PropType, ComponentPublicInstance } from 'vue' import type { PropType, ComponentPublicInstance } from 'vue'
const props = defineProps({ const props = defineProps({
......
...@@ -39,7 +39,6 @@ ...@@ -39,7 +39,6 @@
import { CloudUploadOutline } from '@vicons/ionicons5' import { CloudUploadOutline } from '@vicons/ionicons5'
import type { UploadFileInfo } from 'naive-ui' import type { UploadFileInfo } from 'naive-ui'
export interface UploadOptions { export interface UploadOptions {
/** 弹窗标题 */ /** 弹窗标题 */
title?: string title?: string
......
...@@ -10,9 +10,16 @@ ...@@ -10,9 +10,16 @@
* 包含 SMJC/LMJC/NRCJC 等各类工卡头部节点。 * 包含 SMJC/LMJC/NRCJC 等各类工卡头部节点。
*/ */
export const HEADER_TAGS = [ export const HEADER_TAGS = [
'SMJC-HEADER', 'LMJC-HEADER', 'NRCJC-HEADER', 'SMJC-HEADER',
'TCJC-HEADER', 'QECJC-HEADER', 'EOTK-HEADER', 'LMJC-HEADER',
'DRJC-HEADER', 'CMJC-HEADER', 'EOJC-HEADER', 'MAOJC-HEADER' 'NRCJC-HEADER',
'TCJC-HEADER',
'QECJC-HEADER',
'EOTK-HEADER',
'DRJC-HEADER',
'CMJC-HEADER',
'EOJC-HEADER',
'MAOJC-HEADER'
] ]
/** /**
...@@ -21,24 +28,46 @@ export const HEADER_TAGS = [ ...@@ -21,24 +28,46 @@ export const HEADER_TAGS = [
*/ */
export const INLINE_ELEMENTS = [ export const INLINE_ELEMENTS = [
// 引用类 // 引用类
'REFBLOCK', 'REFINT', 'REFEXT', 'GRPHCREF', 'REFBLOCK',
'REFINT',
'REFEXT',
'GRPHCREF',
// 零件编号类 // 零件编号类
'EIN', 'EINMFR', 'PAN', 'SBNBR', 'EIN',
'EINMFR',
'PAN',
'SBNBR',
// 工具类 // 工具类
'TOOLNBR', 'TOOLNAME', 'TED', 'TOOLNBR',
'TOOLNAME',
'TED',
// 标识类 // 标识类
'STDNAME', 'STDNBR', 'STDNAME',
'STDNBR',
// 区域 // 区域
'ZONE', 'ZONE',
// 适用性标记 // 适用性标记
'EFFECT', 'CONEFFECT', 'EFFECT',
'CONEFFECT',
// CB 类(工具箱) // CB 类(工具箱)
'CB', 'CBNAME', 'CBLOC', 'CB',
'CBNAME',
'CBLOC',
// 上下标 // 上下标
'SUPER', 'SUPERSCRIPT', 'SUB', 'SUBSCRIPT', 'SUPER',
'SUPERSCRIPT',
'SUB',
'SUBSCRIPT',
// 其他常见行内 // 其他常见行内
'SSI', 'ACRO', 'SP', 'KWD', 'CSN', 'CON', 'NCON', 'SSI',
'REVST', 'REVEND' 'ACRO',
'SP',
'KWD',
'CSN',
'CON',
'NCON',
'REVST',
'REVEND'
] ]
// 段落类节点标签(PARA 为英文段落,PARAC 为中文段落) // 段落类节点标签(PARA 为英文段落,PARAC 为中文段落)
...@@ -81,6 +110,25 @@ export const CB_COMPONENT_TAGS = ['CB', 'CBNAME', 'CBLOC'] ...@@ -81,6 +110,25 @@ export const CB_COMPONENT_TAGS = ['CB', 'CBNAME', 'CBLOC']
// 上下标标签 // 上下标标签
export const SUPER_SUB_TAGS = ['SUPER', 'SUPERSCRIPT', 'SUB', 'SUBSCRIPT'] export const SUPER_SUB_TAGS = ['SUPER', 'SUPERSCRIPT', 'SUB', 'SUBSCRIPT']
/**
* EMPTY 类型的空标记节点(DTD contentModel = EMPTY)
* 这些节点在 XML 中没有内容,仅作为语义标记,渲染时必须输出空,
* 否则 fallback 处理器会把 node.tagName 当文本显示,造成界面污染。
*/
export const EMPTY_MARKER_TAGS = ['REVST', 'REVEND', 'COCST', 'COCEND', 'COCEFF', 'HOTLINK', 'ISEMPTY', 'DELETED']
/**
* NOTE 变体标签(与 NOTE 结构相同,但语义上属于不同类型的注意事项)
* 渲染时使用 NOTE 的样式,但显示不同的标签文字。
*/
export const NOTE_VARIANT_TAGS = ['HNANOTE', 'ACPANOTE', 'APAPNOTE', 'EAPPNOTE', 'INTNOTE', 'SPECNOTE']
/**
* 隐藏结构型元数据容器(在 DocNodeRenderer 中不显示内容,仅选中时显示提示)
* 这些节点包含工卡的关联数据、任务要求等,不属于正文内容。
*/
export const HIDDEN_STRUCTURAL_TAGS = ['ASSODATA', 'TASKREQ', 'ELAPSTIM', 'MANHOUR', 'NBRPERS', 'SFTLST', 'SFTDATA', 'SPRL', 'SPRLS', 'SPID', 'SPPHASE', 'SPNAME', 'SPSKILL']
// 复合结构特殊容器及其子节点关系映射表(键为直接父级节点标签名,值为其下需要特殊高亮/选中定位的子节点标签名) // 复合结构特殊容器及其子节点关系映射表(键为直接父级节点标签名,值为其下需要特殊高亮/选中定位的子节点标签名)
export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = { export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = {
CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'], CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'],
...@@ -105,37 +153,40 @@ export const ROMAN_LOOKUP: Array<[string, number]> = [ ...@@ -105,37 +153,40 @@ export const ROMAN_LOOKUP: Array<[string, number]> = [
// ─── 派生组合标签集(由以上原子集合组合而成)──────────────────────────────── // ─── 派生组合标签集(由以上原子集合组合而成)────────────────────────────────
// 文档型叶子节点(用于在左侧树提取文本副标题展示) // 文档型叶子节点(用于在左侧树提取文本副标题展示)
export const DOCUMENT_LIKE_TAGS = [ export const DOCUMENT_LIKE_TAGS = ['PARA', 'PARAC', 'TITLE', 'TITLEC', 'REGULATION', 'REFBLOCK', 'GRPHCREF', 'REFINT', 'EQU']
'PARA', 'PARAC', 'TITLE', 'TITLEC', 'REGULATION',
'REFBLOCK', 'GRPHCREF', 'REFINT', 'EQU'
]
// 所有列表容器标签(有序 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']
// 警示及适用性关联标签(WARNING_LIKE + CAUTION + NOTE,在列表拆分等场景用作前置拦截) // 警示及适用性关联标签(WARNING_LIKE + CAUTION + NOTE 及其变体,在列表拆分等场景用作前置拦截)
export const ALERT_AND_EFF_TAGS = [...WARNING_LIKE_TAGS, 'CAUTION', 'NOTE'] export const ALERT_AND_EFF_TAGS = [...WARNING_LIKE_TAGS, 'CAUTION', 'NOTE', 'HNANOTE', 'ACPANOTE', 'APAPNOTE', 'EAPPNOTE', 'INTNOTE', 'SPECNOTE']
// 所有需要自定义颜色显示的特殊节点(WARNING_LIKE + CAUTION + NOTE_AND_REF) // 所有需要自定义颜色显示的特殊节点(WARNING_LIKE + CAUTION + NOTE_AND_REF)
export const COLORED_TAGS = [...WARNING_LIKE_TAGS, 'CAUTION', ...NOTE_AND_REF_TAGS] export const COLORED_TAGS = [...WARNING_LIKE_TAGS, 'CAUTION', ...NOTE_AND_REF_TAGS]
// 表格单元格下的复杂内容容器标签(当包含这些标签时使用 DocNodeRenderer 渲染) // 表格单元格下的复杂内容容器标签(当包含这些标签时使用 DocNodeRenderer 渲染)
export const COMPLEX_ENTRY_TAGS = [ export const COMPLEX_ENTRY_TAGS = [...ORDERED_LIST_TAGS, 'UNLIST', 'NUMLIST', 'WARNING', 'CAUTION', 'NOTE', 'GRAPHIC', 'TABLE']
...ORDERED_LIST_TAGS, 'UNLIST', 'NUMLIST',
'WARNING', 'CAUTION', 'NOTE',
'GRAPHIC', 'TABLE'
]
// 树结构容器节点集合(在获取文本预览等场景中跳过这些节点的文本提取) // 树结构容器节点集合(在获取文本预览等场景中跳过这些节点的文本提取)
export const STRUCTURAL_TAGS = [ export const STRUCTURAL_TAGS = [
...TRANSPARENT_TAGS, ...TRANSPARENT_TAGS,
'PRETOPIC', 'TOPIC', 'SUBTASK', 'STEP', 'PRETOPIC',
'TOPIC',
'SUBTASK',
'STEP',
...ORDERED_LIST_TAGS, ...ORDERED_LIST_TAGS,
...LIST_ITEM_TAGS, ...LIST_ITEM_TAGS,
'UNLIST', 'NUMLIST', 'UNLIST',
'NUMLIST',
...CALS_TABLE_ALL_TAGS, ...CALS_TABLE_ALL_TAGS,
'WARNING', 'CAUTION', 'NOTE', 'HNANOTE', 'WARNING',
'CBSUBLST', 'GRAPHIC', 'FTNOTE', 'APPEND' 'CAUTION',
'NOTE',
'HNANOTE',
'CBSUBLST',
'GRAPHIC',
'FTNOTE',
'APPEND'
] ]
// 中文段落优先的标签排序列表 // 中文段落优先的标签排序列表
...@@ -143,4 +194,3 @@ export const CHINESE_FIRST_PARA_TAGS = ['PARAC', 'PARA'] ...@@ -143,4 +194,3 @@ export const CHINESE_FIRST_PARA_TAGS = ['PARAC', 'PARA']
// 默认翻译目标标签列表 // 默认翻译目标标签列表
export const TRANSLATE_TARGET_TAGS = ['PARAC', 'TITLEC'] export const TRANSLATE_TARGET_TAGS = ['PARAC', 'TITLEC']
import mitt from 'mitt' import mitt from 'mitt'
type Fn = (...args: any[]) => void type Fn = (...args: any[]) => void
interface Option { interface Option {
......
...@@ -7,16 +7,15 @@ interface ShortcutOptions { ...@@ -7,16 +7,15 @@ interface ShortcutOptions {
onEscape?: () => void onEscape?: () => void
} }
export function useKeyboardShortcuts( export function useKeyboardShortcuts(options: ShortcutOptions = {}, enableGlobal = false) {
options: ShortcutOptions = {},
enableGlobal = false
) {
const appStore = useAppStore() const appStore = useAppStore()
const editorStore = useEditorStore() const editorStore = useEditorStore()
// 1. 全局快捷键(使用 useEventListener 并开启 capture: true 捕获事件) // 1. 全局快捷键(使用 useEventListener 并开启 capture: true 捕获事件)
if (enableGlobal) { if (enableGlobal) {
useEventListener('keydown', (e: KeyboardEvent) => { useEventListener(
'keydown',
(e: KeyboardEvent) => {
const ctrl = e.ctrlKey || e.metaKey const ctrl = e.ctrlKey || e.metaKey
const shift = e.shiftKey const shift = e.shiftKey
...@@ -57,7 +56,9 @@ export function useKeyboardShortcuts( ...@@ -57,7 +56,9 @@ export function useKeyboardShortcuts(
}, 0) }, 0)
} }
} }
}, { capture: true }) },
{ capture: true }
)
} }
// 2. 局部/上下文快捷键(使用 onKeyStroke,在冒泡阶段响应) // 2. 局部/上下文快捷键(使用 onKeyStroke,在冒泡阶段响应)
......
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import MainLayout from '@/layouts/MainLayout.vue' import MainLayout from '@/layouts/MainLayout.vue'
// 静态路由 // 静态路由
const constantRoutes: Array<RouteRecordRaw> = [ const constantRoutes: Array<RouteRecordRaw> = [
{ {
...@@ -41,8 +40,6 @@ const router = createRouter({ ...@@ -41,8 +40,6 @@ const router = createRouter({
routes: constantRoutes routes: constantRoutes
}) })
router.beforeEach(() => { router.beforeEach(() => {
window.$loadingBar?.start() window.$loadingBar?.start()
}) })
...@@ -61,6 +58,4 @@ router.onError(() => { ...@@ -61,6 +58,4 @@ router.onError(() => {
window.$loadingBar?.error() window.$loadingBar?.error()
}) })
export default router export default router
...@@ -741,15 +741,11 @@ export const useEditorStore = defineStore('editor', { ...@@ -741,15 +741,11 @@ export const useEditorStore = defineStore('editor', {
// 退化校验:如果子元素为空,直接收归为纯文本并清空混合数组 // 退化校验:如果子元素为空,直接收归为纯文本并清空混合数组
if (parent.children.length === 0) { if (parent.children.length === 0) {
const mergedText = parent.mixedContent const mergedText = parent.mixedContent.map((item: any) => (item.type === 'text' ? item.text || '' : '')).join('')
.map((item: any) => (item.type === 'text' ? item.text || '' : ''))
.join('')
parent.textContent = mergedText parent.textContent = mergedText
parent.mixedContent = [] parent.mixedContent = []
} else { } else {
parent.textContent = parent.mixedContent parent.textContent = parent.mixedContent.map((item: any) => (item.type === 'text' ? item.text || '' : '')).join('')
.map((item: any) => (item.type === 'text' ? item.text || '' : ''))
.join('')
} }
// 选中父节点 // 选中父节点
...@@ -758,7 +754,6 @@ export const useEditorStore = defineStore('editor', { ...@@ -758,7 +754,6 @@ export const useEditorStore = defineStore('editor', {
this.rebuildNodeMap() this.rebuildNodeMap()
}, },
/** /**
* 批量删除指定的多个节点(支持跨层级跨父节点) * 批量删除指定的多个节点(支持跨层级跨父节点)
*/ */
......
...@@ -17,9 +17,6 @@ declare global { ...@@ -17,9 +17,6 @@ declare global {
[key: string]: any [key: string]: any
} }
/** /**
* 业务封装后的 Dialog API (Promise 风格) * 业务封装后的 Dialog API (Promise 风格)
*/ */
......
/** /**
* 通用列表数据请求工具 * 通用列表数据请求工具
* 逻辑来源:抽离自 CommonTable 和 CommonSelect 的数据获取逻辑 * 逻辑来源:抽离自 CommonTable 和 CommonSelect 的数据获取逻辑
......
...@@ -11,7 +11,45 @@ let _schema: DtdSchema | null = null ...@@ -11,7 +11,45 @@ let _schema: DtdSchema | null = null
* 加载 DTD Schema * 加载 DTD Schema
*/ */
export function loadDtdSchema(json: DtdSchema): void { export function loadDtdSchema(json: DtdSchema): void {
_schema = json // 深度复制一份原始 schema 避免直接改变导入的数据
const schema = JSON.parse(JSON.stringify(json)) as DtdSchema
// 根据定制环境代号 VITE_CUSTOM_CODE 自动加载对应的 DTD 扩展配置
const customCode = import.meta.env.VITE_CUSTOM_CODE
let dtdExtension: any = null
if (customCode) {
// 使用 import.meta.glob 匹配并载入所有子目录下的 assets/json 目录中的 json 文件
const modules = import.meta.glob('../**/assets/json/*.json', { eager: true }) as Record<string, any>
const matchKey = Object.keys(modules).find(
(key) => key.toLowerCase().includes(`/${customCode.toLowerCase()}/`) && key.toLowerCase().endsWith('/dtdextension.json')
)
if (matchKey) {
dtdExtension = modules[matchKey].default || modules[matchKey]
}
}
// 合并自定义 DTD 扩展配置
if (dtdExtension && dtdExtension.elements) {
for (const [tagName, elementExt] of Object.entries(dtdExtension.elements as Record<string, any>)) {
if (schema.elements[tagName]) {
const targetElement = schema.elements[tagName]
if (elementExt && elementExt.attributes) {
for (const [attrName, attrExt] of Object.entries(elementExt.attributes as Record<string, any>)) {
if (targetElement.attributes[attrName]) {
// 覆盖属性定义
Object.assign(targetElement.attributes[attrName], attrExt)
} else {
// 新增属性定义
targetElement.attributes[attrName] = attrExt as any
}
}
}
}
}
}
_schema = schema
} }
/** /**
......
import { NText } from 'naive-ui' import { NText } from 'naive-ui'
import CommonButton from '@/components/CommonButton.vue' import CommonButton from '@/components/CommonButton.vue'
......
...@@ -66,8 +66,8 @@ function domElementToXmlNode(element: Element, parentId: string | null): XmlNode ...@@ -66,8 +66,8 @@ function domElementToXmlNode(element: Element, parentId: string | null): XmlNode
let textContent = '' let textContent = ''
const childNodes = Array.from(element.childNodes) const childNodes = Array.from(element.childNodes)
const hasElementChildren = childNodes.some(n => n.nodeType === Node.ELEMENT_NODE) const hasElementChildren = childNodes.some((n) => n.nodeType === Node.ELEMENT_NODE)
const hasTextChildren = childNodes.some(n => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()) const hasTextChildren = childNodes.some((n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim())
if (hasElementChildren && hasTextChildren) { if (hasElementChildren && hasTextChildren) {
// 混合内容节点(如 PARA, PARAC 中嵌有 REFBLOCK 等行内元素) // 混合内容节点(如 PARA, PARAC 中嵌有 REFBLOCK 等行内元素)
...@@ -132,7 +132,7 @@ export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: b ...@@ -132,7 +132,7 @@ export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: b
if (item.type === 'text') { if (item.type === 'text') {
content += escapeXmlText(item.text || '') content += escapeXmlText(item.text || '')
} else if (item.type === 'element' && item.nodeId) { } else if (item.type === 'element' && item.nodeId) {
const child = node.children.find(c => c.id === item.nodeId) const child = node.children.find((c) => c.id === item.nodeId)
if (child) { if (child) {
content += serializeTreeToXml(child, 0, compact) content += serializeTreeToXml(child, 0, compact)
} }
...@@ -147,7 +147,7 @@ export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: b ...@@ -147,7 +147,7 @@ export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: b
} }
// 纯元素子节点 // 纯元素子节点
const childrenXml = node.children.map(c => serializeTreeToXml(c, indent + 1, compact)).join(newline) const childrenXml = node.children.map((c) => serializeTreeToXml(c, indent + 1, compact)).join(newline)
return `${pad}<${openTag}>${newline}${childrenXml}${newline}${pad}</${node.tagName}>` return `${pad}<${openTag}>${newline}${childrenXml}${newline}${pad}</${node.tagName}>`
} }
...@@ -216,9 +216,9 @@ export function cloneNode(node: XmlNode, newParentId: string | null = null): Xml ...@@ -216,9 +216,9 @@ export function cloneNode(node: XmlNode, newParentId: string | null = null): Xml
id: newId, id: newId,
tagName: node.tagName, tagName: node.tagName,
attributes: { ...node.attributes }, attributes: { ...node.attributes },
children: node.children.map(c => cloneNode(c, newId)), children: node.children.map((c) => cloneNode(c, newId)),
textContent: node.textContent, textContent: node.textContent,
mixedContent: node.mixedContent.map(item => { mixedContent: node.mixedContent.map((item) => {
if (item.type === 'text') return { ...item } if (item.type === 'text') return { ...item }
// 元素引用需要更新 nodeId,但这里只做浅复制标记 // 元素引用需要更新 nodeId,但这里只做浅复制标记
return { ...item } return { ...item }
...@@ -234,7 +234,7 @@ export function getNodeDisplayName(node: XmlNode): string { ...@@ -234,7 +234,7 @@ export function getNodeDisplayName(node: XmlNode): string {
const tag = node.tagName const tag = node.tagName
// 有标题的节点 // 有标题的节点
const titleChild = node.children.find(c => c.tagName === 'TITLEC' || c.tagName === 'TITLE') const titleChild = node.children.find((c) => c.tagName === 'TITLEC' || c.tagName === 'TITLE')
if (titleChild) { if (titleChild) {
const title = titleChild.textContent || getTextFromMixedContent(titleChild) const title = titleChild.textContent || getTextFromMixedContent(titleChild)
if (title) return `${tag}: ${title.slice(0, 40)}${title.length > 40 ? '...' : ''}` if (title) return `${tag}: ${title.slice(0, 40)}${title.length > 40 ? '...' : ''}`
...@@ -270,7 +270,7 @@ export function getNodeDisplayName(node: XmlNode): string { ...@@ -270,7 +270,7 @@ export function getNodeDisplayName(node: XmlNode): string {
function getTextFromMixedContent(node: XmlNode): string { function getTextFromMixedContent(node: XmlNode): string {
if (node.textContent) return node.textContent if (node.textContent) return node.textContent
return node.mixedContent return node.mixedContent
.filter(item => item.type === 'text') .filter((item) => item.type === 'text')
.map(item => item.text || '') .map((item) => item.text || '')
.join('') .join('')
} }
...@@ -135,6 +135,42 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -135,6 +135,42 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
return `${idx + 1}.` return `${idx + 1}.`
} }
if (node.tagName === 'L1ITEM') { if (node.tagName === 'L1ITEM') {
// 1. 尝试向上寻找 SUBTASK 和 TOPIC
let subtaskNode: XmlNode | null = null
let topicNode: XmlNode | null = null
let curr = editorStore.nodeMap.get(node.id)
while (curr) {
if (curr.node.tagName === 'SUBTASK') {
subtaskNode = curr.node
} else if (curr.node.tagName === 'TOPIC') {
topicNode = curr.node
break
}
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
}
// 2. 如果存在 SUBTASK 且在 TOPIC 容器中,按 SUBTASK 的 (FUNC, SEQ) 进行去重编号
if (subtaskNode && topicNode) {
const subtasks = topicNode.children.filter((c) => c.tagName === 'SUBTASK')
const uniqueKeys: string[] = []
for (const sub of subtasks) {
const func = sub.attributes?.FUNC || ''
const seq = sub.attributes?.SEQ || ''
const key = `${func}-${seq}`
if (!uniqueKeys.includes(key)) {
uniqueKeys.push(key)
}
}
const currentFunc = subtaskNode.attributes?.FUNC || ''
const currentSeq = subtaskNode.attributes?.SEQ || ''
const currentKey = `${currentFunc}-${currentSeq}`
const idx = uniqueKeys.indexOf(currentKey)
if (idx !== -1) {
return `${String.fromCharCode(65 + idx)}.`
}
}
// 3. 兜底逻辑:正常在 LIST1 中的索引
const parent = props.parent const parent = props.parent
if (!parent) return 'A.' if (!parent) return 'A.'
const idx = parent.children.filter((c) => c.tagName === 'L1ITEM').findIndex((c) => c.id === node.id) const idx = parent.children.filter((c) => c.tagName === 'L1ITEM').findIndex((c) => c.id === node.id)
...@@ -179,14 +215,56 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -179,14 +215,56 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
return '•' return '•'
} }
// 获取 TOPIC / PRETOPIC 序号 // 获取 TOPIC / PRETOPIC 序号 (与 PDF 端的 XSL 解析逻辑对齐)
const getTopicSeqNum = (node: XmlNode): string => { const getTopicSeqNum = (node: XmlNode): string => {
const parent = props.parent let rootNode: XmlNode | null = null
if (!parent) return '' let isAlphaFormat = false
if (parent.tagName === 'CEP' || parent.tagName === 'TASK') { let curr = editorStore.nodeMap.get(node.id)
const topicSiblings = parent.children.filter((c) => c.tagName === 'TOPIC' || c.tagName === 'PRETOPIC') while (curr) {
const idx = topicSiblings.findIndex((c) => c.id === node.id) if (curr.node.tagName === 'CEP' || curr.node.tagName === 'TASK') {
rootNode = curr.node
isAlphaFormat = false
break
}
if (curr.node.tagName === 'JC-TASK') {
rootNode = curr.node
isAlphaFormat = true
break
}
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
}
if (!rootNode) return ''
// 在 rootNode 下收集所有符合条件的 TOPIC 和 PRETOPIC 节点
const collected: XmlNode[] = []
const traverse = (n: XmlNode) => {
if (n.tagName === 'TOPIC') {
const parentItem = editorStore.nodeMap.get(n.id)?.parent
if (parentItem && (parentItem.tagName === 'CEP' || parentItem.tagName === 'TASK' || parentItem.tagName === 'JC-TASK')) {
collected.push(n)
}
} else if (n.tagName === 'PRETOPIC') {
const parentItem = editorStore.nodeMap.get(n.id)?.parent
if (parentItem && parentItem.tagName === 'TFMATR') {
const grandParentItem = editorStore.nodeMap.get(parentItem.id)?.parent
if (
grandParentItem &&
(grandParentItem.tagName === 'CEP' || grandParentItem.tagName === 'TASK' || grandParentItem.tagName === 'JC-TASK')
) {
collected.push(n)
}
}
}
n.children.forEach(traverse)
}
traverse(rootNode)
const idx = collected.findIndex((n) => n.id === node.id)
if (idx !== -1) { if (idx !== -1) {
if (isAlphaFormat) {
return `${String.fromCharCode(65 + idx)}. `
} else {
return `${idx + 1}. ` return `${idx + 1}. `
} }
} }
...@@ -329,8 +407,8 @@ export const getSplitListChildren = (children?: XmlNode[]) => { ...@@ -329,8 +407,8 @@ export const getSplitListChildren = (children?: XmlNode[]) => {
} }
export const getCepTaskNumber = (node: XmlNode): string => { export const getCepTaskNumber = (node: XmlNode): string => {
const { CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR } = node.attributes || {} const { CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR } = node.attributes || {}
const parts = [CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR].filter(Boolean) const parts = [CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR].filter(Boolean)
return parts.join('-') return parts.join('-')
} }
......
...@@ -19,9 +19,12 @@ export function useFindReplace( ...@@ -19,9 +19,12 @@ export function useFindReplace(
const matchItemRefs = ref<any[]>([]) const matchItemRefs = ref<any[]>([])
// 每次匹配列表变化时重置 ref 数组 // 每次匹配列表变化时重置 ref 数组
watch(() => matches.value, () => { watch(
() => matches.value,
() => {
matchItemRefs.value = [] matchItemRefs.value = []
}) }
)
const scrollToActiveMatch = () => { const scrollToActiveMatch = () => {
nextTick(() => { nextTick(() => {
......
...@@ -5,9 +5,12 @@ ...@@ -5,9 +5,12 @@
v-if="visible" v-if="visible"
class="find-replace-panel absolute top-20 right-4 z-[999] bg-card/90 backdrop-blur-md border border-divider shadow-2xl rounded-xl p-4 flex flex-col space-y-3 select-none" class="find-replace-panel absolute top-20 right-4 z-[999] bg-card/90 backdrop-blur-md border border-divider shadow-2xl rounded-xl p-4 flex flex-col space-y-3 select-none"
:style="{ :style="{
width: isExpanded ? '800px' : '500px', width: computedWidth,
height: computedHeight,
transform: `translate(${dragOffset.x}px, ${dragOffset.y}px)`, transform: `translate(${dragOffset.x}px, ${dragOffset.y}px)`,
transition: dragging ? 'none' : 'width 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.1s ease-out' transition: dragging
? 'none'
: 'width 0.3s cubic-bezier(0.16, 1, 0.3, 1), height 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.1s ease-out'
}" }"
:class="{ 'is-expanded': isExpanded }" :class="{ 'is-expanded': isExpanded }"
> >
...@@ -116,14 +119,15 @@ ...@@ -116,14 +119,15 @@
</div> </div>
<!-- 匹配文本摘要预览(引入 CommonNodeDetailList 全新统一布局组件,采用 flat 平铺和响应式 dense 尺寸) --> <!-- 匹配文本摘要预览(引入 CommonNodeDetailList 全新统一布局组件,采用 flat 平铺和响应式 dense 尺寸) -->
<div v-if="matches.length > 0" class="border border-divider/50 rounded-lg overflow-hidden"> <div v-if="matches.length > 0" class="border border-divider/50 rounded-lg overflow-hidden flex-1 min-h-0 flex flex-col">
<CommonNodeDetailList <CommonNodeDetailList
v-model:selected-id="activeMatchNodeId" v-model:selected-id="activeMatchNodeId"
:node-ids="matchNodeIds" :node-ids="matchNodeIds"
:flat="true" :flat="true"
:dense="!isExpanded" :dense="!isExpanded"
mode="radio" mode="radio"
:max-height="isExpanded ? 480 : 240" :max-height="'100%'"
class="h-full"
:active-match-index="currentMatchIndex" :active-match-index="currentMatchIndex"
:node-match-stats="nodeMatchStats" :node-match-stats="nodeMatchStats"
:highlight="findQuery" :highlight="findQuery"
...@@ -136,6 +140,7 @@ ...@@ -136,6 +140,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { SearchOutline, CloseOutline, ChevronUpOutline, ChevronDownOutline, CodeOutline } from '@vicons/ionicons5' import { SearchOutline, CloseOutline, ChevronUpOutline, ChevronDownOutline, CodeOutline } from '@vicons/ionicons5'
import { useWindowSize } from '@vueuse/core'
import { useFindReplace, useDraggable } from './functionals' import { useFindReplace, useDraggable } from './functionals'
const props = defineProps<{ const props = defineProps<{
...@@ -172,6 +177,33 @@ const { ...@@ -172,6 +177,33 @@ const {
closePanel closePanel
} = useFindReplace(props, emit) } = useFindReplace(props, emit)
const { width: windowWidth, height: windowHeight } = useWindowSize()
// 计算弹框宽度,基于窗口宽度动态计算
const computedWidth = computed(() => {
if (isExpanded.value) {
// 放大模式:屏幕宽度的 55%,限制在 700px 到 1200px 之间
return `${Math.min(1200, Math.max(700, Math.round(windowWidth.value * 0.55)))}px`
} else {
// 折叠模式:屏幕宽度的 30%,限制在 380px 到 600px 之间
return `${Math.min(600, Math.max(380, Math.round(windowWidth.value * 0.3)))}px`
}
})
// 计算弹框高度,基于窗口高度和是否有匹配结果动态计算
const computedHeight = computed(() => {
if (matches.value.length === 0) {
return 'auto'
}
if (isExpanded.value) {
// 放大模式:屏幕高度的 70%,限制在 550px 到 900px 之间
return `${Math.min(900, Math.max(550, Math.round(windowHeight.value * 0.7)))}px`
} else {
// 折叠模式:屏幕高度的 45%,限制在 350px 到 550px 之间
return `${Math.min(550, Math.max(350, Math.round(windowHeight.value * 0.45)))}px`
}
})
const matchNodeIds = computed(() => { const matchNodeIds = computed(() => {
const ids = matches.value.map((m) => m.nodeId) const ids = matches.value.map((m) => m.nodeId)
return Array.from(new Set(ids)) return Array.from(new Set(ids))
......
...@@ -14,21 +14,32 @@ export const ESTIMATED_HEIGHT = 200 ...@@ -14,21 +14,32 @@ export const ESTIMATED_HEIGHT = 200
/** 按节点类型获取预估高度(用于虚拟列表初始高度计算) */ /** 按节点类型获取预估高度(用于虚拟列表初始高度计算) */
export const getEstimatedHeight = (tagName: string): number => { export const getEstimatedHeight = (tagName: string): number => {
switch (tagName) { switch (tagName) {
case 'TABLE': return 400 // 表格通常较高 case 'TABLE':
case 'GRAPHIC': return 300 // 图片/图纸 return 400 // 表格通常较高
case 'WARNING': return 180 // 警告块 case 'GRAPHIC':
case 'CAUTION': return 180 return 300 // 图片/图纸
case 'NOTE': return 150 case 'WARNING':
case 'PRETOPIC': return 160 // 模板段落 return 180 // 警告块
case 'CAUTION':
return 180
case 'NOTE':
return 150
case 'PRETOPIC':
return 160 // 模板段落
case 'UNLIST': case 'UNLIST':
case 'LIST1': case 'LIST1':
case 'LIST2': case 'LIST2':
case 'LIST3': return 200 // 列表 case 'LIST3':
return 200 // 列表
case 'PARA': case 'PARA':
case 'PARAC': return 80 // 普通段落 case 'PARAC':
case 'SMUC-HEADER': return 120 return 80 // 普通段落
case 'FINLIST': return 100 case 'SMUC-HEADER':
default: return ESTIMATED_HEIGHT return 120
case 'FINLIST':
return 100
default:
return ESTIMATED_HEIGHT
} }
} }
......
...@@ -41,7 +41,12 @@ ...@@ -41,7 +41,12 @@
</div> </div>
<!-- 文档编辑区(虚拟滚动容器) --> <!-- 文档编辑区(虚拟滚动容器) -->
<div ref="viewportRef" class="flex-1 overflow-y-auto min-h-0 leading-relaxed relative" @scroll="handleScroll" @contextmenu="handleContextMenu"> <div
ref="viewportRef"
class="flex-1 overflow-y-auto min-h-0 leading-relaxed relative"
@scroll="handleScroll"
@contextmenu="handleContextMenu"
>
<!-- 占位撑高,模拟全量内容总高度 --> <!-- 占位撑高,模拟全量内容总高度 -->
<div :style="{ height: totalHeight + 'px', position: 'relative' }"> <div :style="{ height: totalHeight + 'px', position: 'relative' }">
<!-- 仅渲染可视区块,通过 translateY 定位 --> <!-- 仅渲染可视区块,通过 translateY 定位 -->
......
...@@ -99,7 +99,6 @@ ...@@ -99,7 +99,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CheckmarkCircleOutline } from '@vicons/ionicons5' import { CheckmarkCircleOutline } from '@vicons/ionicons5'
import { useBatchTranslate } from './functionals' import { useBatchTranslate } from './functionals'
......
import type { FormRules } from 'naive-ui'
export const SIGN_FORM_RULES: FormRules = {
ckLevel: { required: true, message: '请选择签字等级', trigger: ['blur', 'change'] }
}
import type { FormInst } from 'naive-ui'
import { SIGN_FORM_RULES } from '../constants'
export function useCreateSignoffModal(emit: (event: 'confirm', ckLevel: string) => void) {
const show = ref(false)
const formRef = ref<FormInst | null>(null)
const form = reactive({
ckLevel: ''
})
const ckLevelOptions = computed(() => {
const rule = getElementRule('SIGNOFF')
const attrRule = rule?.attributes?.['CK-LEVEL']
if (attrRule && attrRule.enumValues) {
return attrRule.enumValues.map((val: string) => ({ label: `${val} 级`, value: val }))
}
return [
{ label: 'A 级', value: 'A' },
{ label: 'B 级', value: 'B' }
]
})
const open = () => {
if (ckLevelOptions.value.length > 0) {
form.ckLevel = ckLevelOptions.value[0].value
} else {
form.ckLevel = ''
}
show.value = true
}
const handleConfirm = async () => {
try {
await formRef.value?.validate()
emit('confirm', form.ckLevel)
show.value = false
} catch (err) {
// validation failed
}
}
return {
show,
formRef,
form,
rules: SIGN_FORM_RULES,
ckLevelOptions,
open,
handleConfirm
}
}
<template>
<CommonModal v-model="show" title="插入签字点" :width="380" @confirm="handleConfirm">
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
<div class="flex flex-col gap-4 py-2">
<n-form-item label="签字等级 (CK-LEVEL)" path="ckLevel">
<CommonSelect v-model:value="form.ckLevel" :options="ckLevelOptions" />
</n-form-item>
</div>
</n-form>
</CommonModal>
</template>
<script setup lang="ts">
import { useCreateSignoffModal } from './functionals'
const emit = defineEmits<{
confirm: [ckLevel: string]
}>()
const { show, formRef, form, rules, ckLevelOptions, open, handleConfirm } = useCreateSignoffModal(emit)
defineExpose({ open })
</script>
...@@ -30,11 +30,7 @@ ...@@ -30,11 +30,7 @@
</div> </div>
<n-form-item label="单元格初始化段落节点" path="cellChildTags"> <n-form-item label="单元格初始化段落节点" path="cellChildTags">
<div class="w-full bg-fill-2 p-3 rounded border border-divider"> <div class="w-full bg-fill-2 p-3 rounded border border-divider">
<CommonCheckbox <CommonCheckbox v-model:value="form.cellChildTags" :options="cellChildOptions" :space-size="24" />
v-model:value="form.cellChildTags"
:options="cellChildOptions"
:space-size="24"
/>
</div> </div>
</n-form-item> </n-form-item>
</div> </div>
......
...@@ -38,26 +38,12 @@ ...@@ -38,26 +38,12 @@
</div> </div>
<div class="flex flex-col gap-2 max-h-[200px] overflow-y-auto pr-1"> <div class="flex flex-col gap-2 max-h-[200px] overflow-y-auto pr-1">
<div <div v-for="(pair, index) in form.tag_pairs" :key="index" class="flex items-center gap-2">
v-for="(pair, index) in form.tag_pairs" <n-input v-model:value="pair.en_tag" placeholder="源英文标签 (例: PARA)" size="small" class="flex-1" />
:key="index"
class="flex items-center gap-2"
>
<n-input
v-model:value="pair.en_tag"
placeholder="源英文标签 (例: PARA)"
size="small"
class="flex-1"
/>
<n-icon class="text-color3"> <n-icon class="text-color3">
<arrow-forward-outline /> <arrow-forward-outline />
</n-icon> </n-icon>
<n-input <n-input v-model:value="pair.cn_tag" placeholder="目标中文标签 (例: PARAC)" size="small" class="flex-1" />
v-model:value="pair.cn_tag"
placeholder="目标中文标签 (例: PARAC)"
size="small"
class="flex-1"
/>
<CommonButton <CommonButton
size="small" size="small"
type="error" type="error"
...@@ -89,9 +75,13 @@ ...@@ -89,9 +75,13 @@
<div> <div>
<div class="text-base font-bold text-color1">对照对提取处理成功!</div> <div class="text-base font-bold text-color1">对照对提取处理成功!</div>
<div class="text-xs text-color3"> <div class="text-xs text-color3">
共提取中英双语对:<span class="text-primary font-bold text-sm">{{ totalCount }}</span> 共提取中英双语对:
<span class="text-primary font-bold text-sm">{{ totalCount }}</span>
<span v-if="form.import_to_db" class="ml-2"> <span v-if="form.import_to_db" class="ml-2">
,导入数据库:<span class="text-success font-bold text-sm">{{ importCount }}</span> ,导入数据库:
<span class="text-success font-bold text-sm">{{ importCount }}</span>
</span> </span>
</div> </div>
</div> </div>
...@@ -99,16 +89,8 @@ ...@@ -99,16 +89,8 @@
<!-- 提取数据预览列表 --> <!-- 提取数据预览列表 -->
<div class="border border-divider rounded-lg overflow-hidden"> <div class="border border-divider rounded-lg overflow-hidden">
<div class="bg-fill-3 px-3 py-2 text-xs font-bold text-color2 border-b border-divider"> <div class="bg-fill-3 px-3 py-2 text-xs font-bold text-color2 border-b border-divider">数据预览 (展示前 10 条)</div>
数据预览 (展示前 10 条) <n-data-table :columns="previewColumns" :data="previewList" :max-height="250" size="small" :bordered="false" />
</div>
<n-data-table
:columns="previewColumns"
:data="previewList"
:max-height="250"
size="small"
:bordered="false"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -38,11 +38,7 @@ export function useInsertFragmentModal() { ...@@ -38,11 +38,7 @@ export function useInsertFragmentModal() {
isSaving.value = true isSaving.value = true
try { try {
const count = editorStore.insertXmlFragment( const count = editorStore.insertXmlFragment(xmlContent.value.trim(), insertModeSetting.value, targetNodeIdSetting.value)
xmlContent.value.trim(),
insertModeSetting.value,
targetNodeIdSetting.value
)
window.$message?.success(`成功插入 ${count} 个 XML 节点`) window.$message?.success(`成功插入 ${count} 个 XML 节点`)
visible.value = false visible.value = false
} catch (err: any) { } catch (err: any) {
......
<template> <template>
<CommonModal <CommonModal v-model="visible" title="插入 XML 片段" :width="600" :loading="isSaving" confirm-text="插入" @confirm="handleConfirm">
v-model="visible"
title="插入 XML 片段"
:width="600"
:loading="isSaving"
confirm-text="插入"
@confirm="handleConfirm"
>
<div class="flex flex-col space-y-3 p-1"> <div class="flex flex-col space-y-3 p-1">
<div class="text-xs text-color3"> <div class="text-xs text-color3">
请输入从其他地方复制的 XML 节点片段,系统将自动解析节点结构,并根据当前选中节点及 DTD 架构规则进行兼容性校验: 请输入从其他地方复制的 XML 节点片段,系统将自动解析节点结构,并根据当前选中节点及 DTD 架构规则进行兼容性校验:
</div> </div>
<n-input <n-input v-model:value="xmlContent" type="textarea" rows="10" placeholder="例如:<PARAC>测试记录行</PARAC>" />
v-model:value="xmlContent"
type="textarea"
rows="10"
placeholder="例如:<PARAC>测试记录行</PARAC>"
/>
</div> </div>
</CommonModal> </CommonModal>
</template> </template>
...@@ -24,13 +12,7 @@ ...@@ -24,13 +12,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useInsertFragmentModal } from './functionals' import { useInsertFragmentModal } from './functionals'
const { const { visible, xmlContent, isSaving, open, handleConfirm } = useInsertFragmentModal()
visible,
xmlContent,
isSaving,
open,
handleConfirm
} = useInsertFragmentModal()
defineExpose({ defineExpose({
open, open,
......
...@@ -57,7 +57,6 @@ export function useSearchTranslate() { ...@@ -57,7 +57,6 @@ export function useSearchTranslate() {
{ label: '中译英 (ZH -> EN)', value: 'zh_to_en' } { label: '中译英 (ZH -> EN)', value: 'zh_to_en' }
] ]
const open = () => { const open = () => {
showModal.value = true showModal.value = true
activeTab.value = 'search' activeTab.value = 'search'
...@@ -264,7 +263,6 @@ export function useSearchTranslate() { ...@@ -264,7 +263,6 @@ export function useSearchTranslate() {
} }
} }
const copyText = async (text: string) => { const copyText = async (text: string) => {
try { try {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
......
...@@ -136,21 +136,11 @@ ...@@ -136,21 +136,11 @@
<n-form label-placement="left" label-width="80" size="medium"> <n-form label-placement="left" label-width="80" size="medium">
<n-form-item label="英文原文"> <n-form-item label="英文原文">
<n-input <n-input v-model:value="addForm.text" type="textarea" :rows="3" placeholder="请输入需要保存的英文术语或原文段落..." />
v-model:value="addForm.text"
type="textarea"
:rows="3"
placeholder="请输入需要保存的英文术语或原文段落..."
/>
</n-form-item> </n-form-item>
<n-form-item label="中文翻译"> <n-form-item label="中文翻译">
<n-input <n-input v-model:value="addForm.translation" type="textarea" :rows="3" placeholder="请输入对应的中文标准翻译..." />
v-model:value="addForm.translation"
type="textarea"
:rows="3"
placeholder="请输入对应的中文标准翻译..."
/>
</n-form-item> </n-form-item>
<div class="flex justify-end mt-2"> <div class="flex justify-end mt-2">
...@@ -250,7 +240,12 @@ ...@@ -250,7 +240,12 @@
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-xs text-color3">选择时间段</span> <span class="text-xs text-color3">选择时间段</span>
<CommonDatePicker v-model:start="deleteForm.startTime" v-model:end="deleteForm.endTime" type="datetimerange" size="small" /> <CommonDatePicker
v-model:start="deleteForm.startTime"
v-model:end="deleteForm.endTime"
type="datetimerange"
size="small"
/>
</div> </div>
</div> </div>
</div> </div>
...@@ -265,7 +260,9 @@ ...@@ -265,7 +260,9 @@
</div> </div>
<div class="flex justify-end mt-2"> <div class="flex justify-end mt-2">
<CommonButton type="warning" size="medium" :loading="loading" @click="handleDeleteByFilter">执行批量删除</CommonButton> <CommonButton type="warning" size="medium" :loading="loading" @click="handleDeleteByFilter">
执行批量删除
</CommonButton>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -7,7 +7,7 @@ export const TOOLBAR_TITLE = 'XML 编辑工具栏' ...@@ -7,7 +7,7 @@ export const TOOLBAR_TITLE = 'XML 编辑工具栏'
export const GREEN_BUTTONS: any[] = [ export const GREEN_BUTTONS: any[] = [
// { label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline }, // { label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline },
{ label: '插入表格', tag: 'TABLE', icon: GridOutline } { label: '插入表格', tag: 'TABLE', icon: GridOutline },
// { label: '插入模板', tag: 'PRETOPIC', icon: DocumentTextOutline }, // { label: '插入模板', tag: 'PRETOPIC', icon: DocumentTextOutline },
// { label: '插入签字点', tag: 'SIGNOFF', icon: CreateOutline } { label: '插入签字点', tag: 'SIGNOFF', icon: CreateOutline }
] ]
import { useEditorStore, createTableStructure } from '@/store/editor' import { useEditorStore, createTableStructure } from '@/store/editor'
import { useAppStore } from '@/store/app/index' import { useAppStore } from '@/store/app/index'
import { canAddChild } from '@/utils/dtdManager' import { canAddChild } from '@/utils/dtdManager'
import type { XmlNode } from '@/types/xmlNode'
/** /**
* EditorToolbar 组件级业务逻辑 Hook * EditorToolbar 组件级业务逻辑 Hook
...@@ -13,6 +14,7 @@ export function useEditorToolbar(emit: any) { ...@@ -13,6 +14,7 @@ export function useEditorToolbar(emit: any) {
const fileInputRef = ref<HTMLInputElement | null>(null) const fileInputRef = ref<HTMLInputElement | null>(null)
const isUploading = ref(false) const isUploading = ref(false)
const createTableModalRef = ref<any>(null) const createTableModalRef = ref<any>(null)
const createSignoffModalRef = ref<any>(null)
const canUndo = computed(() => editorStore.undoStack.length > 0) const canUndo = computed(() => editorStore.undoStack.length > 0)
const canRedo = computed(() => editorStore.redoStack.length > 0) const canRedo = computed(() => editorStore.redoStack.length > 0)
...@@ -46,11 +48,28 @@ export function useEditorToolbar(emit: any) { ...@@ -46,11 +48,28 @@ export function useEditorToolbar(emit: any) {
if (tag === 'TABLE') { if (tag === 'TABLE') {
createTableModalRef.value?.open() createTableModalRef.value?.open()
} else if (tag === 'SIGNOFF') {
createSignoffModalRef.value?.open()
} else { } else {
editorStore.insertNode(tag, insertBelow.value) editorStore.insertNode(tag, insertBelow.value)
} }
} }
const handleCreateSignoffConfirm = (ckLevel: string) => {
const signoffNode: XmlNode = {
id: crypto.randomUUID(),
tagName: 'SIGNOFF',
attributes: {
'CK-LEVEL': ckLevel
},
children: [],
textContent: '',
mixedContent: [],
parentId: undefined as any
}
editorStore.insertNode('SIGNOFF', insertBelow.value, signoffNode)
}
const handleCreateTableConfirm = (rows: number, cols: number, cellChildTags: string[]) => { const handleCreateTableConfirm = (rows: number, cols: number, cellChildTags: string[]) => {
const tableNode = createTableStructure(rows, cols, cellChildTags) const tableNode = createTableStructure(rows, cols, cellChildTags)
editorStore.insertNode('TABLE', insertBelow.value, tableNode) editorStore.insertNode('TABLE', insertBelow.value, tableNode)
...@@ -116,6 +135,8 @@ export function useEditorToolbar(emit: any) { ...@@ -116,6 +135,8 @@ export function useEditorToolbar(emit: any) {
triggerUpload, triggerUpload,
createTableModalRef, createTableModalRef,
handleCreateTableConfirm, handleCreateTableConfirm,
createSignoffModalRef,
handleCreateSignoffConfirm,
batchTranslateModalRef, batchTranslateModalRef,
extractTranslateModalRef, extractTranslateModalRef,
searchTranslateModalRef, searchTranslateModalRef,
......
...@@ -156,6 +156,9 @@ ...@@ -156,6 +156,9 @@
<!-- 插入表格弹窗 --> <!-- 插入表格弹窗 -->
<CreateTableModal ref="createTableModalRef" @confirm="handleCreateTableConfirm" /> <CreateTableModal ref="createTableModalRef" @confirm="handleCreateTableConfirm" />
<!-- 插入签字点弹窗 -->
<CreateSignoffModal ref="createSignoffModalRef" @confirm="handleCreateSignoffConfirm" />
<!-- 批量翻译弹窗 --> <!-- 批量翻译弹窗 -->
<BatchTranslateModal ref="batchTranslateModalRef" /> <BatchTranslateModal ref="batchTranslateModalRef" />
...@@ -188,6 +191,7 @@ import { GREEN_BUTTONS } from './constants' ...@@ -188,6 +191,7 @@ import { GREEN_BUTTONS } from './constants'
import SettingsDrawer from '@/layouts/components/SettingsDrawer.vue' import SettingsDrawer from '@/layouts/components/SettingsDrawer.vue'
import InsertFragmentModal from './components/InsertFragmentModal/index.vue' import InsertFragmentModal from './components/InsertFragmentModal/index.vue'
import CreateTableModal from './components/CreateTableModal/index.vue' import CreateTableModal from './components/CreateTableModal/index.vue'
import CreateSignoffModal from './components/CreateSignoffModal/index.vue'
import BatchTranslateModal from './components/BatchTranslateModal/index.vue' import BatchTranslateModal from './components/BatchTranslateModal/index.vue'
import ExtractTranslateModal from './components/ExtractTranslateModal/index.vue' import ExtractTranslateModal from './components/ExtractTranslateModal/index.vue'
import SearchTranslateModal from './components/SearchTranslateModal/index.vue' import SearchTranslateModal from './components/SearchTranslateModal/index.vue'
...@@ -207,6 +211,8 @@ const { ...@@ -207,6 +211,8 @@ const {
triggerUpload, triggerUpload,
createTableModalRef, createTableModalRef,
handleCreateTableConfirm, handleCreateTableConfirm,
createSignoffModalRef,
handleCreateSignoffConfirm,
batchTranslateModalRef, batchTranslateModalRef,
extractTranslateModalRef, extractTranslateModalRef,
searchTranslateModalRef searchTranslateModalRef
......
...@@ -19,11 +19,16 @@ export function useListEditor() { ...@@ -19,11 +19,16 @@ export function useListEditor() {
const getItemTagName = (containerTag: string): string => { const getItemTagName = (containerTag: string): string => {
switch (containerTag) { switch (containerTag) {
case 'LIST1': return 'L1ITEM' case 'LIST1':
case 'LIST2': return 'L2ITEM' return 'L1ITEM'
case 'LIST3': return 'L3ITEM' case 'LIST2':
case 'UNLIST': return 'UNLITEM' return 'L2ITEM'
default: return 'L1ITEM' case 'LIST3':
return 'L3ITEM'
case 'UNLIST':
return 'UNLITEM'
default:
return 'L1ITEM'
} }
} }
...@@ -32,10 +37,10 @@ export function useListEditor() { ...@@ -32,10 +37,10 @@ export function useListEditor() {
const itemTagName = getItemTagName(node.tagName) const itemTagName = getItemTagName(node.tagName)
return node.children return node.children
.filter(c => c.tagName === itemTagName) .filter((c) => c.tagName === itemTagName)
.map(itemNode => { .map((itemNode) => {
let text = itemNode.textContent || '' let text = itemNode.textContent || ''
const firstPara = itemNode.children.find(c => PARA_TAGS.includes(c.tagName)) const firstPara = itemNode.children.find((c) => PARA_TAGS.includes(c.tagName))
if (firstPara) { if (firstPara) {
text = firstPara.textContent || '' text = firstPara.textContent || ''
} }
...@@ -49,10 +54,10 @@ export function useListEditor() { ...@@ -49,10 +54,10 @@ export function useListEditor() {
} }
const updateItemText = (node: XmlNode, itemId: string, text: string): void => { const updateItemText = (node: XmlNode, itemId: string, text: string): void => {
const itemNode = node.children.find(c => c.id === itemId) const itemNode = node.children.find((c) => c.id === itemId)
if (!itemNode) return if (!itemNode) return
const para = itemNode.children.find(c => PARA_TAGS.includes(c.tagName)) const para = itemNode.children.find((c) => PARA_TAGS.includes(c.tagName))
if (para) { if (para) {
para.textContent = text para.textContent = text
if (para.mixedContent.length > 0) { if (para.mixedContent.length > 0) {
...@@ -96,7 +101,7 @@ export function useListEditor() { ...@@ -96,7 +101,7 @@ export function useListEditor() {
} }
const deleteItem = (node: XmlNode, itemId: string): void => { const deleteItem = (node: XmlNode, itemId: string): void => {
const idx = node.children.findIndex(c => c.id === itemId) const idx = node.children.findIndex((c) => c.id === itemId)
if (idx !== -1) { if (idx !== -1) {
node.children.splice(idx, 1) node.children.splice(idx, 1)
store.triggerSync() store.triggerSync()
...@@ -104,7 +109,7 @@ export function useListEditor() { ...@@ -104,7 +109,7 @@ export function useListEditor() {
} }
const moveItem = (node: XmlNode, itemId: string, direction: 'up' | 'down'): void => { const moveItem = (node: XmlNode, itemId: string, direction: 'up' | 'down'): void => {
const idx = node.children.findIndex(c => c.id === itemId) const idx = node.children.findIndex((c) => c.id === itemId)
if (idx === -1) return if (idx === -1) return
const target = direction === 'up' ? idx - 1 : idx + 1 const target = direction === 'up' ? idx - 1 : idx + 1
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
<div class="flex items-center justify-between pb-2 border-b border-divider"> <div class="flex items-center justify-between pb-2 border-b border-divider">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<CommonTag type="info" size="small">{{ node.tagName }}</CommonTag> <CommonTag type="info" size="small">{{ node.tagName }}</CommonTag>
<span class="text-xs text-color3"> <span class="text-xs text-color3">{{ isOrdered ? '有序' : '无序' }}列表编辑器 (子项共: {{ listItems.length }} 个)</span>
{{ isOrdered ? '有序' : '无序' }}列表编辑器 (子项共: {{ listItems.length }} 个)
</span>
</div> </div>
<CommonButton type="primary" size="tiny" secondary @click="handleAddItem"> <CommonButton type="primary" size="tiny" secondary @click="handleAddItem">
<template #icon><n-icon><add-outline /></n-icon></template> <template #icon>
<n-icon><add-outline /></n-icon>
</template>
添加列表项 添加列表项
</CommonButton> </CommonButton>
</div> </div>
...@@ -41,13 +41,19 @@ ...@@ -41,13 +41,19 @@
<!-- 操作按钮组 --> <!-- 操作按钮组 -->
<div class="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div class="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
<CommonButton size="tiny" quaternary circle @click="handleMove(item.id, 'up')" :disabled="index === 0"> <CommonButton size="tiny" quaternary circle @click="handleMove(item.id, 'up')" :disabled="index === 0">
<template #icon><n-icon><arrow-up-outline /></n-icon></template> <template #icon>
<n-icon><arrow-up-outline /></n-icon>
</template>
</CommonButton> </CommonButton>
<CommonButton size="tiny" quaternary circle @click="handleMove(item.id, 'down')" :disabled="index === listItems.length - 1"> <CommonButton size="tiny" quaternary circle @click="handleMove(item.id, 'down')" :disabled="index === listItems.length - 1">
<template #icon><n-icon><arrow-down-outline /></n-icon></template> <template #icon>
<n-icon><arrow-down-outline /></n-icon>
</template>
</CommonButton> </CommonButton>
<CommonButton size="tiny" quaternary circle type="error" @click="handleDelete(item.id)"> <CommonButton size="tiny" quaternary circle type="error" @click="handleDelete(item.id)">
<template #icon><n-icon><trash-outline /></n-icon></template> <template #icon>
<n-icon><trash-outline /></n-icon>
</template>
</CommonButton> </CommonButton>
</div> </div>
</div> </div>
...@@ -76,9 +82,13 @@ const isOrdered = computed(() => ORDERED_LIST_TAGS.includes(props.node.tagName)) ...@@ -76,9 +82,13 @@ const isOrdered = computed(() => ORDERED_LIST_TAGS.includes(props.node.tagName))
const listItems = ref(parseListItems(props.node)) const listItems = ref(parseListItems(props.node))
watch(() => props.node, (newVal) => { watch(
() => props.node,
(newVal) => {
listItems.value = parseListItems(newVal) listItems.value = parseListItems(newVal)
}, { deep: true, immediate: true }) },
{ deep: true, immediate: true }
)
function handleTextBlur(itemId: string, e: FocusEvent) { function handleTextBlur(itemId: string, e: FocusEvent) {
const el = e.target as HTMLElement const el = e.target as HTMLElement
...@@ -108,5 +118,4 @@ function handleMove(itemId: string, direction: 'up' | 'down') { ...@@ -108,5 +118,4 @@ function handleMove(itemId: string, direction: 'up' | 'down') {
} }
</script> </script>
<style scoped> <style scoped></style>
</style>
...@@ -142,7 +142,17 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -142,7 +142,17 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
const defs = getElementAttributes(node.tagName) const defs = getElementAttributes(node.tagName)
const attrs: Record<string, string | null> = {} const attrs: Record<string, string | null> = {}
for (const name of Object.keys(defs)) { for (const name of Object.keys(defs)) {
attrs[name] = node.attributes[name] !== undefined ? node.attributes[name] : null const matchedKey = Object.keys(node.attributes).find((k) => k.toUpperCase() === name.toUpperCase())
let val = matchedKey ? node.attributes[matchedKey] : null
// 如果定义了枚举值,则进行不区分大小写匹配,归一化为 DTD 标准枚举值
if (val && defs[name].enumValues && defs[name].enumValues.length > 0) {
const matchedEnum = defs[name].enumValues.find((ev) => ev.toUpperCase() === val!.toUpperCase())
if (matchedEnum) {
val = matchedEnum
}
}
attrs[name] = val
} }
form.attrs = attrs form.attrs = attrs
} }
...@@ -180,7 +190,15 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -180,7 +190,15 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
store.saveSnapshot() store.saveSnapshot()
const cleanAttrs: Record<string, string> = {} const cleanAttrs: Record<string, string> = { ...targetNode.attributes }
// 清理掉 DTD 声明的属性中本次重新编辑或设置为空的属性(不区分大小写匹配)
for (const name of Object.keys(form.attrs)) {
const matchedKey = Object.keys(cleanAttrs).find((k) => k.toUpperCase() === name.toUpperCase())
if (matchedKey) {
delete cleanAttrs[matchedKey]
}
}
// 写入本次编辑后的有效属性
for (const [k, v] of Object.entries(form.attrs)) { for (const [k, v] of Object.entries(form.attrs)) {
if (v !== null && v !== undefined && v !== '') { if (v !== null && v !== undefined && v !== '') {
cleanAttrs[k] = v cleanAttrs[k] = v
...@@ -194,9 +212,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -194,9 +212,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
targetNode.mixedContent[textIdx].text = form.textContent targetNode.mixedContent[textIdx].text = form.textContent
} }
// 同步更新父节点的 textContent // 同步更新父节点的 textContent
targetNode.textContent = targetNode.mixedContent targetNode.textContent = targetNode.mixedContent.map((item) => (item.type === 'text' ? item.text || '' : '')).join('')
.map((item) => (item.type === 'text' ? item.text || '' : ''))
.join('')
store.rebuildNodeMap() store.rebuildNodeMap()
if (store.selectedNodeId === addNodeTargetId.value) { if (store.selectedNodeId === addNodeTargetId.value) {
...@@ -221,9 +237,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -221,9 +237,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
targetNode.mixedContent.unshift({ type: 'text', text: form.textContent }) targetNode.mixedContent.unshift({ type: 'text', text: form.textContent })
} }
// 重新拼合 textContent,确保与 mixedContent 数据一致 // 重新拼合 textContent,确保与 mixedContent 数据一致
targetNode.textContent = targetNode.mixedContent targetNode.textContent = targetNode.mixedContent.map((item) => (item.type === 'text' ? item.text || '' : '')).join('')
.map((item) => (item.type === 'text' ? item.text || '' : ''))
.join('')
} else { } else {
targetNode.textContent = form.textContent targetNode.textContent = form.textContent
} }
...@@ -243,9 +257,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -243,9 +257,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
targetNode.mixedContent.push({ type: 'text', text: targetNode.textContent }) targetNode.mixedContent.push({ type: 'text', text: targetNode.textContent })
} }
targetNode.mixedContent.push({ type: 'text', text: form.textContent }) targetNode.mixedContent.push({ type: 'text', text: form.textContent })
targetNode.textContent = targetNode.mixedContent targetNode.textContent = targetNode.mixedContent.map((item) => (item.type === 'text' ? item.text || '' : '')).join('')
.map((item) => (item.type === 'text' ? item.text || '' : ''))
.join('')
store.rebuildNodeMap() store.rebuildNodeMap()
const newTextIdx = targetNode.mixedContent.length - 1 const newTextIdx = targetNode.mixedContent.length - 1
store.setSelectedNodeId(`${targetNode.id}-txt-${newTextIdx}`) store.setSelectedNodeId(`${targetNode.id}-txt-${newTextIdx}`)
...@@ -270,9 +282,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -270,9 +282,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
destParent.mixedContent.splice(insertIdx, 0, { type: 'text', text: form.textContent }) destParent.mixedContent.splice(insertIdx, 0, { type: 'text', text: form.textContent })
} }
} }
destParent.textContent = destParent.mixedContent destParent.textContent = destParent.mixedContent.map((item) => (item.type === 'text' ? item.text || '' : '')).join('')
.map((item) => (item.type === 'text' ? item.text || '' : ''))
.join('')
store.rebuildNodeMap() store.rebuildNodeMap()
const newTextIdx = destParent.mixedContent.findIndex((item) => item.type === 'text' && item.text === form.textContent) const newTextIdx = destParent.mixedContent.findIndex((item) => item.type === 'text' && item.text === form.textContent)
...@@ -308,9 +318,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -308,9 +318,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
} }
targetNode.mixedContent.push({ type: 'element', nodeId: newNode.id }) targetNode.mixedContent.push({ type: 'element', nodeId: newNode.id })
// 重新拼接父节点的 textContent,以防混合渲染时直接被覆盖 // 重新拼接父节点的 textContent,以防混合渲染时直接被覆盖
targetNode.textContent = targetNode.mixedContent targetNode.textContent = targetNode.mixedContent.map((item) => (item.type === 'text' ? item.text || '' : '')).join('')
.map((item) => (item.type === 'text' ? item.text || '' : ''))
.join('')
} }
} else { } else {
let destParent: XmlNode | null = null let destParent: XmlNode | null = null
...@@ -333,7 +341,10 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -333,7 +341,10 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
destParent.children.splice(insertIdx, 0, newNode) destParent.children.splice(insertIdx, 0, newNode)
const mixedIdx = destParent.mixedContent.findIndex((item) => item.nodeId === addNodeTargetId.value) const mixedIdx = destParent.mixedContent.findIndex((item) => item.nodeId === addNodeTargetId.value)
if (mixedIdx !== -1) { if (mixedIdx !== -1) {
destParent.mixedContent.splice(addNodeMode.value === 'before' ? mixedIdx : mixedIdx + 1, 0, { type: 'element', nodeId: newNode.id }) destParent.mixedContent.splice(addNodeMode.value === 'before' ? mixedIdx : mixedIdx + 1, 0, {
type: 'element',
nodeId: newNode.id
})
} }
} }
} }
......
...@@ -17,7 +17,7 @@ export function useBatchDeleteConfirmModal(emit: (event: 'confirm') => void) { ...@@ -17,7 +17,7 @@ export function useBatchDeleteConfirmModal(emit: (event: 'confirm') => void) {
blockedNodes.value = blocked blockedNodes.value = blocked
safeNodes.value = safeIds.map(id => { safeNodes.value = safeIds.map((id) => {
const item = nodeMap.get(id) const item = nodeMap.get(id)
const node = item?.node const node = item?.node
const tagName = node?.tagName || '未知' const tagName = node?.tagName || '未知'
...@@ -39,28 +39,28 @@ export function useBatchDeleteConfirmModal(emit: (event: 'confirm') => void) { ...@@ -39,28 +39,28 @@ export function useBatchDeleteConfirmModal(emit: (event: 'confirm') => void) {
// 计算全选状态 // 计算全选状态
const isAllChecked = computed(() => { const isAllChecked = computed(() => {
if (safeNodes.value.length === 0) return false if (safeNodes.value.length === 0) return false
return safeNodes.value.every(item => item.checked) return safeNodes.value.every((item) => item.checked)
}) })
const isIndeterminate = computed(() => { const isIndeterminate = computed(() => {
if (safeNodes.value.length === 0) return false if (safeNodes.value.length === 0) return false
const checkedCount = safeNodes.value.filter(item => item.checked).length const checkedCount = safeNodes.value.filter((item) => item.checked).length
return checkedCount > 0 && checkedCount < safeNodes.value.length return checkedCount > 0 && checkedCount < safeNodes.value.length
}) })
const checkedCount = computed(() => { const checkedCount = computed(() => {
return safeNodes.value.filter(item => item.checked).length return safeNodes.value.filter((item) => item.checked).length
}) })
const toggleCheckAll = (checked: boolean) => { const toggleCheckAll = (checked: boolean) => {
safeNodes.value.forEach(item => { safeNodes.value.forEach((item) => {
item.checked = checked item.checked = checked
}) })
} }
// 确认删除选中节点 // 确认删除选中节点
const handleConfirm = () => { const handleConfirm = () => {
const idsToDelete = safeNodes.value.filter(item => item.checked).map(item => item.id) const idsToDelete = safeNodes.value.filter((item) => item.checked).map((item) => item.id)
if (idsToDelete.length > 0) { if (idsToDelete.length > 0) {
editorStore.batchDeleteMultipleNodes(idsToDelete) editorStore.batchDeleteMultipleNodes(idsToDelete)
window.$message?.success('批量选择性删除成功') window.$message?.success('批量选择性删除成功')
...@@ -100,10 +100,7 @@ const getNodeDisplayName = (node: XmlNode): string => { ...@@ -100,10 +100,7 @@ const getNodeDisplayName = (node: XmlNode): string => {
return `<${node.tagName}>${suffix}` return `<${node.tagName}>${suffix}`
} }
const getNodeParentPath = ( const getNodeParentPath = (nodeId: string, nodeMap: Map<string, { node: XmlNode; parent: XmlNode | null }>): string => {
nodeId: string,
nodeMap: Map<string, { node: XmlNode; parent: XmlNode | null }>
): string => {
const path: string[] = [] const path: string[] = []
let currentId: string | null = nodeId let currentId: string | null = nodeId
while (currentId) { while (currentId) {
...@@ -118,11 +115,7 @@ const getNodeParentPath = ( ...@@ -118,11 +115,7 @@ const getNodeParentPath = (
return path.join(' > ') return path.join(' > ')
} }
const isAncestorSelected = ( const isAncestorSelected = (nodeId: string, selectedSet: Set<string>, nodeMap: Map<string, { node: XmlNode; parent: XmlNode | null }>): boolean => {
nodeId: string,
selectedSet: Set<string>,
nodeMap: Map<string, { node: XmlNode; parent: XmlNode | null }>
): boolean => {
let currentId: string | null = nodeId let currentId: string | null = nodeId
while (currentId) { while (currentId) {
if (selectedSet.has(currentId)) { if (selectedSet.has(currentId)) {
...@@ -134,10 +127,7 @@ const isAncestorSelected = ( ...@@ -134,10 +127,7 @@ const isAncestorSelected = (
return false return false
} }
const partitionBatchDelete = ( const partitionBatchDelete = (selectedNodeIds: string[], nodeMap: Map<string, { node: XmlNode; parent: XmlNode | null }>) => {
selectedNodeIds: string[],
nodeMap: Map<string, { node: XmlNode; parent: XmlNode | null }>
) => {
const safeIds: string[] = [] const safeIds: string[] = []
const blocked: BlockedNodeDetail[] = [] const blocked: BlockedNodeDetail[] = []
const selectedSet = new Set(selectedNodeIds) const selectedSet = new Set(selectedNodeIds)
......
...@@ -8,9 +8,7 @@ ...@@ -8,9 +8,7 @@
@confirm="handleConfirm" @confirm="handleConfirm"
> >
<div class="flex flex-col gap-4 py-1 text-xs pr-1"> <div class="flex flex-col gap-4 py-1 text-xs pr-1">
<span class="text-color2 text-sm"> <span class="text-color2 text-sm">系统检测到您选中的节点中,部分节点由于 DTD 规则要求无法删除。您可以选择勾选并删除其余合法节点:</span>
系统检测到您选中的节点中,部分节点由于 DTD 规则要求无法删除。您可以选择勾选并删除其余合法节点:
</span>
<!-- 1. 无法删除的节点列表 (Blocked Nodes) --> <!-- 1. 无法删除的节点列表 (Blocked Nodes) -->
<div v-if="blockedNodes.length > 0" class="flex flex-col gap-2"> <div v-if="blockedNodes.length > 0" class="flex flex-col gap-2">
...@@ -33,13 +31,7 @@ ...@@ -33,13 +31,7 @@
<n-icon><checkmark-circle-outline /></n-icon> <n-icon><checkmark-circle-outline /></n-icon>
可安全删除的节点 ({{ safeNodes.length }} 个): 可安全删除的节点 ({{ safeNodes.length }} 个):
</span> </span>
<n-checkbox <n-checkbox :checked="isAllChecked" :indeterminate="isIndeterminate" @update:checked="toggleCheckAll">全选</n-checkbox>
:checked="isAllChecked"
:indeterminate="isIndeterminate"
@update:checked="toggleCheckAll"
>
全选
</n-checkbox>
</div> </div>
<CommonNodeDetailList <CommonNodeDetailList
:items="formattedSafeNodes" :items="formattedSafeNodes"
...@@ -51,13 +43,18 @@ ...@@ -51,13 +43,18 @@
</div> </div>
<!-- 3. 空提示(例如没有可选删除节点) --> <!-- 3. 空提示(例如没有可选删除节点) -->
<div v-if="safeNodes.length === 0" class="p-3 style-error-alert rounded-lg text-danger flex items-center justify-center font-bold text-sm"> <div
v-if="safeNodes.length === 0"
class="p-3 style-error-alert rounded-lg text-danger flex items-center justify-center font-bold text-sm"
>
当前所选节点由于规则约束均不可删除!请取消并重新选择。 当前所选节点由于规则约束均不可删除!请取消并重新选择。
</div> </div>
<!-- 4. 底部小结 --> <!-- 4. 底部小结 -->
<div v-if="safeNodes.length > 0" class="text-color3 text-right mt-1 border-t border-divider pt-2"> <div v-if="safeNodes.length > 0" class="text-color3 text-right mt-1 border-t border-divider pt-2">
已选中 <strong class="text-success text-sm">{{ checkedCount }}</strong> / {{ safeNodes.length }} 个可删除节点进行删除 已选中
<strong class="text-success text-sm">{{ checkedCount }}</strong>
/ {{ safeNodes.length }} 个可删除节点进行删除
</div> </div>
</div> </div>
</CommonModal> </CommonModal>
...@@ -74,17 +71,8 @@ const emit = defineEmits<{ ...@@ -74,17 +71,8 @@ const emit = defineEmits<{
const editorStore = useEditorStore() const editorStore = useEditorStore()
const { const { show, blockedNodes, safeNodes, isAllChecked, isIndeterminate, checkedCount, open, toggleCheckAll, handleConfirm } =
show, useBatchDeleteConfirmModal(emit)
blockedNodes,
safeNodes,
isAllChecked,
isIndeterminate,
checkedCount,
open,
toggleCheckAll,
handleConfirm
} = useBatchDeleteConfirmModal(emit)
const handleNodeClick = (item: any) => { const handleNodeClick = (item: any) => {
editorStore.setSelectedNodeId(item.id) editorStore.setSelectedNodeId(item.id)
......
...@@ -32,4 +32,3 @@ const open = (nodeName: string, rawModel: string, humanReadable: string, parsed: ...@@ -32,4 +32,3 @@ const open = (nodeName: string, rawModel: string, humanReadable: string, parsed:
defineExpose({ open }) defineExpose({ open })
</script> </script>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<!-- XML 渲染面板,带优雅网格背景与代码字体 --> <!-- XML 渲染面板,带优雅网格背景与代码字体 -->
<div <div
class="rounded-xl overflow-hidden border border-divider shadow-lg relative bg-fill-4" class="rounded-xl overflow-hidden border border-divider shadow-lg relative bg-fill-4"
style="background-image: radial-gradient(circle, rgba(0,0,0,0.02) 1px, transparent 1px); background-size: 16px 16px;" style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.02) 1px, transparent 1px); background-size: 16px 16px"
> >
<!-- 磨砂玻璃质感的顶部装饰条 --> <!-- 磨砂玻璃质感的顶部装饰条 -->
<div class="h-8 bg-fill-3 border-b border-divider flex items-center px-4 space-x-1.5 select-none shrink-0"> <div class="h-8 bg-fill-3 border-b border-divider flex items-center px-4 space-x-1.5 select-none shrink-0">
...@@ -54,7 +54,7 @@ defineExpose({ open }) ...@@ -54,7 +54,7 @@ defineExpose({ open })
<style scoped> <style scoped>
.xml-content-pre { .xml-content-pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
} }
......
...@@ -248,15 +248,17 @@ export function useNodeTree( ...@@ -248,15 +248,17 @@ export function useNodeTree(
} else if (node.tagName === 'CBLST') { } else if (node.tagName === 'CBLST') {
const action = node.attributes.ACTION === 'verif-close' ? '确认关闭' : node.attributes.ACTION === 'open' ? '断开' : '操作' const action = node.attributes.ACTION === 'verif-close' ? '确认关闭' : node.attributes.ACTION === 'open' ? '断开' : '操作'
subtitle = `行动: ${action}` subtitle = `行动: ${action}`
} else if (node.tagName === 'CEP') { } else if (node.tagName === 'CEP' || node.tagName === 'SUBTASK') {
const { CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR } = node.attributes || {} const { CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR } = node.attributes || {}
const parts = [CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR].filter(Boolean) const parts = [CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR].filter(Boolean)
subtitle = parts.join('-') subtitle = parts.join('-')
} else if (node.tagName === 'UNIT-RECORD') { } else if (node.tagName === 'UNIT-RECORD') {
const text = node.textContent ? node.textContent.trim() : '' const text = node.textContent ? node.textContent.trim() : ''
subtitle = `${text} (单位: ${node.attributes.UNIT || 'mm'})` subtitle = `${text} (单位: ${node.attributes.UNIT || 'mm'})`
} else if (node.tagName === 'SIGNOFF') { } else if (node.tagName === 'SIGNOFF') {
subtitle = `Tag: ${node.attributes.TAG || '签字'}` const tag = node.attributes.TAG || '签字'
const level = node.attributes['CK-LEVEL']
subtitle = level ? `Tag: ${tag} (${level}级)` : `Tag: ${tag}`
} 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) {
......
...@@ -83,10 +83,7 @@ ...@@ -83,10 +83,7 @@
> >
<!-- 局部翻译加载状态 --> <!-- 局部翻译加载状态 -->
<Transition name="translate-loading"> <Transition name="translate-loading">
<div <div v-if="translatingNodeId === item.id" class="translate-loading-mask">
v-if="translatingNodeId === item.id"
class="translate-loading-mask"
>
<div class="translate-loading-inner"> <div class="translate-loading-inner">
<n-icon size="13" class="translate-loading-icon"> <n-icon size="13" class="translate-loading-icon">
<SyncOutline /> <SyncOutline />
...@@ -94,7 +91,9 @@ ...@@ -94,7 +91,9 @@
<span class="translate-loading-text"> <span class="translate-loading-text">
正在智能翻译 正在智能翻译
<span class="translate-dots"> <span class="translate-dots">
<span>.</span><span>.</span><span>.</span> <span>.</span>
<span>.</span>
<span>.</span>
</span> </span>
</span> </span>
</div> </div>
...@@ -180,13 +179,13 @@ ...@@ -180,13 +179,13 @@
/> />
<!-- 查看规则弹窗 --> <!-- 查看规则弹窗 -->
<CheckRuleModal :ref="(el) => checkRuleModalRef = el" /> <CheckRuleModal :ref="(el) => (checkRuleModalRef = el)" />
<!-- 查看XML片段弹窗 --> <!-- 查看XML片段弹窗 -->
<ViewXmlModal :ref="(el) => viewXmlModalRef = el" /> <ViewXmlModal :ref="(el) => (viewXmlModalRef = el)" />
<!-- 添加/插入节点弹窗 --> <!-- 添加/插入节点弹窗 -->
<AddNodeModal :ref="(el) => addNodeModalRef = el" /> <AddNodeModal :ref="(el) => (addNodeModalRef = el)" />
<!-- 插入 XML 片段弹窗 --> <!-- 插入 XML 片段弹窗 -->
<InsertFragmentModal ref="insertFragmentModalRef" /> <InsertFragmentModal ref="insertFragmentModalRef" />
...@@ -199,7 +198,16 @@ ...@@ -199,7 +198,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { SearchOutline, ListOutline, TrashOutline, CloseOutline, SyncOutline } from '@vicons/ionicons5' import { SearchOutline, ListOutline, TrashOutline, CloseOutline, SyncOutline } from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { useNodeTree, checkRuleVisible, viewXmlVisible, addNodeVisible, translatingNodeId, checkRuleModalRef, viewXmlModalRef, addNodeModalRef } from './functionals' import {
useNodeTree,
checkRuleVisible,
viewXmlVisible,
addNodeVisible,
translatingNodeId,
checkRuleModalRef,
viewXmlModalRef,
addNodeModalRef
} from './functionals'
import CheckRuleModal from './components/CheckRuleModal/index.vue' import CheckRuleModal from './components/CheckRuleModal/index.vue'
import AddNodeModal from './components/AddNodeModal/index.vue' import AddNodeModal from './components/AddNodeModal/index.vue'
import ViewXmlModal from './components/ViewXmlModal/index.vue' import ViewXmlModal from './components/ViewXmlModal/index.vue'
...@@ -396,14 +404,22 @@ const batchDeleteConfirmModalRef = ref<any>(null) ...@@ -396,14 +404,22 @@ const batchDeleteConfirmModalRef = ref<any>(null)
animation: translate-bounce 1.2s ease-in-out infinite; animation: translate-bounce 1.2s ease-in-out infinite;
font-weight: 900; font-weight: 900;
} }
.translate-dots span:nth-child(1) { animation-delay: 0s; } .translate-dots span:nth-child(1) {
.translate-dots span:nth-child(2) { animation-delay: 0.2s; } animation-delay: 0s;
.translate-dots span:nth-child(3) { animation-delay: 0.4s; } }
.translate-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.translate-dots span:nth-child(3) {
animation-delay: 0.4s;
}
/* 入场 / 离场过渡 */ /* 入场 / 离场过渡 */
.translate-loading-enter-active, .translate-loading-enter-active,
.translate-loading-leave-active { .translate-loading-leave-active {
transition: opacity 0.18s ease, transform 0.18s ease; transition:
opacity 0.18s ease,
transform 0.18s ease;
} }
.translate-loading-enter-from, .translate-loading-enter-from,
.translate-loading-leave-to { .translate-loading-leave-to {
...@@ -413,12 +429,22 @@ const batchDeleteConfirmModalRef = ref<any>(null) ...@@ -413,12 +429,22 @@ const batchDeleteConfirmModalRef = ref<any>(null)
} }
@keyframes translate-spin { @keyframes translate-spin {
from { transform: rotate(0deg); } from {
to { transform: rotate(360deg); } transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }
@keyframes translate-bounce { @keyframes translate-bounce {
0%, 80%, 100% { transform: translateY(0); } 0%,
40% { transform: translateY(-3px); } 80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-3px);
}
} }
</style> </style>
export interface SplitterProps { export interface SplitterProps {
width: number width: number
collapsed: boolean collapsed: boolean
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<!-- 折叠状态:展开箭头 --> <!-- 折叠状态:展开箭头 -->
<div v-if="collapsed" class="split-expand-btn"> <div v-if="collapsed" class="split-expand-btn">
<svg class="w-3 h-3" viewBox="0 0 12 12" fill="currentColor"> <svg class="w-3 h-3" viewBox="0 0 12 12" fill="currentColor">
<path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/> <path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" />
</svg> </svg>
</div> </div>
<!-- 展开状态:可视指示线 + 手柄圆点 --> <!-- 展开状态:可视指示线 + 手柄圆点 -->
...@@ -27,29 +27,27 @@ ...@@ -27,29 +27,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { useSplitter } from './functionals' import { useSplitter } from './functionals'
const props = withDefaults(defineProps<{ const props = withDefaults(
defineProps<{
width: number width: number
collapsed: boolean collapsed: boolean
collapseThreshold?: number collapseThreshold?: number
defaultWidth?: number defaultWidth?: number
maxRatio?: number maxRatio?: number
}>(), { }>(),
{
collapseThreshold: 150, collapseThreshold: 150,
defaultWidth: 560, defaultWidth: 560,
maxRatio: 0.6 maxRatio: 0.6
}) }
)
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:width', val: number): void (e: 'update:width', val: number): void
(e: 'update:collapsed', val: boolean): void (e: 'update:collapsed', val: boolean): void
}>() }>()
const { const { dividerRef, isDragging, startDrag, handleDividerClick } = useSplitter(props, emit)
dividerRef,
isDragging,
startDrag,
handleDividerClick
} = useSplitter(props, emit)
</script> </script>
<style scoped> <style scoped>
...@@ -80,7 +78,9 @@ const { ...@@ -80,7 +78,9 @@ const {
width: 1px; width: 1px;
transform: translateX(-50%); transform: translateX(-50%);
background-color: var(--divider-color, rgba(0, 0, 0, 0.08)); background-color: var(--divider-color, rgba(0, 0, 0, 0.08));
transition: background-color 0.2s, width 0.2s; transition:
background-color 0.2s,
width 0.2s;
} }
.split-divider:hover .split-divider-line, .split-divider:hover .split-divider-line,
...@@ -102,7 +102,10 @@ const { ...@@ -102,7 +102,10 @@ const {
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.08)); border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.08));
opacity: 0; opacity: 0;
transform: scaleY(0.8); transform: scaleY(0.8);
transition: opacity 0.2s, transform 0.2s, background 0.2s; transition:
opacity 0.2s,
transform 0.2s,
background 0.2s;
} }
.split-divider:hover .split-divider-handle, .split-divider:hover .split-divider-handle,
......
...@@ -17,11 +17,7 @@ ...@@ -17,11 +17,7 @@
<template v-if="!isDelete"> <template v-if="!isDelete">
<n-form-item label="单元格初始化段落节点" path="cellChildTags"> <n-form-item label="单元格初始化段落节点" path="cellChildTags">
<div class="w-full bg-fill-2 p-3 rounded border border-divider"> <div class="w-full bg-fill-2 p-3 rounded border border-divider">
<CommonCheckbox <CommonCheckbox v-model:value="form.cellChildTags" :options="cellChildOptions" :space-size="24" />
v-model:value="form.cellChildTags"
:options="cellChildOptions"
:space-size="24"
/>
</div> </div>
</n-form-item> </n-form-item>
</template> </template>
......
...@@ -64,5 +64,5 @@ export const ACTION_META: Record<string, BatchActionMeta> = { ...@@ -64,5 +64,5 @@ export const ACTION_META: Record<string, BatchActionMeta> = {
'col-right': { title: '在右侧插入列', description: '在当前列右侧插入多少列?', isDelete: false, count: 1 }, 'col-right': { title: '在右侧插入列', description: '在当前列右侧插入多少列?', isDelete: false, count: 1 },
'col-append': { title: '在末尾追加列', description: '在表格末尾追加多少列?', isDelete: false, count: 1 }, 'col-append': { title: '在末尾追加列', description: '在表格末尾追加多少列?', isDelete: false, count: 1 },
'row-delete': { title: '删除行', description: '从当前行开始向下删除多少行?', isDelete: true, count: 1 }, 'row-delete': { title: '删除行', description: '从当前行开始向下删除多少行?', isDelete: true, count: 1 },
'col-delete': { title: '删除列', description: '从当前列开始向右删除多少列?', isDelete: true, count: 1 }, 'col-delete': { title: '删除列', description: '从当前列开始向右删除多少列?', isDelete: true, count: 1 }
} }
import { ref, computed } from 'vue'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
...@@ -356,7 +357,13 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -356,7 +357,13 @@ export function useTableEditor(props: { node: XmlNode }) {
store.rebuildNodeMap() store.rebuildNodeMap()
} }
const addRow = (node: XmlNode, section: 'THEAD' | 'TBODY' = 'TBODY', activeRowId?: string, insertBelow = true, cellChildTags?: string[] | null): void => { const addRow = (
node: XmlNode,
section: 'THEAD' | 'TBODY' = 'TBODY',
activeRowId?: string,
insertBelow = true,
cellChildTags?: string[] | null
): void => {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) return if (!tgroup) return
...@@ -527,7 +534,6 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -527,7 +534,6 @@ export function useTableEditor(props: { node: XmlNode }) {
store.rebuildNodeMap() store.rebuildNodeMap()
} }
const addColumn = (node: XmlNode, activeColIdx?: number, insertRight = true, cellChildTags?: string[] | null): void => { const addColumn = (node: XmlNode, activeColIdx?: number, insertRight = true, cellChildTags?: string[] | null): void => {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) return if (!tgroup) return
...@@ -785,7 +791,9 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -785,7 +791,9 @@ export function useTableEditor(props: { node: XmlNode }) {
const deleteCellFromSection = (sec?: XmlNode) => { const deleteCellFromSection = (sec?: XmlNode) => {
if (!sec) return if (!sec) return
sec.children.filter((c) => c.tagName === 'ROW').forEach((row) => { sec.children
.filter((c) => c.tagName === 'ROW')
.forEach((row) => {
const entries = row.children.filter((c) => c.tagName === 'ENTRY') const entries = row.children.filter((c) => c.tagName === 'ENTRY')
if (entries[colIdx]) { if (entries[colIdx]) {
const entryIdx = row.children.findIndex((c) => c.id === entries[colIdx].id) const entryIdx = row.children.findIndex((c) => c.id === entries[colIdx].id)
...@@ -1177,7 +1185,6 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1177,7 +1185,6 @@ export function useTableEditor(props: { node: XmlNode }) {
mergeMultipleCells(props.node, allSelectedIds, minCol, maxCol, minRow, maxRow, isThead) mergeMultipleCells(props.node, allSelectedIds, minCol, maxCol, minRow, maxRow, isThead)
} }
const handleSplitSelected = () => { const handleSplitSelected = () => {
if (selectedCellIds.value.length !== 1) return if (selectedCellIds.value.length !== 1) return
const cellId = selectedCellIds.value[0] const cellId = selectedCellIds.value[0]
...@@ -1540,6 +1547,158 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1540,6 +1547,158 @@ export function useTableEditor(props: { node: XmlNode }) {
contextMenu.value.show = false contextMenu.value.show = false
} }
// === CALS 表格样式控制逻辑 ===
/** 计算各列的宽度样式 */
const colWidthStyles = computed(() => {
const specs = structure.value.colSpecs || []
const colsCount = structure.value.cols
const parsedSpecs = Array.from({ length: colsCount }, (_, idx) => {
const spec = specs[idx]
const colwidth = spec?.attributes?.COLWIDTH || spec?.attributes?.colwidth || ''
if (!colwidth) {
return { type: 'auto', value: 1 }
}
const cleanWidth = colwidth.trim()
if (cleanWidth.includes('*')) {
const starVal = cleanWidth.replace('*', '').trim()
const val = starVal ? parseFloat(starVal) : 1
return { type: 'prop', value: isNaN(val) ? 1 : val }
}
const numVal = parseFloat(cleanWidth)
if (!isNaN(numVal)) {
const unit = cleanWidth.replace(/[0-9.]/g, '').trim() || 'px'
return { type: 'abs', value: numVal, unit }
}
return { type: 'auto', value: 1 }
})
let propSum = 0
parsedSpecs.forEach((spec) => {
if (spec.type === 'prop' || spec.type === 'auto') {
propSum += spec.value
}
})
if (propSum <= 0) propSum = 1
const hasAbsolute = parsedSpecs.some((s) => s.type === 'abs')
if (!hasAbsolute) {
return parsedSpecs.map((spec) => {
const pct = (spec.value / propSum) * 100
return `${pct.toFixed(2)}%`
})
} else {
const absSpecs = parsedSpecs.filter((s) => s.type === 'abs')
const absSumStr = absSpecs.map((s) => `${s.value}${s.unit}`).join(' + ')
return parsedSpecs.map((spec) => {
if (spec.type === 'abs') {
return `${spec.value}${spec.unit}`
}
const ratio = spec.value / propSum
if (absSumStr) {
return `calc((100% - (${absSumStr})) * ${ratio.toFixed(4)})`
} else {
return `${(ratio * 100).toFixed(2)}%`
}
})
}
})
/** 计算表格的外边框 Frame 样式类 */
const tableFrameClass = computed(() => {
const frame = (props.node.attributes?.FRAME || props.node.attributes?.frame || 'ALL').toUpperCase()
return `frame-${frame.toLowerCase()}`
})
/** 解析并计算单元格的对齐(水平 ALIGN、垂直 VALIGN)样式 */
const getCellStyle = (cell: any) => {
const tgroup = props.node.children?.find((c) => c.tagName === 'TGROUP')
// 1. 解析 ALIGN 水平对齐方式
let align = cell.attributes?.ALIGN || cell.attributes?.align
if (!align) {
const spanname = cell.attributes?.SPANNAME || cell.attributes?.spanname
if (spanname && tgroup) {
const spanspec = tgroup.children?.find(
(c) =>
c.tagName === 'SPANSPEC' && (c.attributes?.SPANNAME || c.attributes?.spanname || '').toLowerCase() === spanname.toLowerCase()
)
if (spanspec) {
align = spanspec.attributes?.ALIGN || spanspec.attributes?.align
}
}
}
if (!align) {
const colname = cell.attributes?.COLNAME || cell.attributes?.colname
let colSpec = null
if (colname && tgroup) {
colSpec = tgroup.children?.find(
(c) => c.tagName === 'COLSPEC' && (c.attributes?.COLNAME || c.attributes?.colname || '').toLowerCase() === colname.toLowerCase()
)
} else if (cell.colIdx !== undefined && structure.value.colSpecs) {
colSpec = structure.value.colSpecs[cell.colIdx]
}
if (colSpec) {
align = colSpec.attributes?.ALIGN || colSpec.attributes?.align
}
}
if (!align && tgroup) {
align = tgroup.attributes?.ALIGN || tgroup.attributes?.align
}
// 2. 解析 VALIGN 垂直对齐方式
let valign = cell.attributes?.VALIGN || cell.attributes?.valign
if (!valign) {
const entryNodeId = cell.rawNode?.id || cell.id
const rowEntry = store.nodeMap.get(entryNodeId)
const rowNode = rowEntry?.parent
if (rowNode) {
valign = rowNode.attributes?.VALIGN || rowNode.attributes?.valign
if (!valign) {
const sectionEntry = store.nodeMap.get(rowNode.id)
const sectionNode = sectionEntry?.parent
if (sectionNode) {
valign = sectionNode.attributes?.VALIGN || sectionNode.attributes?.valign
}
}
}
}
const styles: Record<string, string> = {}
if (align) {
const a = align.toLowerCase()
if (a === 'left' || a === 'right' || a === 'center' || a === 'justify') {
styles['text-align'] = a
styles['--cell-align'] = a === 'center' ? 'center' : a === 'right' ? 'flex-end' : 'flex-start'
} else if (a === 'char') {
styles['text-align'] = 'left'
styles['--cell-align'] = 'flex-start'
}
}
if (valign) {
const v = valign.toLowerCase()
if (v === 'top' || v === 'middle' || v === 'bottom') {
styles['vertical-align'] = v
}
}
return styles
}
return { return {
structure, structure,
selectedCellIds, selectedCellIds,
...@@ -1579,7 +1738,11 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1579,7 +1738,11 @@ export function useTableEditor(props: { node: XmlNode }) {
mergeMultipleCells, mergeMultipleCells,
splitCell, splitCell,
insertedRowIds, insertedRowIds,
insertedCellIds insertedCellIds,
// 样式计算
colWidthStyles,
tableFrameClass,
getCellStyle
} }
} }
......
...@@ -12,11 +12,11 @@ ...@@ -12,11 +12,11 @@
<table <table
class="w-full border-collapse text-sm table-fixed min-w-[600px] transition-all" class="w-full border-collapse text-sm table-fixed min-w-[600px] transition-all"
:data-node-id="structure.tgroupId" :data-node-id="structure.tgroupId"
:class="[isNodeSelected(structure.tgroupId) ? 'ring-2 ring-primary ring-offset-2 rounded' : '']" :class="[isNodeSelected(structure.tgroupId) ? 'ring-2 ring-primary ring-offset-2 rounded' : '', tableFrameClass]"
> >
<colgroup> <colgroup>
<col class="w-12" /> <col class="w-12" />
<col v-for="(_, idx) in structure.cols" :key="idx" /> <col v-for="(_, idx) in structure.cols" :key="idx" :style="{ width: colWidthStyles[idx] }" />
<col class="w-12" /> <col class="w-12" />
</colgroup> </colgroup>
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
isNodeSelected(structure.theadId) ? 'bg-primary/10 border-primary/40' : '', isNodeSelected(structure.theadId) ? 'bg-primary/10 border-primary/40' : '',
insertedCellIds.has(cell.id) ? 'inserted-cell-highlight' : '' insertedCellIds.has(cell.id) ? 'inserted-cell-highlight' : ''
]" ]"
:style="getCellStyle(cell)"
@click.stop="handleCellClick(cell, $event)" @click.stop="handleCellClick(cell, $event)"
@contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)" @contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)"
> >
...@@ -130,6 +131,7 @@ ...@@ -130,6 +131,7 @@
isNodeSelected(structure.tbodyId) ? 'bg-primary/5 border-primary/30' : '', isNodeSelected(structure.tbodyId) ? 'bg-primary/5 border-primary/30' : '',
insertedCellIds.has(cell.id) ? 'inserted-cell-highlight' : '' insertedCellIds.has(cell.id) ? 'inserted-cell-highlight' : ''
]" ]"
:style="getCellStyle(cell)"
@click.stop="handleCellClick(cell, $event)" @click.stop="handleCellClick(cell, $event)"
@contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)" @contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)"
> >
...@@ -237,7 +239,10 @@ const { ...@@ -237,7 +239,10 @@ const {
batchDeleteRows, batchDeleteRows,
batchDeleteColumns, batchDeleteColumns,
insertedRowIds, insertedRowIds,
insertedCellIds insertedCellIds,
colWidthStyles,
tableFrameClass,
getCellStyle
} = useTableEditor(props) } = useTableEditor(props)
/** TableBatchModal 组件实例引用 */ /** TableBatchModal 组件实例引用 */
...@@ -282,4 +287,39 @@ const { onContextMenuSelect, executeBatchAction } = useTableBatchActions({ ...@@ -282,4 +287,39 @@ const { onContextMenuSelect, executeBatchAction } = useTableBatchActions({
outline: 1px solid transparent; outline: 1px solid transparent;
} }
} }
/* === CALS 表格边框 Frame 样式控制 === */
table.frame-none {
border: none !important;
}
table.frame-none tr > *:first-child,
table.frame-top tr > *:first-child,
table.frame-bottom tr > *:first-child,
table.frame-topbot tr > *:first-child {
border-left: none !important;
}
table.frame-none tr > *:last-child,
table.frame-top tr > *:last-child,
table.frame-bottom tr > *:last-child,
table.frame-topbot tr > *:last-child {
border-right: none !important;
}
/* 当存在 thead 时,去除 thead 首行的上边框 */
table.frame-none thead tr:first-child > *,
table.frame-bottom thead tr:first-child > *,
table.frame-sides thead tr:first-child > * {
border-top: none !important;
}
/* 当不存在 thead 时,去除 tbody 首行的上边框 */
table.frame-none colgroup + tbody tr:first-child > *,
table.frame-bottom colgroup + tbody tr:first-child > *,
table.frame-sides colgroup + tbody tr:first-child > * {
border-top: none !important;
}
/* 去除 tbody 最后一行数据行的下边框(倒数第二行,因为倒数第一行是列删除按钮行) */
table.frame-none tbody tr:nth-last-child(2) > *,
table.frame-top tbody tr:nth-last-child(2) > *,
table.frame-sides tbody tr:nth-last-child(2) > * {
border-bottom: none !important;
}
</style> </style>
...@@ -69,7 +69,7 @@ export function useTextBlockEditor(getNode: () => XmlNode) { ...@@ -69,7 +69,7 @@ export function useTextBlockEditor(getNode: () => XmlNode) {
attributes: {} attributes: {}
}) })
} else if (item.type === 'element' && item.nodeId) { } else if (item.type === 'element' && item.nodeId) {
const child = nodeVal.children.find(c => c.id === item.nodeId) const child = nodeVal.children.find((c) => c.id === item.nodeId)
if (child) { if (child) {
list.push({ list.push({
type: 'element', type: 'element',
......
...@@ -83,13 +83,7 @@ ...@@ -83,13 +83,7 @@
class="flex-1" class="flex-1"
@update:value="syncMixedContent" @update:value="syncMixedContent"
/> />
<n-input <n-input v-else v-model:value="item.attributes[attrName]" size="tiny" class="flex-1" @input="syncMixedContent" />
v-else
v-model:value="item.attributes[attrName]"
size="tiny"
class="flex-1"
@input="syncMixedContent"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -25,8 +25,7 @@ interface XmlNode { ...@@ -25,8 +25,7 @@ interface XmlNode {
// ── 工具函数 ────────────────────────────────────────────────────────────────── // ── 工具函数 ──────────────────────────────────────────────────────────────────
function generateId(): string { function generateId(): string {
return (self as any).crypto?.randomUUID?.() return (self as any).crypto?.randomUUID?.() ?? `node_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
?? `node_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
} }
// ── DOM → XmlNode 递归转换 ──────────────────────────────────────────────────── // ── DOM → XmlNode 递归转换 ────────────────────────────────────────────────────
...@@ -44,8 +43,8 @@ function domElementToXmlNode(element: Element, parentId: string | null): XmlNode ...@@ -44,8 +43,8 @@ function domElementToXmlNode(element: Element, parentId: string | null): XmlNode
let textContent = '' let textContent = ''
const childNodes = Array.from(element.childNodes) const childNodes = Array.from(element.childNodes)
const hasElementChildren = childNodes.some(n => n.nodeType === 1 /* ELEMENT_NODE */) const hasElementChildren = childNodes.some((n) => n.nodeType === 1 /* ELEMENT_NODE */)
const hasTextChildren = childNodes.some(n => n.nodeType === 3 /* TEXT_NODE */ && n.textContent?.trim()) const hasTextChildren = childNodes.some((n) => n.nodeType === 3 /* TEXT_NODE */ && n.textContent?.trim())
if (hasElementChildren && hasTextChildren) { if (hasElementChildren && hasTextChildren) {
for (const child of childNodes) { for (const child of childNodes) {
......
{
"elements": {
"SIGNOFF": {
"attributes": {
"CK-LEVEL": {
"typeDefinition": "(A | B | C)",
"enumValues": ["A", "B", "C"]
}
}
}
}
}
<template>
<div class="my-3 flex overflow-x-auto select-none" :class="containerClass">
<table class="border-collapse border border-black text-xs bg-white text-black">
<tbody>
<!-- 第一行:表头与签字人 -->
<tr class="h-10">
<!-- 工作者 Per.By -->
<td class="px-2 border border-black font-bold text-center w-24">
工作者
<br />
Per.By
</td>
<td class="px-2 border border-black text-center w-32 font-mono">
<span v-if="node.attributes.ACTION === 'NA'" style="color: red" class="font-bold">N/A</span>
<span v-else>{{ node.attributes.mech ? `${node.attributes.mech} ${node.attributes.mechName || ''}` : '' }}</span>
</td>
<!-- 检查者 Insp.By (CK-LEVEL B, 且支持 D/E 兼容) -->
<template v-if="['B', 'D', 'E'].includes(node.attributes['CK-LEVEL'])">
<td class="px-2 border border-black font-bold text-center w-24">
检查者
<br />
Insp.By
</td>
<td class="px-2 border border-black text-center w-32 font-mono">
<span v-if="node.attributes.ACTION === 'NA'" style="color: red" class="font-bold">N/A</span>
<span v-else>{{ node.attributes.insp ? `${node.attributes.insp} ${node.attributes.inspName || ''}` : '' }}</span>
</td>
</template>
<!-- 必检 RII.By (CK-LEVEL C) -->
<template v-else-if="node.attributes['CK-LEVEL'] === 'C'">
<td class="px-2 border border-black font-bold text-center w-24">
必检
<br />
RII.By
</td>
<td class="px-2 border border-black text-center w-32 font-mono">
<span v-if="node.attributes.ACTION === 'NA'" style="color: red" class="font-bold">N/A</span>
<span v-else>
{{
node.attributes.verf
? `${node.attributes.verf} ${node.attributes.verfName || ''}`
: node.attributes.insp
? `${node.attributes.insp} ${node.attributes.inspName || ''}`
: ''
}}
</span>
</td>
</template>
</tr>
<!-- 第二行:签字时间 (如果 disStime 有值时渲染) -->
<tr v-if="node.attributes.disStime" class="h-5 text-[10px] text-center font-mono">
<!-- A 级签字时间 -->
<td v-if="!['B', 'D', 'E', 'C'].includes(node.attributes['CK-LEVEL'])" colspan="2" class="border border-black px-1">
{{ node.attributes.disStime }}
</td>
<!-- B/C 级签字时间 -->
<template v-else>
<td colspan="2" class="border border-black px-1">
{{ node.attributes.disStime }}
</td>
<td colspan="2" class="border border-black px-1">
{{ node.attributes.SUP_DIS_STIME || node.attributes.disStime }}
</td>
</template>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import type { XmlNode } from '@/types/xmlNode'
const props = defineProps<{
node: XmlNode
}>()
// 签字表靠左或靠右显示样式,由 LOCATION 属性控制;
// 在 .env.xm 环境中,默认是要靠右 (RIGHT) 显示
const containerClass = computed(() => {
const location = props.node.attributes.LOCATION || 'RIGHT'
return location === 'LEFT' ? 'justify-start' : 'justify-end'
})
</script>
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