zydx-plus 1.35.640 → 1.36.640

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zydx-plus",
3
- "version": "1.35.640",
3
+ "version": "1.36.640",
4
4
  "description": "Vue.js",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -0,0 +1,6 @@
1
+ import main from './src/graph.vue';
2
+
3
+ main.install = function(Vue) {
4
+ Vue.component(main.name, main);
5
+ };
6
+ export default main;
@@ -0,0 +1,207 @@
1
+ .graph-text{
2
+ width: 100%;
3
+ height: 100%;
4
+ font-size: 14px;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ position: relative;
9
+ }
10
+ .graph-input{
11
+ width: 150%;
12
+ height: 40px;
13
+ border: 2px solid #4B0C77;
14
+ border-radius: 10px;
15
+ position: absolute;
16
+ top: 50%;
17
+ left: -25%;
18
+ transform: translateY(-50%);
19
+ }
20
+ .graph-content{
21
+ width: 100%;
22
+ height: 100%;
23
+ overflow: hidden;
24
+ position: relative;
25
+ }
26
+ .full-screen {
27
+ background: #fff;
28
+ width: 100vw;
29
+ height: 100vh;
30
+ position: fixed;
31
+ top: 0;
32
+ left: 0;
33
+ z-index: 100;
34
+ }
35
+ .graph-tab{
36
+ height: 30px;
37
+ display: flex;
38
+ border: 1px solid #ccc;
39
+ }
40
+ .graph-tab button{
41
+ height: 100%;
42
+ background-color: #fff;
43
+ border: none;
44
+ border-right: 1px solid #ccc;
45
+ cursor: pointer;
46
+ padding: 0 10px;
47
+ flex: 1;
48
+ }
49
+ .graph-tab button:last-child{
50
+ border-right: none;
51
+ }
52
+ .graph-relation{
53
+ height: calc(100% - 30px);
54
+ }
55
+ .graph-relation-only{
56
+ height: 100%;
57
+ }
58
+ .graph-tool {
59
+ position: absolute;
60
+ right: 0;
61
+ top: 50%;
62
+ background-color: #fff;
63
+ border-bottom-left-radius: 5px;
64
+ border-top-left-radius: 5px;
65
+ padding: 10px;
66
+ box-shadow: rgba(0, 0, 0, 0.06) 0 2px 16px 0;
67
+ z-index: 501;
68
+ width: 320px;
69
+ display: none;
70
+ margin-top: -180px;
71
+ }
72
+ .graph-tool-line {
73
+ position: absolute;
74
+ right: 0;
75
+ top: 50%;
76
+ background-color: #fff;
77
+ border-bottom-left-radius: 5px;
78
+ border-top-left-radius: 5px;
79
+ padding: 10px;
80
+ box-shadow: rgba(0, 0, 0, 0.06) 0 2px 16px 0;
81
+ z-index: 501;
82
+ width: 320px;
83
+ display: none;
84
+ margin-top: -140px;
85
+ }
86
+ .graph-tool-show {
87
+ animation: nodeShow 0.3s;
88
+ display: block;
89
+ }
90
+
91
+ .graph-tool-hide {
92
+ animation: nodeHide 0.3s;
93
+ }
94
+
95
+ @keyframes nodeHide {
96
+ 0% {
97
+ opacity: 1;
98
+ transform: translateX(0);
99
+ }
100
+ 100% {
101
+ opacity: 0;
102
+ transform: translateX(110%);
103
+ }
104
+ }
105
+
106
+ @keyframes nodeShow {
107
+ 0% {
108
+ opacity: 0;
109
+ transform: translateX(110%);
110
+ }
111
+ 10% {
112
+ opacity: 1;
113
+ transform: translateX(100%);
114
+ }
115
+ 100% {
116
+ opacity: 1;
117
+ transform: translateX(0);
118
+ }
119
+ }
120
+ .style-li{
121
+ position: relative;
122
+ }
123
+ .style-li img{
124
+ position: absolute;
125
+ top: 0;
126
+ right: 0;
127
+ z-index: 1;
128
+ width: 15px;
129
+ height: 15px;
130
+ cursor: pointer;
131
+ }
132
+ .style-list {
133
+ display: flex;
134
+ padding: 5px 0;
135
+ }
136
+ .node-but {
137
+ display: flex;
138
+ padding: 5px 0;
139
+ }
140
+
141
+ .node-but button {
142
+ flex: 1;
143
+ margin-right: 5px;
144
+ background-color: #fff;
145
+ border: 1px solid #ccc;
146
+ border-radius: 3px;
147
+ padding: 5px 10px;
148
+ cursor: pointer;
149
+ font-size: 12px;
150
+ }
151
+
152
+ .node-but button:last-child {
153
+ margin: 0;
154
+ }
155
+ .node-left {
156
+ display: flex;
157
+ padding-right: 10px;
158
+ }
159
+
160
+ .node-left > span {
161
+ display: inline-block;
162
+ width: 80px;
163
+ line-height: 32px;
164
+ padding-right: 10px;
165
+ box-sizing: border-box;
166
+ }
167
+ .style-title {
168
+ font-size: 16px;
169
+ font-weight: bold;
170
+ padding: 5px 0 10px 0;
171
+ }
172
+ .padd {
173
+ padding-right: 0;
174
+ }
175
+ .backColor {
176
+ width: 93px;
177
+ height: 32px;
178
+ border: 1px solid #ccc;
179
+ border-radius: 3px;
180
+ margin-left: -40px;
181
+ box-shadow: 0 1px 0 1px rgb(0 0 0 / 4%);
182
+ cursor: pointer;
183
+ background-color: #549688;
184
+ }
185
+ .node-input{
186
+ width: 220px;
187
+ height: 32px;
188
+ border: 1px solid #aaa;
189
+ border-radius: 3px;
190
+ padding: 0 10px;
191
+ box-sizing: border-box;
192
+ }
193
+ .node-back-color{
194
+ width: 220px;
195
+ height: 32px;
196
+ border: 1px solid #ccc;
197
+ border-radius: 3px;
198
+ box-shadow: 0 1px 0 1px rgb(0 0 0 / 4%);
199
+ cursor: pointer;
200
+ background-color: #549688;
201
+ }
202
+ .node-select :deep(.wrapper) {
203
+ width: 220px;
204
+ }
205
+ :deep(.rel-canvas-slot-behind) {
206
+ z-index: 600;
207
+ }
@@ -0,0 +1,502 @@
1
+ <template>
2
+ <!--
3
+ 关系图组件 - zydx-graph
4
+ 基于 relation-graph-vue3 封装,支持节点增删、关联线绘制、节点/连线样式编辑、
5
+ 拖拽、撤销/重做、全屏、下载图片(PNG)。
6
+ 通过 v-model:value 与父组件双向同步图谱数据。
7
+ -->
8
+ <div class="graph-content" :class="{'full-screen': !fullShow}">
9
+ <div class="graph-tab" v-if="!readOnly">
10
+ <button :disabled="!nodeData" @click="add">添加节点</button>
11
+ <button :disabled="delDisabled" @click="del">删除节点</button>
12
+ <button :disabled="!nodeData && !lines" @click="style">设置样式</button>
13
+ <button @click="dots">关联线</button>
14
+ <button :disabled="undoDisabled" @click="undo">上一步</button>
15
+ <button :disabled="forwardDisabled" @click="redo">下一步</button>
16
+ <button @click="download">下载图片</button>
17
+ <button @click="fullTap(false)" v-if="fullShow && full">全屏</button>
18
+ <button @click="fullDel(true)" v-if="!fullShow">退出</button>
19
+ </div>
20
+ <div :class="{'graph-relation-only': readOnly, 'graph-relation': !readOnly}" ref="graphContent">
21
+ <RelationGraph ref="graphRef" :options="options" @node-click="onNodeClick" @line-click="onLineClick" @canvas-click="onCanvasClick" @node-drag-end="nodeDragEnd">
22
+ <template #graph-plug>
23
+ <RGEditingController v-if="!readOnly">
24
+ <RGEditingResize />
25
+ </RGEditingController>
26
+ </template>
27
+ <template #node="{node}">
28
+ <div class="graph-text" @dblclick="startEditNodeText(node)">
29
+ <input class="graph-input" v-if="node.textEdit" type="text" @blur="stopEditNodeText(node)" v-model="node.text" />
30
+ <span v-else :style="{'font-size': node.fontSize + 'px', 'line-height': node.lineHeight + 'px', 'font-weight': node.fontWeight, 'font-style': node.fontStyle, 'text-decoration': node.textDecoration}">{{ node.text }}</span>
31
+ </div>
32
+ </template>
33
+ </RelationGraph>
34
+ </div>
35
+ <Tool v-model:open="styleStatus" @select="toolSelect" :node="nodeData"></Tool>
36
+ <LineTool v-model:open="lineStatus" :node="lines" @select="toolSelectLine"></LineTool>
37
+ </div>
38
+ </template>
39
+
40
+ <script>
41
+ /**
42
+ * zydx-graph 关系图组件
43
+ * 功能:节点增删、关联线绘制、样式编辑、拖拽、撤销/重做、全屏、导出 PNG。
44
+ * 数据通过 v-model:value(nodes/lines)与父组件双向同步。
45
+ */
46
+ import RelationGraph, {RGEditingResize, RGEditingController} from 'relation-graph-vue3'
47
+ import Tool from './tool.vue'
48
+ import LineTool from './lineTool.vue'
49
+ export default {
50
+ name: 'zydx-graph',
51
+ components: {
52
+ RelationGraph,
53
+ RGEditingResize,
54
+ RGEditingController,
55
+ Tool,
56
+ LineTool
57
+ },
58
+ data() {
59
+ return {
60
+ // ===== RelationGraph 配置项 =====
61
+ options: {
62
+ allowShowMiniToolBar: false, // 是否显示小工具栏
63
+ defaultLineMarker: {
64
+ markerWidth: 15,
65
+ markerHeight: 15,
66
+ refX: 10,
67
+ refY: 7,
68
+ data: "M 14 7 L 1 .3 L 4 7 L .4 13 L 14 7, Z"
69
+ },
70
+ },
71
+ graphInstance: null, // RelationGraph 实例(通过 $refs.graphRef.getInstance() 获取)
72
+ nodeData: null, // 当前选中的节点
73
+ nodes: null, // 图谱数据(含 nodes / lines 数组),组件唯一数据源
74
+ lines: null, // 当前选中的关联线
75
+ styleStatus: false, // 节点样式弹窗可见性
76
+ top: 0, // 选中节点中心点纵坐标(用于样式弹窗定位)
77
+ left: 0, // 选中节点中心点横坐标
78
+ angle: 0, // 预留:节点旋转角度(暂未使用)
79
+ width: 0, // 预留:节点宽度(暂未使用)
80
+ graphLine: false,// 关联线绘制态标记
81
+ lineStatus: false, // 关联线样式弹窗可见性
82
+ delDisabled: true, // 删除按钮禁用态(有子节点时禁用)
83
+ fullShow: true, // 全屏状态标识:true=非全屏,false=全屏
84
+ // ===== 撤销/重做历史栈 =====
85
+ /** 历史快照栈(深拷贝的 nodes 数据) */
86
+ history: [],
87
+ /** 当前历史指针位置 */
88
+ historyIndex: -1,
89
+ /** 上一步按钮禁用态 */
90
+ undoDisabled: true,
91
+ /** 下一步按钮禁用态 */
92
+ forwardDisabled: true,
93
+ // ===== 新建节点默认样式 =====
94
+ default: {
95
+ text: '节点',
96
+ fontSize: 14,
97
+ lineHeight: 1.5,
98
+ fontColor: 'rgba(255, 255, 255, 1)',
99
+ fontWeight: 'normal',
100
+ fontStyle: 'normal',
101
+ textDecoration: 'none',
102
+ color: 'rgba(64, 158, 255, 1)',
103
+ borderWidth: '10',
104
+ borderColor: 'rgba(64, 158, 255, 1)',
105
+ nodeShape: '0',
106
+ width: 100,
107
+ height: 100,
108
+ fixed: true // 固定坐标,避免被布局器重排
109
+ },
110
+ // ===== 新建关联线默认样式 =====
111
+ defaultLine: {
112
+ color: 'rgba(95,176,253,1)',
113
+ lineWidth: '2',
114
+ text: '',
115
+ fontColor: 'rgba(64, 158, 255, 1)',
116
+ lineShape: 1
117
+ },
118
+ top2: 0, // 预留:关联线弹窗定位(暂未使用)
119
+ left2: 0,
120
+ }
121
+ },
122
+ emits: ['update:value'], // 数据变更时向父组件回传最新图谱数据(v-model:value)
123
+ props: {
124
+ /** 图谱数据(v-model),含 nodes / lines */
125
+ value: {
126
+ type: Object,
127
+ default: () => {}
128
+ },
129
+ /** 是否只读模式(隐藏工具栏、禁用编辑) */
130
+ readOnly: {
131
+ type: Boolean,
132
+ default: false
133
+ },
134
+ /** 是否显示全屏按钮 */
135
+ full: {
136
+ type: Boolean,
137
+ default: true
138
+ }
139
+ },
140
+ watch: {
141
+ // 外部 value 变化:无 lines 时(如初始单节点)用最新数据重载画布
142
+ value: {
143
+ handler(val) {
144
+ if(!val.lines) return
145
+ if (this.graphInstance&&this.nodes.lines.length === 0) this.safeLoadData(val);
146
+ this.nodes = val
147
+ },
148
+ deep: true,
149
+ immediate: true
150
+ },
151
+ // 内部 nodes 变化:同步回传给父组件(双向绑定)
152
+ nodes: {
153
+ handler(val) {
154
+ this.$emit('update:value', val)
155
+ },
156
+ deep: true
157
+ }
158
+ },
159
+ mounted() {
160
+ // 等 DOM 就绪后获取 RelationGraph 实例并初始化数据
161
+ this.$nextTick(() => {
162
+ this.graphInstance = this.$refs.graphRef.getInstance();
163
+ this.init()
164
+ })
165
+ // 监听窗口尺寸变化,全屏/退出或窗口缩放时重绘画布
166
+ window.addEventListener('resize', this.handleWindowResize)
167
+ },
168
+ beforeUnmount() {
169
+ // 移除窗口事件监听,避免内存泄漏
170
+ window.removeEventListener('resize', this.handleWindowResize)
171
+ },
172
+ methods: {
173
+ // ===== 全屏相关 =====
174
+ /** 进入全屏:标记状态并让 body 全屏,随后重绘画布铺满视口 */
175
+ fullTap(v) {
176
+ this.fullShow = v
177
+ this.fullScreen(document.body)
178
+ // 进入全屏后容器尺寸变化,触发画布重绘以铺满全屏
179
+ this.$nextTick(() => {
180
+ if (this.$refs.graphRef && this.$refs.graphRef.onGraphResize) {
181
+ this.$refs.graphRef.onGraphResize()
182
+ }
183
+ })
184
+ },
185
+ // 退出全屏并复位状态
186
+ fullDel(v) {
187
+ this.fullShow = v
188
+ this.cancelFullscreen()
189
+ this.$nextTick(() => {
190
+ if (this.$refs.graphRef && this.$refs.graphRef.onGraphResize) {
191
+ this.$refs.graphRef.onGraphResize()
192
+ }
193
+ })
194
+ },
195
+ // 窗口尺寸变化:非全屏时复位 fullShow,并重绘画布
196
+ handleWindowResize() {
197
+ if (!document.fullscreenElement) {
198
+ this.fullShow = true
199
+ }
200
+ if (this.$refs.graphRef && this.$refs.graphRef.onGraphResize) {
201
+ this.$refs.graphRef.onGraphResize()
202
+ }
203
+ },
204
+ // 跨浏览器请求全屏
205
+ fullScreen(element) {
206
+ if (element.requestFullScreen) {
207
+ element.requestFullScreen()
208
+ } else if (element.webkitRequestFullScreen) {
209
+ element.webkitRequestFullScreen()
210
+ } else if (element.mozRequestFullScreen) {
211
+ element.mozRequestFullScreen()
212
+ }
213
+ },
214
+ // 跨浏览器退出全屏
215
+ cancelFullscreen() {
216
+ if (document.exitFullscreen) {
217
+ document.exitFullscreen();
218
+ } else if (document.msExitFullscreen) {
219
+ document.msExitFullscreen();
220
+ } else if (document.mozCancelFullScreen) {
221
+ document.mozCancelFullScreen();
222
+ } else if (document.webkitExitFullscreen) {
223
+ document.webkitExitFullscreen();
224
+ }
225
+ },
226
+ // 节点样式选择回调:把样式应用到选中节点并同步到数据,记录历史
227
+ toolSelect(v) {
228
+ const node = this.graphInstance.getNodeById(this.nodeData.id)
229
+ node[v.type] = v.value
230
+ this.nodeUpdate(node, v)
231
+ this.recordHistory()
232
+ },
233
+ // 关联线样式选择回调:把样式应用到选中关联线并同步到数据,记录历史
234
+ toolSelectLine(v) {
235
+ const links = this.graphInstance.getLinks()
236
+ const lines = links.reduce((currentLines, link) => currentLines.concat(...link.relations), []);
237
+ lines.forEach(line => {
238
+ if(line.id === this.lines.id) line[v.type] = v.value
239
+ })
240
+ this.linesUpdate(this.lines, v)
241
+ this.recordHistory()
242
+ },
243
+ // 数据校验:无 lines 时构造一个默认根节点,否则原样返回
244
+ verification(v) {
245
+ if(!v.lines) {
246
+ return {
247
+ rootId: this.rootId(),
248
+ nodes: [{id: this.rootId(), ...this.default}],
249
+ lines: []
250
+ }
251
+ }else {
252
+ return v
253
+ }
254
+ },
255
+ // 添加节点
256
+ add() {
257
+ const id = this.rootId()
258
+ const node = this.graphInstance.getNodeById(this.nodeData.id)
259
+ // 从父节点出发,螺旋搜索一个不与已有节点重叠的空位
260
+ const pos = this.getFreePosition(node)
261
+ const data = {
262
+ nodes: [{id: id, x: pos.x, y: pos.y, ...this.default}],
263
+ lines: [{id: this.rootId(), from: this.nodeData.id, to: id, ...this.defaultLine}]
264
+ }
265
+ this.nodes.nodes.push(data.nodes[0])
266
+ this.nodes.lines.push(data.lines[0])
267
+ this.graphInstance.addNodes(data.nodes)
268
+ this.graphInstance.addLines(data.lines)
269
+ this.onCanvasClick()
270
+ this.recordHistory()
271
+ },
272
+ /**
273
+ * 从指定节点出发,螺旋向外搜索一个不与任何已有节点重叠的空位
274
+ * @param {{x:number,y:number,id:string}} parent 父节点
275
+ * @returns {{x:number,y:number}} 新节点坐标
276
+ */
277
+ getFreePosition(parent) {
278
+ const step = (this.default.width || 100) + 100 // 节点尺寸 + 100 间距,确保新节点明显离开父节点
279
+ // 候选方向:优先右侧、下方,再向四周螺旋扩展
280
+ const dirs = [
281
+ {x: 1, y: 0}, {x: 0, y: 1}, {x: 1, y: 1}, {x: -1, y: 1},
282
+ {x: -1, y: 0}, {x: 0, y: -1}, {x: -1, y: -1}, {x: 1, y: -1}
283
+ ]
284
+ for (let r = 1; r <= 30; r++) {
285
+ for (const d of dirs) {
286
+ const x = parent.x + d.x * step * r
287
+ const y = parent.y + d.y * step * r
288
+ if (this.isFree(x, y, parent.id)) return {x, y}
289
+ }
290
+ }
291
+ // 极端情况下兜底:在父节点右下方固定偏移
292
+ return {x: parent.x + step * 2, y: parent.y + step * 2}
293
+ },
294
+ /**
295
+ * 判断坐标 (x,y) 处放置新节点是否与已有节点重叠
296
+ * @param {number} x 候选中心点 x
297
+ * @param {number} y 候选中心点 y
298
+ * @param {string} excludeId 排除的节点(父节点本身)
299
+ * @returns {boolean} 不重叠返回 true
300
+ */
301
+ isFree(x, y, excludeId) {
302
+ const w = this.default.width || 100
303
+ const h = this.default.height || 100
304
+ const pad = 10
305
+ for (const n of this.nodes.nodes) {
306
+ if (n.id === excludeId) continue
307
+ const nw = n.width || w
308
+ const nh = n.height || h
309
+ if (Math.abs(x - n.x) < (w + nw) / 2 + pad &&
310
+ Math.abs(y - n.y) < (h + nh) / 2 + pad) {
311
+ return false
312
+ }
313
+ }
314
+ return true
315
+ },
316
+ // 删除节点:仅允许删除无子节点的末端节点,删除后记录历史
317
+ del() {
318
+ if(this.nodes.nodes.length === 1) return
319
+ const node = this.graphInstance.getNodeById(this.nodeData.id)
320
+ if(node.targetTo.length === 0) {
321
+ const index = this.nodes.nodes.findIndex(x => x.id === this.nodeData.id)
322
+ this.nodes.nodes.splice(index, 1)
323
+ this.graphInstance.setEditingNodes([]);
324
+ this.graphInstance.removeNodeById(this.nodeData.id)
325
+ this.recordHistory()
326
+ }
327
+ },
328
+ // 设置样式
329
+ style() {
330
+ if(this.lines) this.lineStatus = true
331
+ if(this.nodeData) this.styleStatus = true
332
+ },
333
+ // 关联线:进入连线绘制模式,松开后生成关联线并写入数据、记录历史
334
+ dots(e) {
335
+ this.graphInstance.startCreatingLinePlot(e, {
336
+ template: {
337
+ lineWidth: 3,
338
+ color: 'rgba(95,176,253,1)',
339
+ text: '',
340
+ },
341
+ onCreateLine: (from, to) => {
342
+ if (to.id) {
343
+ const line = {from: from.id, to: to.id, ...this.defaultLine}
344
+ this.graphInstance.addLines([line])
345
+ this.nodes.lines.push(line)
346
+ this.recordHistory()
347
+ }
348
+ },
349
+ })
350
+ },
351
+ // 初始化
352
+ init() {
353
+ this.nodes = this.verification(this.value)
354
+ this.safeLoadData(this.nodes); //加载数据(防闪烁)
355
+ this.graphInstance.setZoom(60);
356
+ this.recordHistory(); // 记录初始快照
357
+ },
358
+ // 点击节点:同步尺寸、设置选中态与编辑态、更新删除按钮可用性
359
+ onNodeClick(node) {
360
+ if(this.readOnly) return
361
+ this.widthUpdate()
362
+ this.delDisabled = node.targetTo.length !== 0
363
+ this.top = node.y + node.height / 2
364
+ this.left = node.x + node.width / 2
365
+ this.lines = null
366
+ this.nodeData = node
367
+ this.styleStatus = false
368
+ this.lineStatus = false
369
+ if(!node.textEdit) this.graphInstance.setEditingNodes([node]);
370
+ },
371
+ // 点击线
372
+ onLineClick(line) {
373
+ if(this.readOnly) return
374
+ this.widthUpdate()
375
+ this.lines = line
376
+ },
377
+ // 点击画布:清空选中/编辑态,取消所有节点编辑
378
+ onCanvasClick() {
379
+ this.widthUpdate()
380
+ this.nodeData = null
381
+ this.graphLine = false
382
+ this.styleStatus = false
383
+ this.lineStatus = false
384
+ this.delDisabled = true
385
+ this.graphInstance.setEditingNodes([]); // 点击画布时,取消编辑状态
386
+ },
387
+ // 双击节点:进入文本编辑态
388
+ startEditNodeText(node) {
389
+ if(this.readOnly) return
390
+ node.textEdit = true
391
+ this.graphInstance.setEditingNodes([])
392
+ },
393
+ // 拖拽结束:把节点最新坐标同步到数据并记录历史
394
+ nodeDragEnd(node) {
395
+ if(this.readOnly) return
396
+ this.nodeUpdate(node, {type: 'x', value: node.x})
397
+ this.nodeUpdate(node, {type: 'y', value: node.y})
398
+ this.recordHistory()
399
+ },
400
+ // 文本编辑结束(失焦):保存节点文本到数据并记录历史
401
+ stopEditNodeText(node) {
402
+ node.textEdit = false
403
+ const data = this.nodes.nodes.filter(x => x.id === this.nodeData.id)
404
+ if(data.length > 0) data[0].text = node.text
405
+ this.nodeUpdate(node, {type: 'text', value: node.text})
406
+ this.recordHistory()
407
+ },
408
+ // 把节点某字段的变更写回 this.nodes 数据源
409
+ nodeUpdate(node, v) {
410
+ const data = this.nodes.nodes.filter(x => x.id === node.id)
411
+ if(data.length > 0) data[0][v.type] = v.value
412
+ },
413
+ // 把关联线某字段的变更写回 this.nodes.lines 数据源
414
+ linesUpdate(line, v) {
415
+ const data = this.nodes.lines.filter(x => x.id === line.id)
416
+ if(data.length > 0) data[0][v.type] = v.value
417
+ },
418
+ // 同步选中节点的宽高到数据源(拖拽缩放后调用)
419
+ widthUpdate() {
420
+ if(!this.nodeData || this.readOnly) return
421
+ const node = this.graphInstance.getNodeById(this.nodeData.id)
422
+ this.nodeUpdate(node, {type: 'width', value: node.width})
423
+ this.nodeUpdate(node, {type: 'height', value: node.height})
424
+ },
425
+ // 生成随机唯一 ID(用作节点/连线标识)
426
+ rootId() {
427
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
428
+ },
429
+ // ===== 撤销/重做 =====
430
+ /**
431
+ * 记录一次历史快照(在每次数据变更后调用)
432
+ * 会丢弃当前指针之后的"重做分支",再追加新状态
433
+ */
434
+ recordHistory() {
435
+ if (!this.nodes) return
436
+ if (this.historyIndex < this.history.length - 1) {
437
+ this.history = this.history.slice(0, this.historyIndex + 1)
438
+ }
439
+ this.history.push(JSON.parse(JSON.stringify(this.nodes)))
440
+ this.historyIndex = this.history.length - 1
441
+ this.updateUndoRedoState()
442
+ },
443
+ /** 上一步:回退到上一快照 */
444
+ undo() {
445
+ if (this.historyIndex <= 0) return
446
+ this.historyIndex--
447
+ this.restoreHistory()
448
+ },
449
+ /** 下一步:前进到下一快照 */
450
+ redo() {
451
+ if (this.historyIndex >= this.history.length - 1) return
452
+ this.historyIndex++
453
+ this.restoreHistory()
454
+ },
455
+ /**
456
+ * 防闪烁地加载数据。
457
+ * setJsonData 内部会把画布透明度设为 0.01,并在 doLayout 中延迟约 300ms 才恢复,
458
+ * 这期间画布几乎全透明,导致"闪"一下。这里不 await,立即把透明度复位,
459
+ * 覆盖那 300ms 的透明窗口,从而消除闪烁。初始化、撤销/重做、value 回写均走此封装。
460
+ * @param {Object} data 图谱数据
461
+ * @returns {Promise}
462
+ */
463
+ safeLoadData(data) {
464
+ const inst = this.graphInstance
465
+ const p = inst.setJsonData(data)
466
+ inst.options.canvasOpacity = 1
467
+ if (inst._dataUpdated) inst._dataUpdated()
468
+ p.then(() => {
469
+ inst.options.canvasOpacity = 1
470
+ })
471
+ return p
472
+ },
473
+ /** 用当前指针指向的快照恢复画布 */
474
+ restoreHistory() {
475
+ const snapshot = JSON.parse(JSON.stringify(this.history[this.historyIndex]))
476
+ this.nodes = snapshot
477
+ this.safeLoadData(snapshot)
478
+ this.nodeData = null
479
+ this.lines = null
480
+ this.graphInstance.setEditingNodes([])
481
+ this.delDisabled = true
482
+ this.updateUndoRedoState()
483
+ },
484
+ /** 更新上一步/下一步按钮的禁用态 */
485
+ updateUndoRedoState() {
486
+ this.undoDisabled = this.historyIndex <= 0
487
+ this.forwardDisabled = this.historyIndex >= this.history.length - 1
488
+ },
489
+ // 下载图片:将当前关系图导出为 png 并触发浏览器下载
490
+ async download() {
491
+ if (!this.graphInstance) return
492
+ try {
493
+ await this.graphInstance.downloadAsImage('png', 'graph')
494
+ } catch (e) {
495
+ console.error('[graph] 下载图片失败:', e)
496
+ }
497
+ }
498
+ }
499
+ }
500
+ </script>
501
+
502
+ <style scoped src="./graph.css"></style>
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <div class="graph-tool-line" :class="{'graph-tool-show': show, 'graph-tool-hide': !show}">
3
+ <div class="style-li">
4
+ <img @click="nodeStyle" src="data:image/png;base64,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" alt="" />
5
+ <div class="style-title">连接线</div>
6
+ <div class="style-list">
7
+ <div class="node-left padd">
8
+ <span>文字</span>
9
+ <input class="node-input" v-model="text" type="text" placeholder="请输入连接线文字" />
10
+ </div>
11
+ </div>
12
+ <div class="style-list">
13
+ <div class="node-left padd node-select">
14
+ <span>线宽度</span>
15
+ <Select :options="optionLine" v-model:value="lineValue"></Select>
16
+ </div>
17
+ </div>
18
+ <div class="style-list">
19
+ <div class="node-left padd node-select">
20
+ <span>样式</span>
21
+ <Select :options="styleLine" v-model:value="styleValue"></Select>
22
+ </div>
23
+ </div>
24
+ <div class="style-list">
25
+ <div class="node-left padd">
26
+ <span>文字颜色</span>
27
+ <div @click="colorTap($event,'fontColor')" class="node-back-color"></div>
28
+ </div>
29
+ </div>
30
+ <div class="style-list">
31
+ <div class="node-left padd">
32
+ <span>线颜色</span>
33
+ <div @click="colorTap($event,'color')" class="node-back-color"></div>
34
+ </div>
35
+ </div>
36
+ <div class="back-col" :style="{top: top + 'px'}" v-if="colorShow">
37
+ <Coloring @submit="submit" @empty="empty" @rgba="rgba" :color="color"></Coloring>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <script>
44
+ import Select from '../../select/src/select.vue'
45
+ import Coloring from '../../coloring/src/coloring.vue'
46
+
47
+ export default {
48
+ components: {Select, Coloring},
49
+ data() {
50
+ return {
51
+ show: false,
52
+ colorShow: false,
53
+ optionLine: [
54
+ {label: "1", value: '1'},
55
+ {label: "2", value: '2'},
56
+ {label: "3", value: '3'},
57
+ {label: "4", value: '4'},
58
+ {label: "5", value: '5'},
59
+ {label: "6", value: '6'},
60
+ {label: "7", value: '7'},
61
+ {label: "8", value: '8'},
62
+ {label: "9", value: '9'},
63
+ {label: "10", value: '10'},
64
+ ],
65
+ lineValue: '2',
66
+ styleValue: '1',
67
+ type: '',
68
+ color: 'rgba(84,150,136,1)',
69
+ text: '',
70
+ styleLine: [
71
+ {label: "直线", value: '1'},
72
+ {label: "鱼尾", value: '3'},
73
+ {label: "折线", value: '4'},
74
+ {label: "弧线", value: '5'},
75
+ {label: "曲线", value: '6'},
76
+ ]
77
+ }
78
+ },
79
+ emits: ['update:open', 'select'],
80
+ props: {
81
+ open: {
82
+ type: Boolean,
83
+ default: false
84
+ },
85
+ node: {
86
+ type: Object,
87
+ default: () => {}
88
+ }
89
+ },
90
+ watch: {
91
+ open: {
92
+ handler(val) {
93
+ this.show = val
94
+ if(!val) {
95
+ this.colorShow = false
96
+ return
97
+ }
98
+ this.styleValue = this.node.lineWidth || '2'
99
+ this.text = this.node.text || ''
100
+ this.styleValue = this.node.lineShape.toString() || '1'
101
+ if(this.type === 'fontColor') this.color = this.node.fontColor || 'rgba(64, 158, 255, 1)'
102
+ if(this.type === 'color') this.color = this.node.color || 'rgba(95,176,253,1)'
103
+ },
104
+ deep: true
105
+ },
106
+ show: {
107
+ handler(val) {
108
+ this.$emit('update:open', val)
109
+ },
110
+ deep: true
111
+ },
112
+ text: {
113
+ handler(val) {
114
+ this.$emit('select', {type: 'text', value: val})
115
+ },
116
+ deep: true
117
+ },
118
+ styleValue: {
119
+ handler(val) {
120
+ this.$emit('select', {type: 'lineShape', value: Number(val)})
121
+ },
122
+ deep: true
123
+ },
124
+ lineValue: {
125
+ handler(val) {
126
+ this.$emit('select', {type: 'lineWidth', value: val})
127
+ },
128
+ deep: true
129
+ },
130
+ },
131
+ mounted() {
132
+
133
+ },
134
+ methods: {
135
+ nodeStyle() {
136
+ this.show = false
137
+ },
138
+ colorTap(e, v) {
139
+ this.type = v
140
+ this.colorShow = true
141
+ },
142
+ submit() {
143
+ this.colorShow = false
144
+ },
145
+ empty() {
146
+ this.colorShow = false
147
+ },
148
+ rgba(v) {
149
+ this.$emit('select', {type: this.type, value: v.rgba})
150
+ },
151
+ }
152
+ }
153
+ </script>
154
+
155
+ <style scoped src="./graph.css"></style>
@@ -0,0 +1,329 @@
1
+ <template>
2
+ <!--
3
+ 图谱节点样式工具 - graph-tool
4
+ 用于编辑 relation-graph 中选中节点的文字、边框、背景、形状样式。
5
+ -->
6
+ <div class="graph-tool" :class="{'graph-tool-show': show, 'graph-tool-hide': !show}">
7
+ <div class="style-li">
8
+ <!-- 关闭按钮 -->
9
+ <img @click="nodeStyle" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAADTFJREFUeF7t3VmspEUVB/A/r5oYH+TFBF/UFwcSjcsD4oOJaNQXjBERXMBdwRFhcEMdkUWRGVHGfQOVRVEU3AVBdBhGNlcWFRAFFBTcd0UxxVTP3NvTfbu66vxr/Xcy4WG+OlV16vzu+b6+Pc0e0EsZUAbmZmAP5UYZUAbmZ0BAVB3KwBoZEBCVhzIgIKoBZSAuA+ogcXnTqEEyICCDHLS2GZcBAYnLm0YNkgEBGeSgtc24DAhIXN40apAMCMggB61txmXAEshDAewL4B4AWwHcHrckjVIGgjMwqTk34HIANwePDLzQAshbATwTwN5Tc24HcBqATweuRZcpA6EZOBLAi2bU3LUAzgFwUmigRdelAHkMgAsAPHjBJA7I8QCuX7QY/b0ysCADjwBwIoADFlznoOxjkc0UIPcusYDrABwoJEtkTJdOZ8DhOBfAusDUXArgiYHXzr0sFsixAE5YcnIhWTJhunxnBpbFMRm4HsCWlDzGAHkQgLsiJxWSyMQNPCwWxyRlewK4OzZ/MUAeD+Cy2AkBCElC8gYbmorDpWs/ANti8xYD5IUAPhY7oR8nJIkJHGC4BQ6XJvdu18dj8xUD5GgAm2InXDFOSAyS2GkIKxwuPRsAbI7NUwyQ/QFcGDvh1DghMUpkR2Escbi0PBnARbH5iQGyF4BbYyecMU5IDJPZeChrHC4dDwFwW2xeYoC4udxvKw+KnVRIDDPXTygGDves/OKUFMUCeZR/J+t+KZPrdsswe22HYuBwdyZPAnBnSmpigbg53WewNqZMrk5inL02wzFwuEw8A8D5qSlJASIkqdnXeBaOZ/uPpSRnOBWIkCQfwbABqsfhTsYCiJAMW+PRG2fhcG8cfSZ6VTMGWgE[... omitted end of long line]" alt="" />
10
+
11
+ <!-- ===== 文字样式 ===== -->
12
+ <div class="style-title">文字</div>
13
+ <div class="style-list">
14
+ <div class="node-left">
15
+ <span>字号</span>
16
+ <Select :options="optionSize" v-model:value="sizeValue"></Select>
17
+ </div>
18
+ <div class="node-left padd">
19
+ <span>行高</span>
20
+ <Select :options="optionRow" v-model:value="rowValue"></Select>
21
+ </div>
22
+ </div>
23
+ <div class="node-but">
24
+ <button @click="colorTap($event,'fontColor')">颜色</button>
25
+ <button :style="{'background-color': (boldAct)? '#549688': '#fff','color': (boldAct)? '#fff': '#333'}"
26
+ @click="bolds">粗体</button>
27
+ <button :style="{'background-color': (italicAct)? '#549688': '#fff','color': (italicAct)? '#fff': '#333'}"
28
+ @click="italics">斜体</button>
29
+ <button :style="{'background-color': (scribeAct)? '#549688': '#fff','color': (scribeAct)? '#fff': '#333'}"
30
+ @click="scribes">划线</button>
31
+ </div>
32
+
33
+ <!-- ===== 边框样式 ===== -->
34
+ <div class="style-li">
35
+ <div class="style-title">边框</div>
36
+ <div class="style-list">
37
+ <div class="node-left">
38
+ <span>颜色</span>
39
+ <div @click="colorTap($event,'borderColor')" class="backColor"></div>
40
+ </div>
41
+ <div class="node-left padd">
42
+ <span>宽度</span>
43
+ <Select :options="optionLine" v-model:value="styleValue"></Select>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- ===== 背景样式 ===== -->
49
+ <div class="style-li">
50
+ <div class="style-title">背景</div>
51
+ <div class="style-list">
52
+ <div class="node-left">
53
+ <span>颜色</span>
54
+ <div @click="colorTap($event,'color')" class="backColor"></div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- ===== 形状样式 ===== -->
60
+ <div class="style-li">
61
+ <div class="style-title">形状</div>
62
+ <div class="style-list">
63
+ <div class="node-left padd">
64
+ <span>形状</span>
65
+ <Select :options="optionShape" v-model:value="shapeValue"></Select>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- 颜色选择浮层 -->
71
+ <div class="back-col" :style="{top: top + 'px'}" v-if="colorShow">
72
+ <Coloring @submit="submit" @empty="empty" @rgba="rgba" :color="color"></Coloring>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </template>
77
+
78
+ <script>
79
+ import Select from '../../select/src/select.vue'
80
+ import Coloring from '../../coloring/src/coloring.vue'
81
+
82
+ /**
83
+ * graph-tool 图谱节点样式编辑组件
84
+ *
85
+ * 功能:
86
+ * 1. 编辑节点文字样式(字号、行高、颜色、粗体、斜体、划线)
87
+ * 2. 编辑节点边框样式(颜色、宽度)
88
+ * 3. 编辑节点背景颜色
89
+ * 4. 编辑节点形状(圆形/矩形)
90
+ *
91
+ * @emits update:open 同步弹窗开关状态
92
+ * @emits select 样式变更事件,payload: { type, value }
93
+ */
94
+ export default {
95
+ components: { Coloring, Select },
96
+
97
+ data() {
98
+ return {
99
+ // ===== 弹窗状态 =====
100
+ /** 弹窗显示/隐藏 */
101
+ show: false,
102
+ /** 颜色选择器浮层坐标 top */
103
+ top: 0,
104
+
105
+ // ===== 字号选项 =====
106
+ optionSize: [
107
+ {label: "12", value: '12'},
108
+ {label: "14", value: '14'},
109
+ {label: "16", value: '16'},
110
+ {label: "18", value: '18'},
111
+ {label: "20", value: '20'},
112
+ {label: "24", value: '24'},
113
+ {label: "26", value: '26'},
114
+ {label: "28", value: '28'},
115
+ {label: "30", value: '30'}
116
+ ],
117
+ /** 当前字号 */
118
+ sizeValue: '14',
119
+
120
+ // ===== 行高选项 =====
121
+ optionRow: [
122
+ {label: "1", value: '1'},
123
+ {label: "1.5", value: '1.5'},
124
+ {label: "2", value: '2'},
125
+ {label: "2.5", value: '2.5'},
126
+ {label: "3", value: '3'}
127
+ ],
128
+ /** 当前行高 */
129
+ rowValue: '1.5',
130
+
131
+ // ===== 文字样式 =====
132
+ /** 当前颜色(共用:字体色/边框色/背景色) */
133
+ color: 'rgba(84,150,136,1)',
134
+ /** 粗体激活状态 */
135
+ boldAct: false,
136
+ /** 斜体激活状态 */
137
+ italicAct: false,
138
+ /** 下划线激活状态 */
139
+ scribeAct: false,
140
+
141
+ // ===== 边框宽度选项 =====
142
+ optionLine: [
143
+ {label: "0", value: '0'},
144
+ {label: "1", value: '1'},
145
+ {label: "2", value: '2'},
146
+ {label: "3", value: '3'},
147
+ {label: "4", value: '4'},
148
+ {label: "5", value: '5'},
149
+ {label: "6", value: '6'},
150
+ {label: "7", value: '7'},
151
+ {label: "8", value: '8'},
152
+ {label: "9", value: '9'},
153
+ {label: "10", value: '10'},
154
+ ],
155
+ /** 当前边框宽度 */
156
+ styleValue: '0',
157
+
158
+ // ===== 形状选项 =====
159
+ optionShape: [
160
+ {label: "圆形", value: '0'},
161
+ {label: "矩形", value: '1'},
162
+ ],
163
+ /** 当前节点形状(0: 圆形, 1: 矩形) */
164
+ shapeValue: '0',
165
+
166
+ // ===== 颜色选择器 =====
167
+ /** 颜色选择器显示状态 */
168
+ colorShow: false,
169
+ /** 当前颜色编辑类型:fontColor / borderColor / color */
170
+ type: ''
171
+ }
172
+ },
173
+
174
+ props: {
175
+ /** 弹窗开关(v-model 双向绑定) */
176
+ open: {
177
+ type: Boolean,
178
+ default: false
179
+ },
180
+ /** 当前选中节点的样式数据 */
181
+ node: {
182
+ type: Object,
183
+ default: () => {}
184
+ }
185
+ },
186
+
187
+ emits: ['update:open', 'select'],
188
+
189
+ watch: {
190
+ /**
191
+ * 监听弹窗打开/关闭
192
+ * 打开时从 node 中回填当前样式值;关闭时隐藏颜色选择器
193
+ */
194
+ open: {
195
+ handler(val) {
196
+ this.show = val
197
+ if (!val) {
198
+ this.colorShow = false
199
+ return
200
+ }
201
+ // 从节点数据中回填当前样式
202
+ this.sizeValue = this.node.fontSize || '14'
203
+ this.rowValue = this.node.lineHeight || '1.5'
204
+ this.boldAct = this.node.fontWeight === 'bold'
205
+ this.italicAct = this.node.fontStyle === 'italic'
206
+ this.scribeAct = this.node.textDecoration === 'underline'
207
+ this.styleValue = this.node.borderWidth || '0'
208
+ this.shapeValue = this.node.nodeShape || '0'
209
+ // 根据当前颜色编辑类型初始化颜色值
210
+ if (this.type === 'fontColor') this.color = this.node.fontColor || 'rgba(255, 255, 255, 1)'
211
+ if (this.type === 'borderColor') this.color = this.node.borderColor || 'rgba(64, 158, 255, 1)'
212
+ if (this.type === 'color') this.color = this.node.color || 'rgba(64, 158, 255, 1)'
213
+ },
214
+ deep: true
215
+ },
216
+
217
+ /**
218
+ * 同步显示状态到父组件
219
+ */
220
+ show: {
221
+ handler(val) {
222
+ this.$emit('update:open', val)
223
+ },
224
+ deep: true
225
+ },
226
+
227
+ /** 字号变化 */
228
+ sizeValue: {
229
+ handler(val) {
230
+ this.$emit('select', {type: 'fontSize', value: val})
231
+ },
232
+ deep: true
233
+ },
234
+
235
+ /** 行高变化 */
236
+ rowValue: {
237
+ handler(val) {
238
+ this.$emit('select', {type: 'lineHeight', value: val})
239
+ },
240
+ deep: true
241
+ },
242
+
243
+ /** 边框宽度变化 */
244
+ styleValue: {
245
+ handler(val) {
246
+ this.$emit('select', {type: 'borderWidth', value: val})
247
+ },
248
+ deep: true
249
+ },
250
+
251
+ /** 形状变化 */
252
+ shapeValue: {
253
+ handler(val) {
254
+ this.$emit('select', {type: 'nodeShape', value: val})
255
+ },
256
+ deep: true
257
+ }
258
+ },
259
+
260
+ methods: {
261
+ /**
262
+ * 切换粗体并发出事件
263
+ */
264
+ bolds() {
265
+ this.boldAct = !this.boldAct
266
+ this.$emit('select', {type: 'fontWeight', value: (!this.boldAct) ? 'normal' : 'bold'})
267
+ },
268
+
269
+ /**
270
+ * 切换斜体并发出事件
271
+ */
272
+ italics() {
273
+ this.italicAct = !this.italicAct
274
+ this.$emit('select', {type: 'fontStyle', value: (!this.italicAct) ? 'normal' : 'italic'})
275
+ },
276
+
277
+ /**
278
+ * 切换下划线并发出事件
279
+ */
280
+ scribes() {
281
+ this.scribeAct = !this.scribeAct
282
+ this.$emit('select', {type: 'textDecoration', value: (!this.scribeAct) ? 'none' : 'underline'})
283
+ },
284
+
285
+ /**
286
+ * 关闭弹窗
287
+ */
288
+ nodeStyle() {
289
+ this.show = false
290
+ },
291
+
292
+ /**
293
+ * 打开颜色选择器
294
+ *
295
+ * @param {Event} e - click 事件对象
296
+ * @param {string} v - 颜色类型:fontColor / borderColor / color
297
+ */
298
+ colorTap(e, v) {
299
+ this.type = v
300
+ this.colorShow = true
301
+ },
302
+
303
+ /**
304
+ * 颜色选择回调(实时预览)
305
+ *
306
+ * @param {{rgba: string}} v - 颜色选择器输出的 rgba 值
307
+ */
308
+ rgba(v) {
309
+ this.$emit('select', {type: this.type, value: v.rgba})
310
+ },
311
+
312
+ /**
313
+ * 颜色选择确认,关闭选择器
314
+ */
315
+ submit() {
316
+ this.colorShow = false
317
+ },
318
+
319
+ /**
320
+ * 颜色选择取消,关闭选择器
321
+ */
322
+ empty() {
323
+ this.colorShow = false
324
+ }
325
+ }
326
+ }
327
+ </script>
328
+
329
+ <style scoped src="./graph.css"></style>
package/src/index.js CHANGED
@@ -37,6 +37,7 @@ import seek from './components/seek/index';
37
37
  import sketchpad from './components/sketchpad/index';
38
38
  import chat from './components/chat/index';
39
39
  import variableBox from './components/variableBox/index';
40
+ import graph from './components/graph/index';
40
41
 
41
42
  const components = [
42
43
  Calendar,
@@ -74,7 +75,8 @@ const components = [
74
75
  sketchpad,
75
76
  chat,
76
77
  variableBox,
77
- dragPopup2
78
+ dragPopup2,
79
+ graph
78
80
  ];
79
81
 
80
82
  function install(app) {
@@ -87,7 +89,7 @@ function install(app) {
87
89
  }
88
90
 
89
91
  export default {
90
- version: '1.35.640',
92
+ version: '1.36.640',
91
93
  install,
92
94
  Calendar,
93
95
  Message,
@@ -127,6 +129,7 @@ export default {
127
129
  sketchpad,
128
130
  chat,
129
131
  variableBox,
130
- dragPopup2
132
+ dragPopup2,
133
+ graph
131
134
  };
132
135