Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
I
Ifar-Xml-Edtor
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
pangchong
Ifar-Xml-Edtor
Commits
846ea4b3
Commit
846ea4b3
authored
Jul 31, 2026
by
pangchong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 优化图片的展示交互
parent
b6bb941a
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
244 additions
and
9 deletions
+244
-9
src/store/editor/index.ts
+5
-1
src/store/editor/types.ts
+1
-0
src/views/editor/components/DocNodeRenderer/index.vue
+118
-7
src/views/editor/components/EditorPanel/functionals/index.ts
+4
-0
src/views/editor/components/NodeTree/functionals/index.ts
+49
-0
src/views/editor/components/NodeTree/index.vue
+67
-1
No files found.
src/store/editor/index.ts
View file @
846ea4b3
...
...
@@ -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
...
...
src/store/editor/types.ts
View file @
846ea4b3
...
...
@@ -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
// 在选中时是否跳过祖先展开逻辑
...
...
src/views/editor/components/DocNodeRenderer/index.vue
View file @
846ea4b3
...
...
@@ -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-o
utline
/></n-icon>
<span
class=
"text-xs font-bold text-color2 flex items-center space-x-
2
"
>
<n-icon><
ImageO
utline
/></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
>
定位引用位置 (
<
{{ getReferencingNodes(node)[0].tagName }}
>
)
</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"
>
<
{{ refNode.tagName }}
>
</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
{
...
...
src/views/editor/components/EditorPanel/functionals/index.ts
View file @
846ea4b3
...
...
@@ -414,6 +414,10 @@ export function useEditorPanel() {
()
=>
editorStore
.
lastUndoRedoTime
,
()
=>
syncEditorScroll
(
editorStore
.
selectedNodeId
,
true
)
)
watch
(
()
=>
editorStore
.
forceScrollSignal
,
()
=>
syncEditorScroll
(
editorStore
.
selectedNodeId
,
true
)
)
return
{
selectedNode
,
...
...
src/views/editor/components/NodeTree/functionals/index.ts
View file @
846ea4b3
...
...
@@ -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
)
...
...
src/views/editor/components/NodeTree/index.vue
View file @
846ea4b3
...
...
@@ -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
(()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment