@mx-sose-front/mx-sose-graph 1.1.8 → 1.1.9

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.
Files changed (52) hide show
  1. package/dist/assets/edgeWorker-b57ca007.js +2 -0
  2. package/dist/assets/edgeWorker-b57ca007.js.map +1 -0
  3. package/dist/index.d.ts +633 -30
  4. package/dist/index.esm.js +8728 -4734
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.umd.js +1 -1
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/style.css +1 -1
  9. package/package.json +1 -1
  10. package/src/components/Common/Tree.vue +451 -0
  11. package/src/components/Common/index.ts +2 -0
  12. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +1 -2
  13. package/src/components/Edge/Edge.vue +172 -169
  14. package/src/components/Gantt/Gantt.vue +1544 -0
  15. package/src/components/GanttContextMenu/GanttContextMenu.vue +304 -0
  16. package/src/components/InteractionLayer.vue +343 -147
  17. package/src/components/Matrix/Matrix.vue +828 -0
  18. package/src/components/Matrix/index.ts +168 -0
  19. package/src/components/Shape/ConceptualRole.vue +2 -34
  20. package/src/components/Table/Table.vue +970 -0
  21. package/src/constants/edgeShapeKeys.ts +8 -5
  22. package/src/constants/index.ts +259 -45
  23. package/src/hooks/index.ts +2 -0
  24. package/src/hooks/useChartRowSelection.ts +456 -0
  25. package/src/hooks/useResize.ts +2 -2
  26. package/src/hooks/useVirtualScroll.ts +258 -0
  27. package/src/index.ts +1 -1
  28. package/src/render/shape-renderer.ts +62 -2
  29. package/src/statics/icons/childIcons//345/221/275/344/273/244@3x.png +0 -0
  30. package/src/statics/icons/childIcons//346/210/230/347/225/245/346/246/202/345/277/265/350/241/250@3x.png +0 -0
  31. package/src/statics/icons/childIcons//346/216/247/345/210/266@3x.png +0 -0
  32. package/src/statics/icons/createMenu/down.png +0 -0
  33. package/src/statics/icons/createMenu/remove.png +0 -0
  34. package/src/statics/icons/createMenu/up.png +0 -0
  35. package/src/store/graphStore.ts +217 -44
  36. package/src/types/index.ts +86 -4
  37. package/src/utils/batchAutoExpand.ts +9 -10
  38. package/src/utils/containers.ts +72 -17
  39. package/src/utils/contextMenuUtils.ts +7 -7
  40. package/src/utils/dateUtils.ts +160 -0
  41. package/src/utils/diagram.ts +10 -8
  42. package/src/utils/drag.ts +6 -5
  43. package/src/utils/edgeUtils.ts +344 -427
  44. package/src/utils/edgeWorker.ts +471 -0
  45. package/src/utils/hittest.ts +37 -38
  46. package/src/utils/index.ts +3 -0
  47. package/src/utils/keyboardUtils.ts +5 -5
  48. package/src/utils/packageOutline.ts +96 -0
  49. package/src/utils/rafThrottle.ts +162 -0
  50. package/src/utils/workerManager.ts +335 -0
  51. package/src/view/graph.vue +47 -33
  52. /package/src/statics/icons/childIcons//346/210/230/347/225/{245@3x.png" → 245/345/261/202@3x.png"} +0 -0
@@ -0,0 +1,970 @@
1
+ <template>
2
+ <div class="gantt-container" @click="onContainerClick">
3
+ <!-- 左侧表格 -->
4
+ <div class="gantt-left" :class="{ 'with-search-bar': showSearchBar }" ref="tableContainerRef" @scroll="() => syncVirtualViewportState()">
5
+ <table class="gantt-table" :style="{ width: tableWidth + 'px' }">
6
+ <!-- 用 colgroup 统一控制整列宽度 -->
7
+ <colgroup>
8
+ <col v-for="col in displayColumns" :key="col.configId"
9
+ :style="{ width: getColumnWidth(col.configId) + 'px', }" />
10
+ </colgroup>
11
+ <thead>
12
+ <tr class="header-row-2">
13
+ <th v-for="col in displayColumns" :key="col.id" class="col-date">
14
+ <div class="th-content">
15
+ <div v-if="col.configId == 'index'" style="text-align: center;">{{ col.name }}</div>
16
+ <div v-else class="th-label">{{ col.name }}</div>
17
+ <div v-if="col.configId !== 'index'" class="col-resizer"
18
+ @mousedown="startColumnResize(col.configId, $event)"></div>
19
+ </div>
20
+ </th>
21
+ </tr>
22
+ </thead>
23
+
24
+ <tbody>
25
+ <!-- 上方占位空间 -->
26
+ <tr v-if="virtualTopSpacerHeight > 0" class="table-spacer-row" aria-hidden="true">
27
+ <td :colspan="visibleColumnSpan">
28
+ <div class="table-spacer" :style="{ height: virtualTopSpacerHeight + 'px' }"></div>
29
+ </td>
30
+ </tr>
31
+
32
+ <!-- 虚拟行 -->
33
+ <tr v-for="row in virtualRows" :key="row.item.id" class="gantt-row"
34
+ :class="{ 'row-selected': selectedRowIds.has(row.item.id) }"
35
+ @click.stop="selectRow(row.item.id, $event)" @contextmenu="handleContextMenu(row.item, $event)">
36
+ <td v-for="col in displayColumns" :key="col.configId" class="col-date"
37
+ :title="getDisplayCellValue(row.item, col)"
38
+ @dblclick.stop="col.configId !== 'index' && openCellEditor(row.item, col)">
39
+ <!-- 编辑态:根据列的 valueType 渲染不同编辑控件(Element Plus) -->
40
+ <template v-if="isCellEditing(row.item.id, col.configId)">
41
+ <!-- 布尔:开关 -->
42
+ <el-switch
43
+ v-if="editingTextCell?.editorType === 'Boolean'"
44
+ v-model="editingTextCell!.value"
45
+ size="small"
46
+ class="cell-switch"
47
+ @click.stop
48
+ @change="saveTextEdit(row.item, col.configId)"
49
+ />
50
+ <!-- 日期:日期时间选择器 -->
51
+ <el-date-picker
52
+ v-else-if="editingTextCell?.editorType === 'Date'"
53
+ :ref="setEditingInputRef"
54
+ v-model="editingTextCell!.value"
55
+ type="datetime"
56
+ size="small"
57
+ class="cell-date"
58
+ format="YYYY-MM-DD HH:mm:ss"
59
+ value-format="YYYY-MM-DD HH:mm:ss"
60
+ :teleported="false"
61
+ :clearable="true"
62
+ :editable="false"
63
+ @click.stop
64
+ @change="saveTextEdit(row.item, col.configId)"
65
+ @blur="saveTextEdit(row.item, col.configId)"
66
+ @keydown.esc.prevent="closeCellEditor"
67
+ />
68
+ <!-- 枚举:下拉选择,选项来自后端 literals -->
69
+ <el-select
70
+ v-else-if="editingTextCell?.editorType === 'Enumeration' && col.literals && col.literals.length"
71
+ v-model="editingTextCell!.value"
72
+ size="small"
73
+ class="cell-select"
74
+ @click.stop
75
+ @change="saveTextEdit(row.item, col.configId)"
76
+ @blur="saveTextEdit(row.item, col.configId)"
77
+ >
78
+ <el-option
79
+ v-for="opt in col.literals"
80
+ :key="opt.value"
81
+ :label="opt.key"
82
+ :value="opt.value"
83
+ />
84
+ </el-select>
85
+ <!-- 富文本:先用 textarea(属性面板是弹窗富文本,这里做轻量版) -->
86
+ <el-input
87
+ v-else-if="editingTextCell?.editorType === 'Html'"
88
+ :ref="setEditingInputRef"
89
+ v-model="editingTextCell!.value"
90
+ type="textarea"
91
+ autosize
92
+ size="small"
93
+ class="cell-textarea"
94
+ @click.stop
95
+ @blur="saveTextEdit(row.item, col.configId)"
96
+ @keydown.esc.prevent="closeCellEditor"
97
+ />
98
+ <!-- 其他(String、Integer 等):文本/数字输入框 -->
99
+ <el-input
100
+ v-else
101
+ :ref="setEditingInputRef"
102
+ v-model="editingTextCell!.value"
103
+ :type="editingTextCell?.editorType === 'Integer' ? 'number' : 'text'"
104
+ size="small"
105
+ class="cell-input"
106
+ @click.stop
107
+ @blur="saveTextEdit(row.item, col.configId)"
108
+ @keydown.enter.prevent="saveTextEdit(row.item, col.configId)"
109
+ @keydown.esc.prevent="closeCellEditor"
110
+ />
111
+ </template>
112
+
113
+ <!-- 显示态 -->
114
+ <template v-else>
115
+ <div v-if="col.configId === 'index'" class="item-index" style="text-align: center;">
116
+ {{ row.index + 1 }}
117
+ </div>
118
+ <div v-else class="item-name">
119
+ <div v-if="getDisplayCellIcon(row.item, col)" class="expand-icon">
120
+ <img :src="getIcon('childIcons', getDisplayCellIcon(row.item, col))" alt=""
121
+ style="width: 16px; height: 16px; display: block;" />
122
+ </div>
123
+ <span class="cell-text">{{ getDisplayCellValue(row.item, col) }}</span>
124
+ </div>
125
+ </template>
126
+ </td>
127
+ </tr>
128
+
129
+ <!-- 下方占位空间 -->
130
+ <tr v-if="virtualBottomSpacerHeight > 0" class="table-spacer-row" aria-hidden="true">
131
+ <td :colspan="visibleColumnSpan">
132
+ <div class="table-spacer" :style="{ height: virtualBottomSpacerHeight + 'px' }"></div>
133
+ </td>
134
+ </tr>
135
+ </tbody>
136
+ </table>
137
+ </div>
138
+ <!-- 右键菜单 -->
139
+ <GanttContextMenu v-model:visible="showContextMenu" :position="contextMenuPosition" :can-move-up="canMoveUp"
140
+ :can-move-down="canMoveDown" @property-config="handlePropertyConfig" @tree-highlight="handleTreeHighlight"
141
+ @move-up="handleMoveUpFromMenu" @move-down="handleMoveDownFromMenu" @delete="handleDeleteFromMenu"
142
+ @remove="handleRemoveFromMenu" />
143
+ </div>
144
+ </template>
145
+
146
+ <script setup lang="ts">
147
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
148
+ import type { Shape, GanttData, GanttColumn } from '../../types'
149
+ import { eventBus } from '../../store/eventBus'
150
+ import GanttContextMenu from '../GanttContextMenu/GanttContextMenu.vue'
151
+ import { getIcon } from '../../utils/iconLoader'
152
+ import { useChartRowSelection, useVirtualScroll } from '../../hooks'
153
+ /**
154
+ * 组件入参
155
+ */
156
+ interface Props {
157
+ shape: Shape
158
+ }
159
+
160
+ const props = defineProps<Props>()
161
+
162
+ /**
163
+ * 前端内置固定列:序号列
164
+ */
165
+ const indexColumn = {
166
+ configId: 'index',
167
+ name: '序号',
168
+ isShow: true,
169
+ isReadonly: false,
170
+ }
171
+ /**
172
+ * 当前组件内部使用的行数据类型
173
+ * 允许动态字段,便于适配后端返回的任意列
174
+ */
175
+ // 列宽度状态(动态调整,使用 configId 作为 key)
176
+ const columnWidths = ref<Record<string, number>>({})
177
+ /**
178
+ * 搜索输入框 ref
179
+ */
180
+ const searchInputRef = ref<HTMLInputElement | null>(null)
181
+
182
+ /**
183
+ * 表格容器 ref(用于虚拟滚动)
184
+ */
185
+ const tableContainerRef = ref<HTMLDivElement | null>(null)
186
+
187
+ /**
188
+ * 当前编辑中的输入框 ref
189
+ */
190
+ const editingInputRef = ref<HTMLInputElement | null>(null)
191
+
192
+ /**
193
+ * 搜索相关状态
194
+ */
195
+ const showSearchBar = ref(false)
196
+ const searchKeyword = ref('')
197
+ const searchMatchedIds = ref<string[]>([])
198
+ const currentMatchIndex = ref(-1)
199
+ /**
200
+ * 默认列配置
201
+ * 没有后端列配置时,使用这组默认列
202
+ */
203
+ const defaultColumns = ref<GanttColumn[]>([])
204
+
205
+ // 甘特图数据:使用后端数据
206
+ type GanttItem = GanttData & { hasChildren: boolean }
207
+ const ganttItems = ref<GanttItem[]>([])
208
+ /**
209
+ * 除序号列之外的第一列 configId(displayColumns[1])
210
+ */
211
+ const firstDataColId = computed(() => {
212
+ return displayColumns.value[1]?.configId || ''
213
+ })
214
+ /**
215
+ * 实际表格数据
216
+ * 优先使用后端数据,没有则使用 mock 数据
217
+ */
218
+ // const ganttItems = ref<GanttItem[]>([...mockData.value])
219
+
220
+ // 列配置:优先使用后端数据
221
+ const columnConfig = ref<any[]>([...defaultColumns.value])
222
+
223
+ /**
224
+ * 当前可见列
225
+ */
226
+ const visibleColumns = computed(() => {
227
+ return columnConfig.value.filter(col => col.isShow)
228
+ })
229
+ /**
230
+ * 最终渲染列
231
+ * 序号列固定在最左侧
232
+ */
233
+ const displayColumns = computed(() => {
234
+ return [indexColumn, ...visibleColumns.value]
235
+ })
236
+
237
+ /**
238
+ * 过滤后的数据(用于搜索)
239
+ * 如果没有搜索关键字,返回所有数据
240
+ */
241
+ const filteredGanttItems = computed(() => {
242
+ const keyword = searchKeyword.value.trim().toLowerCase()
243
+ if (!keyword) {
244
+ return ganttItems.value
245
+ }
246
+
247
+ if (searchMatchedIds.value.length === 0) {
248
+ return []
249
+ }
250
+
251
+ return ganttItems.value.filter(item => searchMatchedIds.value.includes(item.id))
252
+ })
253
+
254
+ // ========== 虚拟滚动 ==========
255
+ const TABLE_HEADER_HEIGHT = 28
256
+ const TABLE_ROW_HEIGHT = 32
257
+ const TABLE_VIRTUAL_OVERSCAN = 8
258
+
259
+ const {
260
+ virtualRows,
261
+ virtualTopSpacerHeight,
262
+ virtualBottomSpacerHeight,
263
+ virtualStartIndex,
264
+ virtualEndIndex,
265
+ syncVirtualViewportState,
266
+ visibleColumnSpan,
267
+ } = useVirtualScroll(filteredGanttItems, {
268
+ itemHeight: TABLE_ROW_HEIGHT,
269
+ headerHeight: TABLE_HEADER_HEIGHT,
270
+ overscan: TABLE_VIRTUAL_OVERSCAN,
271
+ containerRef: tableContainerRef,
272
+ columnCount: computed(() => displayColumns.value.length),
273
+ })
274
+
275
+ /**
276
+ * 监听虚拟索引变化,自动关闭不可见行的编辑状态
277
+ */
278
+ watch([virtualStartIndex, virtualEndIndex], () => {
279
+ if (!editingTextCell.value) return
280
+
281
+ const renderedIds = new Set(virtualRows.value.map(({ item }) => item.id))
282
+ if (!renderedIds.has(editingTextCell.value.id)) {
283
+ closeCellEditor()
284
+ }
285
+ })
286
+
287
+ /**
288
+ * 行交互状态
289
+ * 统一复用表格/甘特图共享的行选择与右键菜单逻辑
290
+ */
291
+ const {
292
+ canMoveDown,
293
+ canMoveUp,
294
+ clearSelection,
295
+ contextMenuPosition,
296
+ deleteRows,
297
+ handleContextMenu,
298
+ handleDeleteFromMenu,
299
+ handleMoveDownFromMenu,
300
+ handleMoveUpFromMenu,
301
+ handlePropertyConfig,
302
+ handleRemoveFromMenu,
303
+ handleTreeHighlight,
304
+ moveDown,
305
+ moveUp,
306
+ onRemoveSuccess,
307
+ removeRows,
308
+ selectLastRow,
309
+ selectRow,
310
+ selectedRowIds,
311
+ showContextMenu,
312
+ } = useChartRowSelection<GanttItem>({
313
+ items: ganttItems,
314
+ rowSelectedEvent: 'table-row-selected',
315
+ onClearSelection: () => {
316
+ closeCellEditor()
317
+ },
318
+ onDeleteRows: () => {
319
+ closeCellEditor()
320
+ },
321
+ onAfterRemoveSuccess: () => {
322
+ closeCellEditor()
323
+ },
324
+ })
325
+
326
+ /**
327
+ * 当前编辑中的单元格
328
+ * 增加 editorType / column 信息,便于根据 valueType 渲染不同编辑控件
329
+ */
330
+ const editingTextCell = ref<{
331
+ id: string
332
+ field: string
333
+ value: any
334
+ editorType?: string
335
+ column?: any
336
+ } | null>(null)
337
+
338
+ // 根据 configId 获取默认列宽
339
+ function getDefaultColumnWidth(configId: string): number {
340
+ if (configId === 'index') {
341
+ return 50
342
+ }
343
+ // 其他列默认中等宽度
344
+ return 400
345
+ }
346
+ function getColumnWidth(configId: string): number {
347
+ if (configId === 'index') {
348
+ return 50
349
+ }
350
+ return columnWidths.value[configId] || getDefaultColumnWidth(configId)
351
+ }
352
+
353
+ /**
354
+ * 点击空白区域,清空选中并关闭编辑态
355
+ */
356
+ function onContainerClick() {
357
+ clearSelection()
358
+ }
359
+ /**
360
+ * 新增行
361
+ */
362
+ function addRow() {
363
+ eventBus.emit('table-add-row')
364
+ }
365
+ /**
366
+ * 搜索结果显示文本
367
+ */
368
+ const searchResultText = computed(() => {
369
+ if (!searchKeyword.value.trim()) return ''
370
+ if (searchMatchedIds.value.length === 0) return '0 of 0'
371
+ return `${currentMatchIndex.value + 1} of ${searchMatchedIds.value.length}`
372
+ })
373
+
374
+ /**
375
+ * 上一条 / 下一条按钮状态
376
+ */
377
+ const canGoPrev = computed(() => {
378
+ return searchMatchedIds.value.length > 0 && currentMatchIndex.value > 0
379
+ })
380
+
381
+ const canGoNext = computed(() => {
382
+ return (
383
+ searchMatchedIds.value.length > 0 &&
384
+ currentMatchIndex.value < searchMatchedIds.value.length - 1
385
+ )
386
+ })
387
+
388
+ /**
389
+ * 执行搜索
390
+ * 搜索范围为当前可见列
391
+ */
392
+ function runSearch() {
393
+ const keyword = searchKeyword.value.trim().toLowerCase()
394
+ if (!keyword) {
395
+ searchMatchedIds.value = []
396
+ currentMatchIndex.value = -1
397
+ return
398
+ }
399
+
400
+ // 模糊匹配:搜索所有可见列的值
401
+ const matched = ganttItems.value.filter(item => {
402
+ return visibleColumns.value.some(col => {
403
+ const value = getDisplayCellValue(item, col)
404
+ return value.toLowerCase().includes(keyword)
405
+ })
406
+ })
407
+
408
+ searchMatchedIds.value = matched.map(item => item.id)
409
+ currentMatchIndex.value = searchMatchedIds.value.length > 0 ? 0 : -1
410
+
411
+ // 自动选中第一个匹配项
412
+ if (currentMatchIndex.value >= 0) {
413
+ selectRow(searchMatchedIds.value[0])
414
+ }
415
+ }
416
+
417
+ /**
418
+ * 搜索输入事件
419
+ */
420
+ function onSearchInput() {
421
+ runSearch()
422
+ }
423
+
424
+ /**
425
+ * 跳转上一条匹配
426
+ */
427
+ function gotoPrevMatch() {
428
+ if (!canGoPrev.value) return
429
+ currentMatchIndex.value--
430
+ selectRow(searchMatchedIds.value[currentMatchIndex.value])
431
+ }
432
+
433
+ /**
434
+ * 跳转下一条匹配
435
+ */
436
+ function gotoNextMatch() {
437
+ if (!canGoNext.value) return
438
+ currentMatchIndex.value++
439
+ selectRow(searchMatchedIds.value[currentMatchIndex.value])
440
+ }
441
+
442
+ /**
443
+ * 打开搜索栏
444
+ */
445
+ function openSearch() {
446
+ showSearchBar.value = true
447
+ nextTick(() => {
448
+ searchInputRef.value?.focus()
449
+ })
450
+ }
451
+
452
+ /**
453
+ * 关闭搜索栏
454
+ */
455
+ function closeSearch() {
456
+ showSearchBar.value = false
457
+ searchKeyword.value = ''
458
+ searchMatchedIds.value = []
459
+ currentMatchIndex.value = -1
460
+ }
461
+
462
+ /**
463
+ * 工具栏指令监听
464
+ */
465
+ function onGanttToolbarAction(action: string) {
466
+ const actionMap: Record<string, () => void> = {
467
+ chartNewModel: addRow,
468
+ chartDelete: deleteRows,
469
+ chartRemove: removeRows,
470
+ chartMoveUp: moveUp,
471
+ chartMoveDown: moveDown,
472
+ chartSearch: openSearch,
473
+ }
474
+ const fn = actionMap[action]
475
+ if (!fn) return
476
+ fn()
477
+ }
478
+ /**
479
+ * 判断当前单元格是否正在编辑
480
+ */
481
+ function isCellEditing(rowId: string, field: string) {
482
+ return editingTextCell.value?.id === rowId && editingTextCell.value?.field === field
483
+ }
484
+
485
+ /**
486
+ * 设置当前编辑输入框 ref
487
+ */
488
+ function setEditingInputRef(el: HTMLInputElement | null | any) {
489
+ editingInputRef.value = el
490
+ }
491
+
492
+ /**
493
+ * 打开单元格编辑
494
+ * 根据列的 valueType 选择合适的编辑控件
495
+ */
496
+ function openCellEditor(item: GanttItem, col: any) {
497
+ if (col.isReadonly === true) return
498
+
499
+ // 原始值直接从数据行中读取,避免使用展示值(例如枚举的 label)
500
+ const rawValue = (item as any)[col.configId]
501
+
502
+ // 复杂类型:交给宿主应用使用“属性面板同款编辑器”处理
503
+ // 这样 graph 包保持轻量,不强依赖业务侧的弹窗/选择器实现
504
+ // 复杂值类型不走表格内联编辑,统一交给宿主侧弹出属性面板同款编辑器。
505
+ // Transfer 在业务上对应“选择用例”穿梭框,双击单元格时也要走这条链路。
506
+ const complexTypes = new Set([
507
+ 'Element',
508
+ 'Element_List',
509
+ 'Relations_End',
510
+ 'ValueSpecification',
511
+ 'NoString',
512
+ 'Transfer',
513
+ ])
514
+ if (complexTypes.has(col.valueType)) {
515
+ eventBus.emit('table-cell-open-editor', {
516
+ item,
517
+ column: col,
518
+ configId: col.configId,
519
+ valueType: col.valueType,
520
+ value: rawValue ?? null,
521
+ })
522
+ return
523
+ }
524
+
525
+ editingTextCell.value = {
526
+ id: item.id,
527
+ field: col.configId,
528
+ value: rawValue ?? '',
529
+ editorType: col.valueType,
530
+ column: col,
531
+ }
532
+ nextTick(() => {
533
+ const inst = editingInputRef.value as any
534
+ inst?.focus?.()
535
+ inst?.select?.()
536
+ })
537
+ }
538
+
539
+ function getRawCellValue(item: any, configId: string) {
540
+ const v = item?.[configId]
541
+ return v
542
+ }
543
+
544
+ /**
545
+ * 保存单元格值
546
+ */
547
+ function saveTextEdit(item: GanttItem, field: string) {
548
+
549
+ if (!editingTextCell.value || editingTextCell.value.value === undefined) {
550
+ editingTextCell.value = null
551
+ return
552
+ }
553
+
554
+ const oldValue = getRawCellValue(item as any, field)
555
+ const newValue = editingTextCell.value.value
556
+
557
+ // 值未改变,直接关闭编辑
558
+ if (oldValue === newValue) {
559
+ editingTextCell.value = null
560
+ return
561
+ }
562
+
563
+ // 发送编辑事件到父组件(与 Gantt 使用相同的事件名)
564
+ eventBus.emit('gantt-cell-edit', {
565
+ item,
566
+ configId: field,
567
+ oldValue,
568
+ newValue
569
+ })
570
+
571
+ // 暂时关闭编辑状态(等待接口返回)
572
+ editingTextCell.value = null
573
+ }
574
+
575
+ /**
576
+ * 关闭单元格编辑
577
+ */
578
+ function closeCellEditor() {
579
+ editingTextCell.value = null
580
+ editingInputRef.value = null
581
+ }
582
+ //监听获取后端列配置
583
+ watch(() => props.shape.attributeColumns, (columns) => {
584
+ if (columns && columns.length > 0) {
585
+ columnConfig.value = columns
586
+ // 初始化列宽(如果还没有设置)
587
+ columns.forEach((col: any) => {
588
+ if (col.configId !== 'index' && !columnWidths.value[col.configId]) {
589
+ columnWidths.value[col.configId] = getDefaultColumnWidth(col.configId)
590
+ }
591
+ })
592
+ // 同步到工具栏菜单
593
+ eventBus.emit('chart-columns-from-backend', columns)
594
+ } else {
595
+ // 没有后端数据时使用默认配置
596
+ columnConfig.value = [...defaultColumns.value]
597
+ }
598
+ }, { immediate: true })
599
+ // 统一提取对象/对象数组类型单元格的展示文本和图标,避免 Element_List 展示成 [object Object]。
600
+ function getCellDisplayMeta(value: any): { text: string; icon: string } {
601
+ if (Array.isArray(value)) {
602
+ const objectItems = value.filter((item) => item && typeof item === 'object')
603
+ const text = objectItems
604
+ .map((item: any) => item.nodeName || item.modelName || '')
605
+ .filter(Boolean)
606
+ .join('、')
607
+
608
+ return {
609
+ text: text || String(value ?? ''),
610
+ icon: objectItems.find((item: any) => item.icon)?.icon || '',
611
+ }
612
+ }
613
+
614
+ if (value && typeof value === 'object') {
615
+ return {
616
+ text: value.nodeName || value.modelName || String(value ?? ''),
617
+ icon: value.icon || '',
618
+ }
619
+ }
620
+
621
+ return {
622
+ text: String(value ?? ''),
623
+ icon: '',
624
+ }
625
+ }
626
+
627
+ // 根据列配置获取“展示值”(用于单元格展示 / title)
628
+ function getDisplayCellValue(item: GanttData & { hasChildren: boolean }, col: any): string {
629
+ const configId = col.configId
630
+ const value = (item as any)[configId]
631
+
632
+ if (col.valueType === 'Boolean') {
633
+ if (value === true) return '是'
634
+ if (value === false) return '否'
635
+ return ''
636
+ }
637
+
638
+ // 枚举类型:用 literals 中的 key 作为展示文案
639
+ if (col.valueType === 'Enumeration' && col.literals && Array.isArray(col.literals)) {
640
+ const matched = col.literals.find((opt: any) => opt.value === value)
641
+ return matched?.key ?? String(value ?? '')
642
+ }
643
+
644
+ return getCellDisplayMeta(value).text
645
+ }
646
+
647
+ // 获取单元格应展示的图标。
648
+ // 第一列仍然优先展示行本身图标,其他对象/对象数组列则展示值里的 icon。
649
+ function getDisplayCellIcon(item: GanttData & { hasChildren: boolean }, col: any): string {
650
+ if (col.configId === firstDataColId.value && item.icon) {
651
+ return item.icon
652
+ }
653
+
654
+ return getCellDisplayMeta((item as any)[col.configId]).icon
655
+ }
656
+ // 根据 configId 获取“原始值”(用于比较 old/new、搜索等)
657
+ function getCellValue(item: GanttData & { hasChildren: boolean }, configId: string): string {
658
+ return getCellDisplayMeta((item as any)[configId]).text
659
+ }
660
+ // 列宽调整
661
+ function startColumnResize(columnKey: string, e: MouseEvent) {
662
+ if (columnKey === 'index') return
663
+
664
+ e.stopPropagation()
665
+ e.preventDefault()
666
+
667
+ const startX = e.clientX
668
+ const startWidth = getColumnWidth(columnKey)
669
+
670
+ const onMove = (ev: MouseEvent) => {
671
+ const delta = ev.clientX - startX
672
+ const nextWidth = Math.max(200, startWidth + delta)
673
+ columnWidths.value[columnKey] = nextWidth
674
+ }
675
+
676
+ const onUp = () => {
677
+ document.removeEventListener('mousemove', onMove)
678
+ document.removeEventListener('mouseup', onUp)
679
+ document.body.style.cursor = ''
680
+ document.body.style.userSelect = ''
681
+ }
682
+
683
+ document.body.style.cursor = 'col-resize'
684
+ document.body.style.userSelect = 'none'
685
+ document.addEventListener('mousemove', onMove)
686
+ document.addEventListener('mouseup', onUp)
687
+ }
688
+ function handleCanvasColumnChange(columns: any[]) {
689
+ if (columns && columns.length > 0) {
690
+ columnConfig.value = columns
691
+
692
+ // 初始化列宽(如果还没有设置)
693
+ columns.forEach((col: any) => {
694
+ if (col.configId !== 'index' && !columnWidths.value[col.configId]) {
695
+ columnWidths.value[col.configId] = getDefaultColumnWidth(col.configId)
696
+ }
697
+ })
698
+
699
+ // 同步到工具栏菜单
700
+ // eventBus.emit('chart-columns-from-backend', columns)
701
+ }
702
+ }
703
+
704
+ // 监听后端数据变化
705
+ function handleCanvasDataChange(data: any[]) {
706
+ if (data && data.length > 0) {
707
+ ganttItems.value = data.map(d => ({ ...d, hasChildren: false })) as (GanttData & { hasChildren: boolean })[]
708
+ }
709
+ }
710
+
711
+ // 监听列配置变更(来自工具栏菜单)
712
+ function onColumnConfigChange(config: Array<any>) {
713
+ columnConfig.value = config
714
+ }
715
+ const tableWidth = computed(() => {
716
+ return displayColumns.value.reduce((total, col) => {
717
+ return total + getColumnWidth(col.configId)
718
+ }, 0)
719
+ })
720
+ /**
721
+ * 生命周期:注册事件
722
+ */
723
+ onMounted(() => {
724
+ eventBus.on('table-toolbar-action', onGanttToolbarAction)
725
+ eventBus.on('canvas-data-change', handleCanvasDataChange)
726
+ eventBus.on('chart-column-config', onColumnConfigChange)
727
+ eventBus.on('canvas-data-column', handleCanvasColumnChange)
728
+ //监听移除成功事件
729
+ eventBus.on('table-gantt-remove:success', onRemoveSuccess)
730
+ // 新增成功回执:直接选中最后一条
731
+ eventBus.on('table-gantt-add:success', selectLastRow)
732
+ })
733
+
734
+ /**
735
+ * 生命周期:卸载事件
736
+ */
737
+ onUnmounted(() => {
738
+ eventBus.off('table-toolbar-action', onGanttToolbarAction)
739
+ eventBus.off('canvas-data-change', handleCanvasDataChange)
740
+ eventBus.off('chart-column-config', onColumnConfigChange)
741
+ eventBus.off('canvas-data-column', handleCanvasColumnChange)
742
+ eventBus.off('table-gantt-remove:success', onRemoveSuccess)
743
+ eventBus.off('table-gantt-add:success', selectLastRow)
744
+ })
745
+ </script>
746
+
747
+ <style scoped lang="scss">
748
+ /* 甘特表格容器 */
749
+ .gantt-container {
750
+ --header-row-h: 28px;
751
+ --data-row-h: 32px;
752
+
753
+ position: relative;
754
+ display: flex;
755
+ width: 100%;
756
+ height: 100%;
757
+ border: 1px solid #dcdfe6;
758
+ background: #fff;
759
+ overflow: hidden;
760
+ font-size: 12px;
761
+ font-family: 'Source Han Sans SC', 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
762
+ }
763
+ /* 左侧表格区域 */
764
+ .gantt-left {
765
+ width: 100%;
766
+ overflow: auto;
767
+ }
768
+
769
+ .gantt-left.with-search-bar {
770
+ margin-top: 36px;
771
+ }
772
+
773
+ /* 虚拟滚动占位行 */
774
+ .table-spacer-row td {
775
+ height: 0;
776
+ padding: 0;
777
+ border: none !important;
778
+ }
779
+
780
+ .table-spacer {
781
+ width: 1px;
782
+ }
783
+
784
+ /* 表格 */
785
+ .gantt-table {
786
+ border-collapse: separate;
787
+ border-spacing: 0;
788
+ table-layout: fixed;
789
+ }
790
+
791
+ .th-content {
792
+ position: relative;
793
+ display: flex;
794
+ align-items: center;
795
+ width: 100%;
796
+ height: 100%;
797
+ min-width: 0;
798
+ padding: 0 8px;
799
+ box-sizing: border-box;
800
+ }
801
+
802
+ .gantt-table th,
803
+ .gantt-table td {
804
+ box-sizing: border-box;
805
+ padding: 0 8px;
806
+ border: none;
807
+ border-bottom: 1px solid #ebeef5;
808
+ border-right: 1px solid #ebeef5;
809
+ text-align: left;
810
+ vertical-align: middle;
811
+ white-space: nowrap;
812
+ overflow: hidden;
813
+ text-overflow: ellipsis;
814
+ }
815
+
816
+ .gantt-table th:first-child,
817
+ .gantt-table td:first-child {
818
+ border-left: 1px solid #ebeef5;
819
+ width: 50px;
820
+ min-width: 50px;
821
+ max-width: 50px;
822
+ }
823
+
824
+
825
+ /* 表头第二行 */
826
+ .header-row-2 th {
827
+ position: sticky;
828
+ z-index: 1;
829
+ height: var(--header-row-h);
830
+ background: #f5f7fa;
831
+ border-bottom-color: #dcdfe6;
832
+ font-weight: 500;
833
+ color: #606266;
834
+ }
835
+
836
+ /* 数据行 */
837
+ .gantt-table td {
838
+ height: var(--data-row-h);
839
+ }
840
+
841
+ /* 列样式 */
842
+ .col-name {
843
+ text-align: left;
844
+ }
845
+
846
+ .col-td {
847
+ text-align: center;
848
+ }
849
+
850
+ .col-index {
851
+ width: 60px !important;
852
+ }
853
+
854
+ .th-label {
855
+ font-weight: 600;
856
+ }
857
+ .cell-text {
858
+ flex: 1;
859
+ min-width: 0;
860
+ overflow: hidden;
861
+ text-overflow: ellipsis;
862
+ white-space: nowrap;
863
+ }
864
+ /* 展开图标 */
865
+ .expand-icon {
866
+ display: inline-block;
867
+ margin-right: 4px;
868
+ cursor: pointer;
869
+ user-select: none;
870
+ }
871
+
872
+ /* 名称文本 */
873
+ .item-name {
874
+ display: inline-block;
875
+ width: 100%;
876
+ overflow: hidden;
877
+ text-overflow: ellipsis;
878
+ vertical-align: middle;
879
+ display: flex;
880
+ align-items: center;
881
+ min-width: 0;
882
+ }
883
+
884
+ .expand-icon {
885
+ flex: 0 0 auto;
886
+ display: inline-flex;
887
+ align-items: center;
888
+ justify-content: center;
889
+ margin-right: 5px;
890
+ cursor: pointer;
891
+ user-select: none;
892
+ }
893
+
894
+ .item-index {
895
+ text-align: center;
896
+ }
897
+
898
+ /* 单元格编辑输入框(Element Plus 包一层外壳,尽量贴合现有样式) */
899
+ .cell-input {
900
+ width: 100%;
901
+ }
902
+
903
+ .cell-input :deep(.el-input__wrapper) {
904
+ padding: 0 6px;
905
+ height: 24px;
906
+ box-shadow: 0 0 0 1px #409eff inset;
907
+ border-radius: 3px;
908
+ }
909
+
910
+ .cell-select {
911
+ width: 100%;
912
+ }
913
+
914
+ .cell-select :deep(.el-input__wrapper) {
915
+ min-height: 24px;
916
+ height: 24px;
917
+ padding: 0 6px;
918
+ border-radius: 3px;
919
+ }
920
+
921
+ .cell-switch {
922
+ display: inline-flex;
923
+ align-items: center;
924
+ height: 24px;
925
+ }
926
+
927
+ .cell-date {
928
+ width: 100%;
929
+ }
930
+
931
+ .cell-date :deep(.el-input__wrapper) {
932
+ min-height: 24px;
933
+ height: 24px;
934
+ padding: 0 6px;
935
+ border-radius: 3px;
936
+ }
937
+
938
+ .cell-textarea {
939
+ width: 100%;
940
+ }
941
+
942
+ .cell-textarea :deep(.el-textarea__inner) {
943
+ font-size: 12px;
944
+ line-height: 18px;
945
+ padding: 4px 6px;
946
+ }
947
+
948
+ .col-resizer {
949
+ position: absolute;
950
+ right: -8px;
951
+ top: 0;
952
+ bottom: 0;
953
+ width: 5px;
954
+ cursor: col-resize;
955
+ user-select: none;
956
+ }
957
+
958
+ .col-resizer:hover {
959
+ background-color: rgba(64, 158, 255, 0.1);
960
+ }
961
+
962
+ /* 选中行高亮 */
963
+ .row-selected {
964
+ background-color: #ecf5ff !important;
965
+ }
966
+
967
+ .gantt-row.row-selected td {
968
+ background-color: #ecf5ff;
969
+ }
970
+ </style>