Commit 846ea4b3 by pangchong

feat: 优化图片的展示交互

parent b6bb941a
......@@ -370,6 +370,7 @@ export const useEditorStore = defineStore('editor', {
undoStack: [],
redoStack: [],
lastUndoRedoTime: 0,
forceScrollSignal: 0,
editorZoom: 100,
expandedKeys: [],
skipExpandOnSelect: false
......@@ -716,8 +717,11 @@ export const useEditorStore = defineStore('editor', {
this.rebuildNodeMap()
},
setSelectedNodeId(id: string | null) {
setSelectedNodeId(id: string | null, forceScroll = false) {
this.selectedNodeId = id
if (forceScroll) {
this.forceScrollSignal = Date.now()
}
let renderedId: string | null = null
const isVirtualText = id ? id.includes('-txt-') : false
......
......@@ -13,6 +13,7 @@ export interface EditorState {
undoStack: Snapshot[] // 保存 Snapshot 历史快照
redoStack: Snapshot[] // 保存 Snapshot 重做快照
lastUndoRedoTime: number // 回退重做动作的时间戳,用于触发视口重定位
forceScrollSignal: number // 强制触发编辑区定位到选中节点的时间戳
editorZoom: number // 编辑器文字字号缩放比例 (80 - 200)
expandedKeys: string[] // 全局的节点展开状态
skipExpandOnSelect: boolean // 在选中时是否跳过祖先展开逻辑
......
......@@ -12,7 +12,7 @@
!renderInline && isContainer ? 'py-1 px-1' : ''
]"
:style="node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow' } : {}"
@click.stop="editorStore.setSelectedNodeId(node.id)"
@click.stop="handleNodeClick($event)"
>
<!-- 智能翻译加载遮罩 -->
<Transition name="fade">
......@@ -333,7 +333,8 @@
<span
class="inline-ref font-mono font-medium hover:underline cursor-pointer select-all"
style="color: blue"
@click.stop="editorStore.setSelectedNodeId(node.id)"
title="按住 Ctrl + 点击直接跳转到关联插图/节点"
@click.stop="handleNodeClick($event)"
>
<template v-if="!insideRefBlock">
{{ shouldShowPrefix(node.textContent) ? (isChineseContext ? '(参考: ' : '(Ref: ') : '(' }}
......@@ -546,6 +547,16 @@
</template>
<template v-if="node.attributes.SHEETNBR">[Sh.{{ node.attributes.SHEETNBR }}]</template>
</template>
<!-- 显式定位插图按钮 (仅编辑模式显示,HTML 预览与打印自动隐藏) -->
<span
v-if="getRefTargetId(node)"
class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-sans font-medium bg-primary/10 text-primary border border-primary/20 hover:bg-primary/20 transition-all select-none ml-1 cursor-pointer align-middle print-hide"
title="点击直接定位至目标插图"
@click.stop="editorStore.setSelectedNodeId(getRefTargetId(node)!, true)"
>
<n-icon size="11" class="mr-0.5"><NavigateOutline /></n-icon>
定位插图
</span>
</span>
</template>
......@@ -819,11 +830,64 @@
<template v-else-if="node.tagName === 'GRAPHIC'">
<div class="my-4 border border-divider rounded-lg overflow-hidden bg-fill-2 p-4 flex flex-col items-center">
<div class="w-full flex items-center justify-between pb-2 border-b border-divider mb-3 print-hide">
<span class="text-xs font-bold text-color2 flex items-center space-x-1">
<n-icon><image-outline /></n-icon>
<span class="text-xs font-bold text-color2 flex items-center space-x-2">
<n-icon><ImageOutline /></n-icon>
<span>工卡附图</span>
<span v-if="getGraphicKey(node)" class="text-[11px] font-mono text-primary font-medium bg-primary/10 px-1.5 py-0.5 rounded border border-primary/20">
KEY: {{ getGraphicKey(node) }}
</span>
</span>
<CommonTag size="small" type="primary">GRAPHIC</CommonTag>
<div class="flex items-center space-x-2">
<!-- 单个引用情况:直接点击按钮定位 -->
<template v-if="getReferencingNodes(node).length === 1">
<CommonButton
size="tiny"
type="primary"
secondary
@click.stop="editorStore.setSelectedNodeId(getReferencingNodes(node)[0].id, true)"
title="点击直接定位至引用当前插图的位置"
>
<template #icon><n-icon><NavigateOutline /></n-icon></template>
定位引用位置 (&lt;{{ getReferencingNodes(node)[0].tagName }}&gt;)
</CommonButton>
</template>
<!-- 多个引用情况:下拉Popover选择定位 -->
<template v-else-if="getReferencingNodes(node).length > 1">
<n-popover trigger="click" placement="bottom-end">
<template #trigger>
<CommonButton size="tiny" type="primary" secondary @click.stop>
<template #icon><n-icon><LinkOutline /></n-icon></template>
定位引用位置 ({{ getReferencingNodes(node).length }}处)
</CommonButton>
</template>
<div class="p-2 space-y-1 min-w-[240px] select-none" @click.stop>
<div class="text-[11px] font-bold text-color3 pb-1 border-b border-divider mb-1">
引用当前插图的节点列表:
</div>
<div
v-for="refNode in getReferencingNodes(node)"
:key="refNode.id"
class="px-2 py-1.5 text-xs hover:bg-primary/10 hover:text-primary rounded cursor-pointer flex items-center justify-between transition-colors border border-transparent hover:border-primary/20"
@click="editorStore.setSelectedNodeId(refNode.id, true)"
>
<div class="flex items-center space-x-1 truncate mr-2">
<span class="font-mono font-bold">&lt;{{ refNode.tagName }}&gt;</span>
<span v-if="getNodePreviewText(refNode)" class="text-color3 truncate text-[11px]">
{{ getNodePreviewText(refNode) }}
</span>
</div>
<n-icon size="13" class="shrink-0 text-primary"><NavigateOutline /></n-icon>
</div>
</div>
</n-popover>
</template>
<template v-else>
<span class="text-[11px] text-color3 italic">未被引用</span>
</template>
<CommonTag size="small" type="primary">GRAPHIC</CommonTag>
</div>
</div>
<!-- 遍历并渲染所有的 SHEET 子节点(每个 SHEET 节点自己渲染图片或占位卡片) -->
......@@ -1378,12 +1442,59 @@
<script setup lang="ts">
import DocNodeRenderer from './index.vue'
import { ImageOutline, GridOutline, SyncOutline } from '@vicons/ionicons5'
import { ImageOutline, GridOutline, SyncOutline, LinkOutline, NavigateOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode'
import TableEditor from '../TableEditor/index.vue'
import { useDocNodeRenderer, getSplitListChildren, getCepTaskNumber, isAllEinDataSameEffect, getDiffWordClass } from './functionals'
import { translatingNodeId } from '@/views/editor/components/NodeTree/functionals'
const getGraphicKey = (node: XmlNode): string => {
let key = node.attributes?.KEY || node.attributes?.GRAPHICKEY || node.attributes?.ID || ''
if (!key && node.children) {
const sheet = node.children.find((c) => c.tagName === 'SHEET')
if (sheet && sheet.attributes) {
key = sheet.attributes.GNBR || sheet.attributes.KEY || sheet.attributes.ID || ''
}
}
return key
}
const getReferencingNodes = (node: XmlNode): XmlNode[] => {
const key = getGraphicKey(node)
if (!key || !editorStore.nodeMap) return []
const refs: XmlNode[] = []
for (const item of editorStore.nodeMap.values()) {
const childNode = item.node
const attrs = childNode.attributes || {}
if (attrs.REFID === key || attrs.GRAPHICKEY === key || attrs.STRUCTID === key || attrs.GNBR === key) {
refs.push(childNode)
}
}
return refs
}
const getNodePreviewText = (node: XmlNode): string => {
if (node.textContent) return node.textContent.trim().substring(0, 25)
if (node.children && node.children.length > 0) {
for (const c of node.children) {
if (c.textContent) return c.textContent.trim().substring(0, 25)
}
}
return ''
}
const getRefTargetId = (node: XmlNode): string | null => {
let refId = node.attributes?.REFID || node.attributes?.STRUCTID || node.attributes?.GRAPHICKEY || node.attributes?.GNBR
if (!refId && node.textContent) {
refId = node.textContent.trim().replace(/^\(?(?:Ref:\s*|参考:\s*)?|\)?$/gi, '').replace(/\[Sh\.\d+\]/gi, '').trim()
}
if (refId) {
const target = editorStore.findNodeByRef(refId, 'GRAPHIC')
return target ? target.id : null
}
return null
}
const getImgSrc = (gnbr: string) => {
if (!gnbr) return ''
if (gnbr.startsWith('http://') || gnbr.startsWith('https://')) {
......@@ -1504,7 +1615,7 @@ const handleNodeClick = (e: MouseEvent) => {
if (refId) {
const targetNode = editorStore.findNodeByRef(refId, 'GRAPHIC')
if (targetNode) {
editorStore.setSelectedNodeId(targetNode.id)
editorStore.setSelectedNodeId(targetNode.id, true)
window.$message?.success?.(`已按 Ctrl 跳转到关联插图 <${targetNode.tagName}> (KEY/ID: ${refId})`)
return
} else {
......
......@@ -414,6 +414,10 @@ export function useEditorPanel() {
() => editorStore.lastUndoRedoTime,
() => syncEditorScroll(editorStore.selectedNodeId, true)
)
watch(
() => editorStore.forceScrollSignal,
() => syncEditorScroll(editorStore.selectedNodeId, true)
)
return {
selectedNode,
......
......@@ -390,6 +390,55 @@ export function useNodeTree(
}
attrSummary = attrParts.join(' | ')
subtitle = getNodeTextPreview(node, false)
} else if (node.tagName === 'GRAPHIC') {
let key = node.attributes?.KEY || node.attributes?.GRAPHICKEY || node.attributes?.ID || ''
if (!key && node.children) {
const sheet = node.children.find((c) => c.tagName === 'SHEET')
if (sheet && sheet.attributes) {
key = sheet.attributes.GNBR || sheet.attributes.KEY || sheet.attributes.ID || ''
}
}
const titleNode = node.children?.find((c) => c.tagName === 'TITLE' || c.tagName === 'TITLEC')
const graphicTitle = titleNode ? (titleNode.textContent || '').trim() : ''
let refCount = 0
const referencingTags = new Set<string>()
if (key && editorStore.nodeMap) {
for (const item of editorStore.nodeMap.values()) {
const n = item.node
const attrs = n.attributes || {}
if (attrs.REFID === key || attrs.GRAPHICKEY === key || attrs.STRUCTID === key || attrs.GNBR === key) {
refCount++
referencingTags.add(n.tagName)
}
}
}
const refTagStr = Array.from(referencingTags).join('/')
const refInfo = refCount > 0 ? ` [被 ${refTagStr} 引用 ${refCount} 次]` : ''
attrSummary = key ? `KEY: ${key}${refInfo}` : (refInfo.trim() || 'GRAPHIC')
subtitle = graphicTitle || getNodeTextPreview(node, false)
fullSubtitle = graphicTitle || getNodeTextPreview(node, true)
} else if (node.tagName === 'GRPHCREF') {
const refId = node.attributes?.REFID || node.attributes?.GRAPHICKEY || node.attributes?.STRUCTID || ''
const targetGraphic = refId ? editorStore.findNodeByRef(refId, 'GRAPHIC') : null
attrSummary = refId ? (targetGraphic ? `关联插图 KEY: ${refId}` : `未找到插图 ID: ${refId}`) : 'GRPHCREF'
subtitle = getNodeTextPreview(node, false)
fullSubtitle = getNodeTextPreview(node, true)
} else if (node.tagName === 'REFINT') {
const refId = node.attributes?.REFID || node.attributes?.STRUCTID || ''
attrSummary = refId ? `REFID: ${refId}` : 'REFINT'
subtitle = getNodeTextPreview(node, false)
fullSubtitle = getNodeTextPreview(node, true)
} else if (node.attributes.KEY) {
attrSummary = `KEY: ${node.attributes.KEY}`
subtitle = getNodeTextPreview(node, false)
} else if (node.attributes.GRAPHICKEY) {
attrSummary = `GRAPHICKEY: ${node.attributes.GRAPHICKEY}`
subtitle = getNodeTextPreview(node, false)
} else if (node.attributes.REFID) {
attrSummary = `REFID: ${node.attributes.REFID}`
subtitle = getNodeTextPreview(node, false)
} else if (node.attributes.ID) {
attrSummary = `ID: ${node.attributes.ID}`
subtitle = getNodeTextPreview(node, false)
......
......@@ -139,6 +139,28 @@
</n-icon>
</div>
<!-- 快捷定位引用 / 定位目标按钮 (在图标右侧直观展示) -->
<template v-if="item.tagName === 'GRAPHIC' && getTreeItemRefNodes(item.rawNode).length > 0">
<div
class="w-5 h-5 rounded hover:bg-primary/30 flex items-center justify-center mr-1 text-primary cursor-pointer transition-transform hover:scale-110 z-20 shrink-0 border border-primary/30"
:class="[effectiveSelectedId === item.id ? 'text-white border-white/50 bg-white/20' : 'bg-primary/10']"
title="点击直接定位至引用当前插图的位置"
@click.stop="handleTreeRefJump(item.rawNode)"
>
<n-icon size="12"><NavigateOutline /></n-icon>
</div>
</template>
<template v-else-if="(item.tagName === 'GRPHCREF' || item.tagName === 'REFINT') && getTreeItemTargetId(item.rawNode)">
<div
class="w-5 h-5 rounded hover:bg-primary/30 flex items-center justify-center mr-1 text-primary cursor-pointer transition-transform hover:scale-110 z-20 shrink-0 border border-primary/30"
:class="[effectiveSelectedId === item.id ? 'text-white border-white/50 bg-white/20' : 'bg-primary/10']"
title="点击直接定位至关联的插图/目标节点"
@click.stop="editorStore.setSelectedNodeId(getTreeItemTargetId(item.rawNode)!, true)"
>
<n-icon size="12"><NavigateOutline /></n-icon>
</div>
</template>
<!-- Label & Subtitle & AttrSummary -->
<div
class="flex-1 min-w-0 flex items-center space-x-1.5 z-10"
......@@ -230,7 +252,7 @@
</template>
<script setup lang="ts">
import { SearchOutline, ListOutline, TrashOutline, CloseOutline, SyncOutline, ArrowUpOutline } from '@vicons/ionicons5'
import { SearchOutline, ListOutline, TrashOutline, CloseOutline, SyncOutline, ArrowUpOutline, NavigateOutline } from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { VIRTUAL_LAYOUT_TAGS } from '@/configs/xmlTags'
import {
......@@ -258,6 +280,50 @@ const props = defineProps<{
const emit = defineEmits(['update:expandedKeys'])
const editorStore = useEditorStore()
const getGraphicKey = (node: any): string => {
let key = node.attributes?.KEY || node.attributes?.GRAPHICKEY || node.attributes?.ID || ''
if (!key && node.children) {
const sheet = node.children.find((c: any) => c.tagName === 'SHEET')
if (sheet && sheet.attributes) {
key = sheet.attributes.GNBR || sheet.attributes.KEY || sheet.attributes.ID || ''
}
}
return key
}
const getTreeItemRefNodes = (node: any): any[] => {
const key = getGraphicKey(node)
if (!key || !editorStore.nodeMap) return []
const refs: any[] = []
for (const item of editorStore.nodeMap.values()) {
const childNode = item.node
const attrs = childNode.attributes || {}
if (attrs.REFID === key || attrs.GRAPHICKEY === key || attrs.STRUCTID === key || attrs.GNBR === key) {
refs.push(childNode)
}
}
return refs
}
const getTreeItemTargetId = (node: any): string | null => {
let refId = node.attributes?.REFID || node.attributes?.STRUCTID || node.attributes?.GRAPHICKEY || node.attributes?.GNBR
if (!refId && node.textContent) {
refId = node.textContent.trim().replace(/^\(?(?:Ref:\s*|参考:\s*)?|\)?$/gi, '').replace(/\[Sh\.\d+\]/gi, '').trim()
}
if (refId) {
const target = editorStore.findNodeByRef(refId, 'GRAPHIC')
return target ? target.id : null
}
return null
}
const handleTreeRefJump = (node: any) => {
const refs = getTreeItemRefNodes(node)
if (refs.length > 0) {
editorStore.setSelectedNodeId(refs[0].id, true)
}
}
const insertFragmentModalRef = ref<any>(null)
const isAnyModalVisible = computed(() => {
......
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