@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,828 @@
1
+ <template>
2
+ <div class="matrix">
3
+
4
+ <!-- 矩阵表格:左侧行树(文件夹+方框 / 橙圆C+三角),表头仅 1-7,虚线层级 -->
5
+ <div class="matrix-table-wrap">
6
+ <table class="matrix-table">
7
+ <thead>
8
+ <tr v-for="(row, rowIndex) in columnHeaderRows" :key="rowIndex">
9
+ <!-- 表头角格:显示 relationModelIcon + relationModelName -->
10
+ <th
11
+ v-if="rowIndex === 0"
12
+ class="matrix-corner"
13
+ :rowspan="columnHeaderRows.length"
14
+ >
15
+ <div class="matrix-corner__header">
16
+ <template v-if="relationModelIcon">
17
+ <img
18
+ v-if="getIcon('childIcons', relationModelIcon)"
19
+ :src="getIcon('childIcons', relationModelIcon)"
20
+ alt=""
21
+ class="matrix-corner__icon-img"
22
+ />
23
+ <span v-else class="matrix-corner__icon">{{ relationModelIcon }}</span>
24
+ </template>
25
+ <span v-if="relationModelName" class="matrix-corner__name">{{ relationModelName }}</span>
26
+ </div>
27
+ </th>
28
+ <th
29
+ v-for="cell in row"
30
+ :key="cell.node.id"
31
+ :colspan="cell.colspan"
32
+ :rowspan="cell.rowspan"
33
+ class="matrix-col-header"
34
+ :class="{
35
+ 'matrix-col-header--parent': hasColumnChildren(cell.node) && cell.rowspan <= 1,
36
+ 'matrix-col-header--leaf': !hasColumnChildren(cell.node) && cell.rowspan <= 1,
37
+ 'matrix-col-header--spanning': cell.rowspan > 1,
38
+ 'matrix-col-header--selected': selectedColumnId === cell.node.id,
39
+ }"
40
+ @click="onColumnHeaderClick(cell.node)"
41
+ >
42
+ <div class="matrix-col-header__inner">
43
+ <!-- 有子节点:绿色文件夹 + 方框 −/+,可点击折叠 -->
44
+ <template v-if="hasColumnChildren(cell.node)">
45
+ <span
46
+ class="col-expand col-expand--square"
47
+ @click.stop="toggleColumn(cell.node)"
48
+ >
49
+ {{ columnExpandedIds.has(cell.node.id) ? '−' : '+' }}
50
+ </span>
51
+ <template v-if="cell.node.icon && getIcon('childIcons', cell.node.icon)">
52
+ <img :src="getIcon('childIcons', cell.node.icon)" alt="" class="col-node-icon col-node-icon--img" />
53
+ </template>
54
+ <span v-else class="col-node-icon col-node-icon--folder">
55
+ {{ columnExpandedIds.has(cell.node.id) ? '📂' : '📁' }}
56
+ </span>
57
+ <span class="col-node-label">{{ cell.node.nodeName }}</span>
58
+ </template>
59
+ <!-- 叶子:图标或黄圆 C -->
60
+ <template v-else>
61
+ <template v-if="cell.node.icon && getIcon('childIcons', cell.node.icon)">
62
+ <img :src="getIcon('childIcons', cell.node.icon)" alt="" class="col-node-icon col-node-icon--img" />
63
+ </template>
64
+ <span v-else class="col-node-icon col-node-icon--circle">C</span>
65
+ <span class="col-node-label">{{ cell.node.nodeName }}</span>
66
+ </template>
67
+ </div>
68
+ </th>
69
+ </tr>
70
+ </thead>
71
+ <tbody>
72
+ <tr
73
+ v-for="(item, rowIdx) in flatRowsWithConnectors"
74
+ :key="item.node.id"
75
+ :class="{ 'matrix-row--selected': selectedRowId === item.node.id }"
76
+ >
77
+ <td
78
+ class="matrix-row-header"
79
+ :class="{ 'matrix-row-header--selected': selectedRowId === item.node.id }"
80
+ @click="onRowHeaderClick(item.node)"
81
+ >
82
+ <div
83
+ class="row-tree-cell"
84
+ :style="{ paddingLeft: 8 + item.depth * 20 + 'px' }"
85
+ >
86
+ <!-- 树形连接线 -->
87
+ <template v-if="item.depth > 0">
88
+ <!-- 祖先层级的垂直虚线(depth 0 .. depth-2) -->
89
+ <template v-for="d in item.depth - 1" :key="'vl-' + d">
90
+ <div
91
+ v-if="item.connectors[d - 1]"
92
+ class="row-conn-vline"
93
+ :style="{ left: (16 + (d - 1) * 20) + 'px' }"
94
+ />
95
+ </template>
96
+ <!-- 父级分支连接:垂直线 + 水平线 -->
97
+ <div
98
+ class="row-conn-branch"
99
+ :class="{ 'row-conn-branch--last': !item.connectors[item.depth - 1] }"
100
+ :style="{ left: (16 + (item.depth - 1) * 20) + 'px', width: '12px' }"
101
+ />
102
+ </template>
103
+ <!-- 有子节点:方框展开 + 图标/文件夹 + 文字 -->
104
+ <template v-if="hasRowChildren(item.node)">
105
+ <span class="row-expand row-expand--square" @click.stop="toggleRow(item.node)">
106
+ {{ rowExpandedIds.has(item.node.id) ? '−' : '+' }}
107
+ </span>
108
+ <template v-if="item.node.icon && getIcon('childIcons', item.node.icon)">
109
+ <img :src="getIcon('childIcons', item.node.icon)" alt="" class="row-node-icon row-node-icon--img" />
110
+ </template>
111
+ <span v-else class="row-node-icon row-node-icon--folder">
112
+ {{ rowExpandedIds.has(item.node.id) ? '📂' : '📁' }}
113
+ </span>
114
+ <span class="row-node-label">{{ item.node.nodeName }}</span>
115
+ </template>
116
+ <!-- 叶子:占位 + 图标或橙圆 C + 文字 -->
117
+ <template v-else>
118
+ <span class="row-expand-placeholder" />
119
+ <template v-if="item.node.icon && getIcon('childIcons', item.node.icon)">
120
+ <img :src="getIcon('childIcons', item.node.icon)" alt="" class="row-node-icon row-node-icon--img" />
121
+ </template>
122
+ <span v-else class="row-node-icon row-node-icon--circle">C</span>
123
+ <span class="row-node-label">{{ item.node.nodeName }}</span>
124
+ </template>
125
+ </div>
126
+ </td>
127
+ <td
128
+ v-for="col in columnLeaves"
129
+ :key="col.id"
130
+ class="matrix-cell"
131
+ :class="{
132
+ 'matrix-cell--has-value': isLeafRow(item.node) && !!getCell(item.node.id, col.id) && shouldShowCell(item.node.id, col.id),
133
+ 'matrix-cell--disabled': getCell(item.node.id, col.id)?.operable === false,
134
+ 'matrix-cell--selected': selectedRowId === item.node.id && selectedColumnId === col.id,
135
+ }"
136
+ @click="onCellClick(item.node, col)"
137
+ @dblclick="onCellDblClick(item.node, col, getCell(item.node.id, col.id))"
138
+ >
139
+ <template v-if="isLeafRow(item.node) && getCell(item.node.id, col.id)">
140
+ <span v-if="hasDependencyRelation(getCell(item.node.id, col.id)!) && showDependency" class="cell-dependency" :class="{ 'cell-arrow--up': getCellArrowType(getCell(item.node.id, col.id)!) === 'ARROW_UP', 'cell-arrow--down': getCellArrowType(getCell(item.node.id, col.id)!) === 'ARROW_DOWN' }">{{ getCellDependencySymbol(getCell(item.node.id, col.id)!) }}</span>
141
+ <span v-else-if="!hasDependencyRelation(getCell(item.node.id, col.id)!) && getCell(item.node.id, col.id)?.value" class="cell-value">{{ getCell(item.node.id, col.id)?.value }}</span>
142
+ </template>
143
+ </td>
144
+ </tr>
145
+ </tbody>
146
+ </table>
147
+ </div>
148
+ </div>
149
+ </template>
150
+
151
+ <script setup lang="ts">
152
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
153
+ import type { TreeNode, ColumnHeaderCell } from '../../types'
154
+ import type { Matrix } from '../../types'
155
+ import {
156
+ flattenRowTree,
157
+ getVisibleColumnNodesWithRoot,
158
+ getAllColumnNodeIds,
159
+ getColumnHeaderRows,
160
+ getAllExpandableIds,
161
+ } from './index'
162
+ import type { ColHeaderCell } from './index'
163
+ import { eventBus } from '../../store/eventBus'
164
+ import { getIcon } from '../../utils/iconLoader'
165
+
166
+ /** 当前选中的行 id、列 id(点击行头/列头/单元格时更新并通过 eventBus 发出) */
167
+ const selectedRowId = ref<string | null>(null)
168
+ const selectedColumnId = ref<string | null>(null)
169
+
170
+
171
+ const rowScope = ref('Model')
172
+ const columnScope = ref('Strategy')
173
+ const findText = ref('')
174
+ const showDependency = ref(true)
175
+
176
+ const emptyMatrix: Matrix = { rowTree: [], columnTree: [], crossData: [], relationModelIcon: '', relationModelName: '' }
177
+ const matrixData = ref<Matrix>({ ...emptyMatrix })
178
+
179
+ const relationModelIcon = computed(() => matrixData.value.relationModelIcon ?? '')
180
+ const relationModelName = computed(() => matrixData.value.relationModelName ?? '')
181
+
182
+ /** 数据更新后默认全部展开 */
183
+ function applyDataAndExpand(payload: Matrix): void {
184
+ matrixData.value = payload
185
+ rowExpandedIds.value = getAllExpandableIds(payload.rowTree)
186
+ columnExpandedIds.value = getAllExpandableIds(payload.columnTree)
187
+ }
188
+
189
+ /** 主项目通过 eventBus "matrix-data-change" 下发数据,payload 为 Matrix 类型 */
190
+ function onMatrixDataChange(payload: Matrix): void {
191
+ if (payload && Array.isArray(payload.rowTree) && Array.isArray(payload.columnTree) && Array.isArray(payload.crossData)) {
192
+ applyDataAndExpand(payload)
193
+ }
194
+ }
195
+
196
+ const handleMatrixDataChange = (payload: Matrix) => onMatrixDataChange(payload)
197
+ onMounted(() => {
198
+ eventBus.on('matrix-data-change', handleMatrixDataChange)
199
+ })
200
+ onUnmounted(() => {
201
+ eventBus.off('matrix-data-change', handleMatrixDataChange)
202
+ })
203
+ const rowTreeNodes = computed(() => matrixData.value.rowTree)
204
+ const columnTreeNodes = computed(() => matrixData.value.columnTree)
205
+ const matrixNodes = computed(() => matrixData.value.crossData)
206
+
207
+ // 展开状态:收到数据后默认全部展开,由 applyDataAndExpand 填充
208
+ const rowExpandedIds = ref(new Set<string>())
209
+ const columnExpandedIds = ref(new Set<string>())
210
+
211
+ const flattenedRows = computed(() =>
212
+ flattenRowTree(rowTreeNodes.value, rowExpandedIds.value)
213
+ )
214
+ /** 列数据列:单根展开时第一列为根(战略层),后续列为叶子(能力),与行侧对齐 */
215
+ const treeColumnNodes = computed(() =>
216
+ getVisibleColumnNodesWithRoot(columnTreeNodes.value, columnExpandedIds.value)
217
+ )
218
+ /** 列树中所有节点 id(含未展开的子节点),用于区分「树内但被折叠」与「树外需补列」 */
219
+ const allColumnNodeIds = computed(() =>
220
+ getAllColumnNodeIds(columnTreeNodes.value)
221
+ )
222
+ /** 仅补列树里根本不存在的列 id;折叠后的子列(如能力)属于列树,不补列 */
223
+ const extraColumnIdsFromCrossData = computed(() => {
224
+ const inTree = allColumnNodeIds.value
225
+ const ids: string[] = []
226
+ for (const n of matrixNodes.value) {
227
+ const id = getColumnId(n)
228
+ if (id && !inTree.has(id) && !ids.includes(id)) ids.push(id)
229
+ }
230
+ return ids
231
+ })
232
+ const extraColumnLeaves = computed((): TreeNode[] =>
233
+ extraColumnIdsFromCrossData.value.map((id) => ({
234
+ id,
235
+ nodeName: '',
236
+ modelName: '',
237
+ nodeType: 'item' as const,
238
+ isLeaf: true,
239
+ isChart: false,
240
+ })) as TreeNode[]
241
+ )
242
+ /** 实际数据列 = 可见叶子 + 补列 */
243
+ const columnLeaves = computed(() => [
244
+ ...treeColumnNodes.value,
245
+ ...extraColumnLeaves.value,
246
+ ])
247
+ /** 列头多行:getColumnHeaderRows 自动处理任意层级树 */
248
+ const columnHeaderRowsBase = computed(() =>
249
+ getColumnHeaderRows(columnTreeNodes.value, columnExpandedIds.value)
250
+ )
251
+ const columnHeaderRows = computed((): ColHeaderCell[][] => {
252
+ const base = columnHeaderRowsBase.value
253
+ const extra = extraColumnLeaves.value
254
+
255
+ const nodes = columnTreeNodes.value
256
+ const rootIncluded = nodes.length === 1
257
+ && !!(nodes[0].children?.length)
258
+ && columnExpandedIds.value.has(nodes[0].id)
259
+
260
+ let result = base
261
+
262
+ if (rootIncluded && result.length > 1) {
263
+ const rootNode = nodes[0]
264
+ result = result.map((row, ri) => {
265
+ if (ri === 0) {
266
+ return row.map((cell, ci) =>
267
+ ci === 0 ? { ...cell, colspan: cell.colspan + 1 } : cell
268
+ )
269
+ } else if (ri === 1) {
270
+ const rootCell: ColHeaderCell = {
271
+ node: rootNode,
272
+ colspan: 1,
273
+ rowspan: result.length - 1,
274
+ }
275
+ return [rootCell, ...row]
276
+ }
277
+ return row
278
+ })
279
+ }
280
+
281
+ if (extra.length > 0) {
282
+ const extraCells = extra.map((node) => ({ node, colspan: 1, rowspan: 1 }))
283
+ result = result.map((row, ri) => {
284
+ if (ri < result.length - 1) {
285
+ return row.map((cell, ci) =>
286
+ ci === 0 ? { ...cell, colspan: cell.colspan + extra.length } : cell
287
+ )
288
+ }
289
+ return [...row, ...extraCells]
290
+ })
291
+ }
292
+
293
+ return result
294
+ })
295
+
296
+ /** 行树连接器:计算每行在每个祖先深度是否还有后续兄弟(用于画 │ 或 └) */
297
+ const flatRowsWithConnectors = computed(() => {
298
+ const items = flattenedRows.value
299
+ return items.map((item, i) => {
300
+ const { depth } = item
301
+ const connectors: boolean[] = []
302
+ for (let d = 0; d < depth; d++) {
303
+ let hasNext = false
304
+ for (let j = i + 1; j < items.length; j++) {
305
+ if (items[j].depth <= d) break
306
+ if (items[j].depth === d + 1) { hasNext = true; break }
307
+ }
308
+ connectors.push(hasNext)
309
+ }
310
+ return { ...item, connectors }
311
+ })
312
+ })
313
+
314
+ function getRowId(n: ColumnHeaderCell): string {
315
+ return n.rowNodeId ?? ''
316
+ }
317
+ function getColumnId(n: ColumnHeaderCell): string {
318
+ return n.columnNodeId ?? ''
319
+ }
320
+
321
+ const cellMap = computed(() => {
322
+ const map: Record<string, Record<string, ColumnHeaderCell>> = {}
323
+ for (const n of matrixNodes.value) {
324
+ const rowId = getRowId(n)
325
+ const colId = getColumnId(n)
326
+ if (!rowId || !colId) continue
327
+ if (!map[rowId]) map[rowId] = {}
328
+ map[rowId][colId] = n
329
+ }
330
+ return map
331
+ })
332
+
333
+ function getCell(rowId: string, colId: string): ColumnHeaderCell | undefined {
334
+ return cellMap.value[rowId]?.[colId]
335
+ }
336
+
337
+ function hasDependencyRelation(cell: ColumnHeaderCell): boolean {
338
+ return cell.relations?.some((r) => r.type === 'Dependency' || r.relationType === 'Dependency') ?? false
339
+ }
340
+
341
+ /** 取单元格依赖关系的箭头类型:ARROW_UP → 斜向上 ↗,ARROW_DOWN → 斜向下 ↘ */
342
+ function getCellArrowType(cell: ColumnHeaderCell): 'ARROW_UP' | 'ARROW_DOWN' | null {
343
+ const r = cell.relations?.[0]
344
+ if (!r || (r.relationType !== 'ARROW_UP' && r.relationType !== 'ARROW_DOWN')) return null
345
+ return r.relationType as 'ARROW_UP' | 'ARROW_DOWN'
346
+ }
347
+
348
+ /** 依赖单元格显示符号:ARROW_UP ↗,ARROW_DOWN ↘,否则 √ */
349
+ function getCellDependencySymbol(cell: ColumnHeaderCell): string {
350
+ const arrow = getCellArrowType(cell)
351
+ if (arrow === 'ARROW_UP') return '↗'
352
+ if (arrow === 'ARROW_DOWN') return '↘'
353
+ return '√'
354
+ }
355
+
356
+ function shouldShowCell(rowId: string, colId: string): boolean {
357
+ const cell = getCell(rowId, colId)
358
+ if (!cell) return false
359
+ if (hasDependencyRelation(cell)) return showDependency.value
360
+ return !!cell.value
361
+ }
362
+
363
+ function hasRowChildren(node: TreeNode): boolean {
364
+ return !!(node.children && node.children.length > 0)
365
+ }
366
+
367
+ function isLeafRow(node: TreeNode): boolean {
368
+ return !!node.isLeaf || !node.children?.length
369
+ }
370
+
371
+ function toggleRow(node: TreeNode): void {
372
+ const next = new Set(rowExpandedIds.value)
373
+ if (next.has(node.id)) next.delete(node.id)
374
+ else next.add(node.id)
375
+ rowExpandedIds.value = next
376
+ }
377
+
378
+ function hasColumnChildren(node: TreeNode): boolean {
379
+ return !!(node.children && node.children.length > 0)
380
+ }
381
+
382
+ function toggleColumn(node: TreeNode): void {
383
+ const next = new Set(columnExpandedIds.value)
384
+ if (next.has(node.id)) next.delete(node.id)
385
+ else next.add(node.id)
386
+ columnExpandedIds.value = next
387
+ }
388
+
389
+ /** 点击行头:选中行,eventBus row-selected */
390
+ function onRowHeaderClick(node: TreeNode): void {
391
+ selectedRowId.value = node.id
392
+ selectedColumnId.value = null
393
+ eventBus.emit('row-selected', node.id)
394
+ }
395
+
396
+ /** 点击列头:选中列,eventBus column-selected(角格不绑此逻辑,由上面 v-if 排除) */
397
+ function onColumnHeaderClick(node: TreeNode): void {
398
+ selectedRowId.value = null
399
+ selectedColumnId.value = node.id
400
+ eventBus.emit('column-selected', node.id)
401
+ }
402
+
403
+ /** 点击单元格:选中该行+该列,eventBus cell-selected(relationId 取自 cell.relations) */
404
+ function onCellClick(rowNode: TreeNode, colNode: TreeNode): void {
405
+ const cell = getCell(rowNode.id, colNode.id)
406
+ const relationId = cell?.relations?.[0]?.relationId ?? ''
407
+ selectedRowId.value = rowNode.id
408
+ selectedColumnId.value = colNode.id
409
+ eventBus.emit('cell-selected', { rowId: rowNode.id, columnId: colNode.id, relationId })
410
+ }
411
+
412
+ function onCellDblClick(rowNode: TreeNode, colNode: TreeNode, cell: ColumnHeaderCell | undefined): void {
413
+ if (!cell || cell.operable === false) return
414
+ const hasRelations = cell.relations && cell.relations.length > 0
415
+ eventBus.emit('cell-db-click', {
416
+ columnNodeId: getColumnId(cell),
417
+ rowNodeId: getRowId(cell),
418
+ relationId: cell.relations?.[0]?.relationId ?? '',
419
+ operateType: hasRelations ? 'OPERATE_TYPE_DELETE' : 'OPERATE_TYPE_ADD',
420
+ })
421
+ }
422
+
423
+ /** 供主项目通过 ref 调用:直接设置矩阵数据 */
424
+ function setMatrixData(data: Matrix): void {
425
+ if (data?.rowTree && data?.columnTree && data?.crossData) {
426
+ applyDataAndExpand(data)
427
+ }
428
+ }
429
+
430
+ defineExpose({ setMatrixData })
431
+ </script>
432
+
433
+ <style scoped lang="scss">
434
+ .matrix {
435
+ width: 100%;
436
+ height: 100%;
437
+ display: flex;
438
+ flex-direction: column;
439
+ overflow: hidden;
440
+ background: #fff;
441
+ }
442
+
443
+ .matrix-header {
444
+ padding: 8px 12px;
445
+ border-bottom: 1px solid #e8e8e8;
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 8px;
449
+
450
+ .matrix-header-left {
451
+ display: flex;
452
+ align-items: center;
453
+ gap: 6px;
454
+ }
455
+
456
+ .matrix-header-title {
457
+ font-weight: 600;
458
+ font-size: 14px;
459
+ }
460
+
461
+ .matrix-header-icon {
462
+ display: inline-flex;
463
+ align-items: center;
464
+ cursor: pointer;
465
+ color: #606266;
466
+ font-size: 14px;
467
+ padding: 2px 4px;
468
+ border: 1px solid #dcdfe6;
469
+ border-radius: 4px;
470
+
471
+ .icon-grids {
472
+ margin-right: 2px;
473
+ }
474
+ .icon-arrow {
475
+ font-size: 10px;
476
+ opacity: 0.8;
477
+ }
478
+ }
479
+ }
480
+
481
+ .matrix-toolbar {
482
+ display: flex;
483
+ align-items: center;
484
+ justify-content: space-between;
485
+ padding: 8px 12px;
486
+ border-bottom: 1px solid #e8e8e8;
487
+ flex-shrink: 0;
488
+ }
489
+
490
+ .matrix-criteria {
491
+ display: flex;
492
+ align-items: center;
493
+ gap: 8px;
494
+
495
+ .criteria-label {
496
+ font-size: 12px;
497
+ color: #666;
498
+ }
499
+
500
+ .criteria-select {
501
+ width: 120px;
502
+ }
503
+
504
+ .find-input {
505
+ width: 160px;
506
+ }
507
+
508
+ .criteria-checkbox {
509
+ font-size: 12px;
510
+ color: #606266;
511
+ }
512
+ }
513
+
514
+ .matrix-table-wrap {
515
+ flex: 1;
516
+ overflow: auto;
517
+ padding: 12px;
518
+ }
519
+
520
+ .matrix-table {
521
+ border-collapse: collapse;
522
+ min-width: 100%;
523
+ font-size: 12px;
524
+
525
+ th,
526
+ td {
527
+ border: none;
528
+ padding: 6px 8px;
529
+ vertical-align: middle;
530
+ }
531
+
532
+ .matrix-corner {
533
+ min-width: 140px;
534
+ width: 140px;
535
+ background: #fff;
536
+ border-right: 1px solid #d0d0d0;
537
+ border-bottom: 1px solid #d0d0d0;
538
+ vertical-align: middle;
539
+
540
+ .matrix-corner__header {
541
+ display: inline-flex;
542
+ align-items: center;
543
+ gap: 6px;
544
+ padding: 4px 0;
545
+ }
546
+ .matrix-corner__icon-img {
547
+ width: 16px;
548
+ height: 16px;
549
+ display: block;
550
+ flex-shrink: 0;
551
+ }
552
+ .matrix-corner__icon {
553
+ width: 18px;
554
+ height: 18px;
555
+ border-radius: 50%;
556
+ background: #f0c000;
557
+ color: #333;
558
+ display: inline-flex;
559
+ align-items: center;
560
+ justify-content: center;
561
+ font-size: 10px;
562
+ font-weight: bold;
563
+ flex-shrink: 0;
564
+ }
565
+ .matrix-corner__name {
566
+ font-size: 12px;
567
+ color: #303133;
568
+ overflow: hidden;
569
+ text-overflow: ellipsis;
570
+ white-space: nowrap;
571
+ }
572
+ }
573
+
574
+ .matrix-col-header {
575
+ min-width: 48px;
576
+ text-align: left;
577
+ background: #fff;
578
+ font-weight: 500;
579
+ font-size: 12px;
580
+ color: #303133;
581
+ vertical-align: middle;
582
+ cursor: pointer;
583
+
584
+ .matrix-col-header__inner {
585
+ display: inline-flex;
586
+ align-items: center;
587
+ gap: 6px;
588
+ min-height: 28px;
589
+ }
590
+
591
+ &.matrix-col-header--parent {
592
+ border-bottom: 1px dashed #999;
593
+ .col-expand--square {
594
+ width: 16px;
595
+ height: 16px;
596
+ border: 1px solid #c0c4cc;
597
+ border-radius: 2px;
598
+ display: inline-flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ cursor: pointer;
602
+ font-size: 12px;
603
+ color: #606266;
604
+ flex-shrink: 0;
605
+ }
606
+ .col-node-icon--folder {
607
+ flex-shrink: 0;
608
+ font-size: 14px;
609
+ }
610
+ }
611
+
612
+ .col-node-icon--img {
613
+ width: 16px;
614
+ height: 16px;
615
+ display: block;
616
+ flex-shrink: 0;
617
+ }
618
+ .col-node-icon--circle {
619
+ width: 18px;
620
+ height: 18px;
621
+ border-radius: 50%;
622
+ background: #f0c000;
623
+ color: #333;
624
+ display: inline-flex;
625
+ align-items: center;
626
+ justify-content: center;
627
+ font-size: 10px;
628
+ font-weight: bold;
629
+ flex-shrink: 0;
630
+ }
631
+
632
+ &.matrix-col-header--leaf {
633
+ text-align: center;
634
+ position: relative;
635
+ padding-top: 14px;
636
+ border-bottom: 1px solid #d0d0d0;
637
+ /* 第二列等叶子列:列名(能力)在单元格正中间,对齐红框位置 */
638
+ .matrix-col-header__inner {
639
+ justify-content: center;
640
+ margin: 0 auto;
641
+ }
642
+ &::before {
643
+ content: '';
644
+ position: absolute;
645
+ left: 50%;
646
+ top: 0;
647
+ height: 14px;
648
+ width: 0;
649
+ border-left: 1px dashed #333;
650
+ }
651
+ }
652
+
653
+ &.matrix-col-header--spanning {
654
+ vertical-align: bottom;
655
+ text-align: center;
656
+ position: relative;
657
+ padding-top: 14px;
658
+ border-bottom: 1px solid #d0d0d0;
659
+ &::before {
660
+ content: '';
661
+ position: absolute;
662
+ left: 50%;
663
+ top: 0;
664
+ height: calc(100% - 30px);
665
+ width: 0;
666
+ border-left: 1px dashed #333;
667
+ }
668
+ }
669
+
670
+ .col-node-label {
671
+ color: #303133;
672
+ }
673
+ }
674
+
675
+ .matrix-row-header {
676
+ min-width: 200px;
677
+ background: #fff;
678
+ padding: 0px 10px;
679
+ vertical-align: middle;
680
+ border-right: 1px solid #d0d0d0;
681
+ cursor: pointer;
682
+
683
+ &.matrix-row-header--selected {
684
+ background: #e6f7ff;
685
+ }
686
+ }
687
+
688
+ tr.matrix-row--selected .matrix-cell {
689
+ background: #e6f7ff;
690
+ }
691
+
692
+ .matrix-col-header.matrix-col-header--selected {
693
+ background: #e6f7ff;
694
+ }
695
+
696
+ .matrix-cell.matrix-cell--selected {
697
+ background: #bae7ff;
698
+ }
699
+
700
+ .matrix-row-header .row-tree-cell {
701
+ position: relative;
702
+ display: flex;
703
+ align-items: center;
704
+ min-height: 28px;
705
+ font-size: 12px;
706
+ gap: 6px;
707
+ }
708
+
709
+ /* 祖先层级的垂直虚线(贯穿整行高度) */
710
+ .row-conn-vline {
711
+ position: absolute;
712
+ top: 0;
713
+ bottom: 0;
714
+ width: 0;
715
+ border-left: 1px dashed #999;
716
+ pointer-events: none;
717
+ }
718
+
719
+ /* 父级分支连接:垂直线 + 水平线 */
720
+ .row-conn-branch {
721
+ position: absolute;
722
+ top: 0;
723
+ bottom: 0;
724
+ pointer-events: none;
725
+ &::before {
726
+ content: '';
727
+ position: absolute;
728
+ left: 0;
729
+ top: 0;
730
+ bottom: 0;
731
+ border-left: 1px dashed #999;
732
+ }
733
+ &::after {
734
+ content: '';
735
+ position: absolute;
736
+ left: 0;
737
+ top: 50%;
738
+ right: 0;
739
+ border-top: 1px dashed #999;
740
+ }
741
+ &.row-conn-branch--last::before {
742
+ bottom: 50%;
743
+ }
744
+ }
745
+ .row-expand {
746
+ width: 16px;
747
+ height: 16px;
748
+ flex-shrink: 0;
749
+ display: inline-flex;
750
+ align-items: center;
751
+ justify-content: center;
752
+ cursor: pointer;
753
+ line-height: 1;
754
+ &.row-expand--square {
755
+ border: 1px solid #c0c4cc;
756
+ border-radius: 2px;
757
+ font-size: 12px;
758
+ color: #606266;
759
+ background: #fff;
760
+ }
761
+ }
762
+ .row-expand-placeholder {
763
+ width: 16px;
764
+ flex-shrink: 0;
765
+ }
766
+ .row-node-icon--folder {
767
+ flex-shrink: 0;
768
+ font-size: 14px;
769
+ }
770
+ .row-node-icon--img {
771
+ width: 16px;
772
+ height: 16px;
773
+ display: block;
774
+ flex-shrink: 0;
775
+ }
776
+ .row-node-icon--circle {
777
+ width: 18px;
778
+ height: 18px;
779
+ border-radius: 50%;
780
+ background: #e6a23c;
781
+ color: #fff;
782
+ display: inline-flex;
783
+ align-items: center;
784
+ justify-content: center;
785
+ font-size: 10px;
786
+ font-weight: bold;
787
+ flex-shrink: 0;
788
+ }
789
+ .row-node-label {
790
+ color: #303133;
791
+ }
792
+
793
+ .matrix-cell {
794
+ min-width: 48px;
795
+ text-align: center;
796
+ color: #303133;
797
+ background: #fff;
798
+ border: 1px solid #d0d0d0;
799
+ cursor: pointer;
800
+
801
+ &.matrix-cell--has-value {
802
+ background: #e6f7ff;
803
+ }
804
+
805
+ &.matrix-cell--disabled {
806
+ background: #f5f5f5;
807
+ color: #c0c4cc;
808
+ cursor: not-allowed;
809
+ }
810
+
811
+ .cell-dependency {
812
+ color: #1890ff;
813
+ font-weight: bold;
814
+ font-size: 14px;
815
+
816
+ &.cell-arrow--up,
817
+ &.cell-arrow--down {
818
+ font-size: 16px;
819
+ font-weight: bold;
820
+ }
821
+ }
822
+
823
+ .cell-value {
824
+ font-weight: 500;
825
+ }
826
+ }
827
+ }
828
+ </style>