Commit 71679edb by pangchong

feat(editor-compare): 增强 XML 对比编辑与显示功能

- 对 XML 大小实时计算并显示在编辑工具栏
- 在比较模态框中新增 XML 对比与渲染对比区域的展开/收起开关,保证至少保留一个区域
- 实现 XML 文本对比区域多行范围选中编辑,支持保存与取消操作
- 优化差异行的显示逻辑,支持编辑状态下只显示相关行
- 美化差异条颜色并统一颜色变量,提高视觉一致性
- 隐藏浏览器默认滚动条,使用自定义模拟滑块提升用户体验
- 增加对比弹框头部隐藏样式,简化界面
- 动态调整对比文本和渲染区尺寸与透明度,添加平滑过渡动画
- 在表格编辑器中根据对比模式调整行列选中和交互样式,禁用对比模式下的选中效果
- 优化表格编辑器行、列和单元格相关的鼠标交互逻辑,避免对比模式触发不必要操作
parent 862f9ad6
...@@ -21,6 +21,11 @@ export function useCompareModal() { ...@@ -21,6 +21,11 @@ export function useCompareModal() {
const oldDiffLines = ref<DiffLine[]>([]) const oldDiffLines = ref<DiffLine[]>([])
const newDiffLines = ref<DiffLine[]>([]) const newDiffLines = ref<DiffLine[]>([])
const editingOldRange = ref<{ start: number; end: number } | null>(null)
const editingNewRange = ref<{ start: number; end: number } | null>(null)
const editingOldRangeText = ref('')
const editingNewRangeText = ref('')
const updateTextDiff = () => { const updateTextDiff = () => {
const { leftLines, rightLines } = generateDoubleFolderDiff(oldXmlText.value, newXmlText.value) const { leftLines, rightLines } = generateDoubleFolderDiff(oldXmlText.value, newXmlText.value)
oldDiffLines.value = leftLines oldDiffLines.value = leftLines
...@@ -91,6 +96,8 @@ export function useCompareModal() { ...@@ -91,6 +96,8 @@ export function useCompareModal() {
const resetDiff = () => { const resetDiff = () => {
oldDiffTree.value = null oldDiffTree.value = null
newDiffTree.value = null newDiffTree.value = null
editingOldRange.value = null
editingNewRange.value = null
} }
const open = (currentXml?: string) => { const open = (currentXml?: string) => {
...@@ -104,6 +111,8 @@ export function useCompareModal() { ...@@ -104,6 +111,8 @@ export function useCompareModal() {
isEditingNew.value = true isEditingNew.value = true
oldDiffLines.value = [] oldDiffLines.value = []
newDiffLines.value = [] newDiffLines.value = []
editingOldRange.value = null
editingNewRange.value = null
// 自动拉取当前编辑器内容填充为旧版本 // 自动拉取当前编辑器内容填充为旧版本
if (editorStore.xmlTree) { if (editorStore.xmlTree) {
...@@ -269,6 +278,124 @@ export function useCompareModal() { ...@@ -269,6 +278,124 @@ export function useCompareModal() {
updateTextDiff() updateTextDiff()
} }
const getSelectedLineRange = (isNewSide: boolean): { start: number; end: number } | null => {
const selection = window.getSelection()
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return null
const range = selection.getRangeAt(0)
const getLineNumFromNode = (node: Node | null): number | null => {
let el = node as HTMLElement | null
if (node && node.nodeType === Node.TEXT_NODE) {
el = node.parentElement
}
while (el) {
if (el.classList && el.classList.contains('diff-line-row')) {
const numAttr = el.getAttribute('data-line-num')
if (numAttr) {
const num = parseInt(numAttr, 10)
if (!isNaN(num)) return num
}
}
el = el.parentElement
}
return null
}
const startNum = getLineNumFromNode(range.startContainer)
const endNum = getLineNumFromNode(range.endContainer)
if (startNum !== null && endNum !== null) {
return {
start: Math.min(startNum, endNum),
end: Math.max(startNum, endNum)
}
}
return null
}
const toggleEditOld = () => {
if (isEditingOld.value) {
isEditingOld.value = false
return
}
if (editingOldRange.value) {
saveEditRange(false)
} else {
const range = getSelectedLineRange(false)
if (range) {
editingOldRange.value = range
const lines = oldXmlText.value.split('\n')
editingOldRangeText.value = lines.slice(range.start - 1, range.end).join('\n')
nextTick(() => {
const el = document.getElementById(`old-range-input-${range.start}`)
el?.focus()
})
} else {
window.$message.warning('请先在上方 XML 文本对比区用鼠标划词选中需要编辑的区域')
}
}
}
const toggleEditNew = () => {
if (isEditingNew.value) {
isEditingNew.value = false
return
}
if (editingNewRange.value) {
saveEditRange(true)
} else {
const range = getSelectedLineRange(true)
if (range) {
editingNewRange.value = range
const lines = newXmlText.value.split('\n')
editingNewRangeText.value = lines.slice(range.start - 1, range.end).join('\n')
nextTick(() => {
const el = document.getElementById(`new-range-input-${range.start}`)
el?.focus()
})
} else {
window.$message.warning('请先在上方 XML 文本对比区用鼠标划词选中需要编辑的区域')
}
}
}
const saveEditRange = (isNewSide: boolean) => {
if (isNewSide) {
if (!editingNewRange.value) return
const lines = newXmlText.value.split('\n')
const startIdx = editingNewRange.value.start - 1
const endIdx = editingNewRange.value.end - 1
const newSubLines = editingNewRangeText.value.split('\n')
lines.splice(startIdx, endIdx - startIdx + 1, ...newSubLines)
newXmlText.value = lines.join('\n')
editingNewRange.value = null
updateTextDiff()
handleCompare()
} else {
if (!editingOldRange.value) return
const lines = oldXmlText.value.split('\n')
const startIdx = editingOldRange.value.start - 1
const endIdx = editingOldRange.value.end - 1
const newSubLines = editingOldRangeText.value.split('\n')
lines.splice(startIdx, endIdx - startIdx + 1, ...newSubLines)
oldXmlText.value = lines.join('\n')
editingOldRange.value = null
updateTextDiff()
handleCompare()
}
}
const cancelEditRange = (isNewSide: boolean) => {
if (isNewSide) {
editingNewRange.value = null
} else {
editingOldRange.value = null
}
}
// 实时防抖比对与状态切换侦听 // 实时防抖比对与状态切换侦听
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => { const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
let timer: any = null let timer: any = null
...@@ -775,7 +902,15 @@ export function useCompareModal() { ...@@ -775,7 +902,15 @@ export function useCompareModal() {
onLeftTextScroll, onLeftTextScroll,
onRightTextScroll, onRightTextScroll,
onLeftScroll, onLeftScroll,
onRightScroll onRightScroll,
editingOldRange,
editingNewRange,
editingOldRangeText,
editingNewRangeText,
toggleEditOld,
toggleEditNew,
saveEditRange,
cancelEditRange
} }
} }
......
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 type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { serializeTreeToXml } from '@/utils/xmlParser'
/** /**
* EditorToolbar 组件级业务逻辑 Hook * EditorToolbar 组件级业务逻辑 Hook
...@@ -121,6 +122,20 @@ export function useEditorToolbar(emit: any) { ...@@ -121,6 +122,20 @@ export function useEditorToolbar(emit: any) {
}) })
} }
const xmlSizeStr = computed(() => {
if (!editorStore.xmlTree) return '0 B'
try {
const xml = serializeTreeToXml(editorStore.xmlTree, 0, true)
const bytes = new TextEncoder().encode(xml).length
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
} catch (e) {
console.error('计算 XML 大小失败:', e)
return '0 B'
}
})
return { return {
editorStore, editorStore,
appStore, appStore,
...@@ -139,6 +154,7 @@ export function useEditorToolbar(emit: any) { ...@@ -139,6 +154,7 @@ export function useEditorToolbar(emit: any) {
batchTranslateModalRef, batchTranslateModalRef,
extractTranslateModalRef, extractTranslateModalRef,
searchTranslateModalRef, searchTranslateModalRef,
xmlSizeStr,
handleFileUpload: () => {} // 保留空函数以防组件 template 尚未完全更新时报错 handleFileUpload: () => {} // 保留空函数以防组件 template 尚未完全更新时报错
} }
} }
...@@ -178,6 +178,10 @@ ...@@ -178,6 +178,10 @@
<!-- 组 6:系统工具与偏好设置 (靠右) --> <!-- 组 6:系统工具与偏好设置 (靠右) -->
<div class="flex items-center gap-1.5 flex-shrink-0 ml-auto"> <div class="flex items-center gap-1.5 flex-shrink-0 ml-auto">
<div class="text-xs text-color3 select-none flex items-center gap-1 mr-1">
<span>大小:</span>
<span class="font-mono font-medium text-color2">{{ xmlSizeStr }}</span>
</div>
<div class="w-[1px] h-4 bg-divider mx-0.5 flex-shrink-0"></div> <div class="w-[1px] h-4 bg-divider mx-0.5 flex-shrink-0"></div>
<n-tooltip trigger="hover" :show-delay="600"> <n-tooltip trigger="hover" :show-delay="600">
...@@ -285,7 +289,8 @@ const { ...@@ -285,7 +289,8 @@ const {
handleCreateSignoffConfirm, handleCreateSignoffConfirm,
batchTranslateModalRef, batchTranslateModalRef,
extractTranslateModalRef, extractTranslateModalRef,
searchTranslateModalRef searchTranslateModalRef,
xmlSizeStr
} = useEditorToolbar(emit) } = useEditorToolbar(emit)
const insertFragmentModalRef = ref<any>(null) const insertFragmentModalRef = ref<any>(null)
......
...@@ -50,8 +50,9 @@ ...@@ -50,8 +50,9 @@
<tr <tr
:data-node-id="row.id" :data-node-id="row.id"
data-tag-name="ROW" data-tag-name="ROW"
class="border-b border-divider hover:bg-fill-3 group transition-colors cursor-pointer transition-all" class="border-b border-divider group transition-colors transition-all"
:class="[ :class="[
!isDiffMode ? 'hover:bg-fill-3 cursor-pointer' : 'cursor-default',
isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '', isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '',
insertedRowIds.has(row.id) ? 'inserted-row-highlight' : '' insertedRowIds.has(row.id) ? 'inserted-row-highlight' : ''
]" ]"
...@@ -59,8 +60,12 @@ ...@@ -59,8 +60,12 @@
> >
<!-- 表头行选择号 --> <!-- 表头行选择号 -->
<th <th
class="p-2 border border-divider text-center select-none cursor-pointer transition-colors bg-fill-4 text-color3 font-bold" class="p-2 border border-divider text-center select-none transition-colors bg-fill-4 text-color3 font-bold"
:class="[isNodeSelected(row.id) ? 'bg-primary text-white' : 'hover:bg-fill-3']" :class="[
!isDiffMode ? 'cursor-pointer' : 'cursor-default',
isNodeSelected(row.id) ? 'bg-primary text-white' : '',
!isDiffMode && !isNodeSelected(row.id) ? 'hover:bg-fill-3' : ''
]"
@click.stop="editorStore.setSelectedNodeId(row.id)" @click.stop="editorStore.setSelectedNodeId(row.id)"
@contextmenu.prevent="isDiffMode ? null : handleRowContextMenu(row.id, $event)" @contextmenu.prevent="isDiffMode ? null : handleRowContextMenu(row.id, $event)"
> >
...@@ -155,8 +160,9 @@ ...@@ -155,8 +160,9 @@
<tr <tr
:data-node-id="row.id" :data-node-id="row.id"
data-tag-name="ROW" data-tag-name="ROW"
class="border-b border-divider hover:bg-fill-3 group transition-colors cursor-pointer transition-all" class="border-b border-divider group transition-colors transition-all"
:class="[ :class="[
!isDiffMode ? 'hover:bg-fill-3 cursor-pointer' : 'cursor-default',
isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '', isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '',
insertedRowIds.has(row.id) ? 'inserted-row-highlight' : '' insertedRowIds.has(row.id) ? 'inserted-row-highlight' : ''
]" ]"
...@@ -164,8 +170,12 @@ ...@@ -164,8 +170,12 @@
> >
<!-- 行号选择单元格 --> <!-- 行号选择单元格 -->
<td <td
class="p-2 border border-divider text-center select-none cursor-pointer transition-colors font-bold" class="p-2 border border-divider text-center select-none transition-colors font-bold"
:class="[isNodeSelected(row.id) ? 'bg-primary text-white' : 'bg-fill-4 text-color3 hover:bg-fill-3']" :class="[
!isDiffMode ? 'cursor-pointer' : 'cursor-default',
isNodeSelected(row.id) ? 'bg-primary text-white' : 'bg-fill-4 text-color3',
!isDiffMode && !isNodeSelected(row.id) ? 'hover:bg-fill-3' : ''
]"
@click.stop="editorStore.setSelectedNodeId(row.id)" @click.stop="editorStore.setSelectedNodeId(row.id)"
@contextmenu.prevent="isDiffMode ? null : handleRowContextMenu(row.id, $event)" @contextmenu.prevent="isDiffMode ? null : handleRowContextMenu(row.id, $event)"
> >
...@@ -275,7 +285,8 @@ ...@@ -275,7 +285,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject, computed } from 'vue'
import type { Ref } from 'vue'
import { TrashOutline } from '@vicons/ionicons5' import { TrashOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { useTableEditor, useTableBatchActions } from './functionals' import { useTableEditor, useTableBatchActions } from './functionals'
...@@ -289,7 +300,9 @@ const props = defineProps<{ ...@@ -289,7 +300,9 @@ const props = defineProps<{
}>() }>()
const editorStore = useEditorStore() const editorStore = useEditorStore()
const isDiffMode = inject('diffMode', false) const injectedDiffMode = inject<boolean | Ref<boolean>>('diffMode', false)
const isDiffMode = computed(() => (typeof injectedDiffMode === 'boolean' ? injectedDiffMode : injectedDiffMode.value))
const { const {
structure, structure,
selectedCellIds, selectedCellIds,
...@@ -302,8 +315,8 @@ const { ...@@ -302,8 +315,8 @@ const {
handleColAddSelect, handleColAddSelect,
handleRowDelete, handleRowDelete,
handleColumnDelete, handleColumnDelete,
isNodeSelected, isNodeSelected: rawIsNodeSelected,
isCellSelected, isCellSelected: rawIsCellSelected,
contextMenu, contextMenu,
contextMenuOptions, contextMenuOptions,
handleCellContextMenu, handleCellContextMenu,
...@@ -321,6 +334,16 @@ const { ...@@ -321,6 +334,16 @@ const {
handleResizeStart handleResizeStart
} = useTableEditor(props) } = useTableEditor(props)
const isNodeSelected = (id: string) => {
if (isDiffMode.value) return false
return rawIsNodeSelected(id)
}
const isCellSelected = (cell: any, colIdx: number) => {
if (isDiffMode.value) return false
return rawIsCellSelected(cell, colIdx)
}
/** TableBatchModal 组件实例引用 */ /** TableBatchModal 组件实例引用 */
const batchModalRef = ref<InstanceType<typeof TableBatchModal>>() const batchModalRef = ref<InstanceType<typeof TableBatchModal>>()
......
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