|
@@ -19,39 +19,15 @@
|
|
|
<span class="w-6 text-center">{{ walkSteps }}</span>
|
|
<span class="w-6 text-center">{{ walkSteps }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 配置模式切换 -->
|
|
|
|
|
- <div class="flex items-center gap-3">
|
|
|
|
|
- <span class="text-base-content/60 w-16">配置模式:</span>
|
|
|
|
|
- <label class="flex items-center gap-1 cursor-pointer">
|
|
|
|
|
- <input type="radio" v-model="configMode" value="global" class="radio radio-xs radio-primary" />
|
|
|
|
|
- <span>整体设置</span>
|
|
|
|
|
- </label>
|
|
|
|
|
- <label class="flex items-center gap-1 cursor-pointer">
|
|
|
|
|
- <input type="radio" v-model="configMode" value="step" class="radio radio-xs radio-primary" />
|
|
|
|
|
- <span>分步设置</span>
|
|
|
|
|
- </label>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 整体设置 -->
|
|
|
|
|
- <div v-if="configMode === 'global'" class="pl-4 space-y-2 border-l-2 border-primary/30">
|
|
|
|
|
- <div class="flex items-center gap-2 flex-wrap">
|
|
|
|
|
- <span class="text-base-content/60 w-14">边类型:</span>
|
|
|
|
|
- <label v-for="et in allEdgeTypes" :key="et" class="flex items-center gap-1 cursor-pointer">
|
|
|
|
|
- <input type="checkbox" v-model="globalEdgeTypes" :value="et" class="checkbox checkbox-xs" />
|
|
|
|
|
- <span :style="{ color: edgeColors[et] }">{{ et }}</span>
|
|
|
|
|
- </label>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="flex items-center gap-2">
|
|
|
|
|
- <span class="text-base-content/60 w-14">最小分:</span>
|
|
|
|
|
- <input type="range" :min="0" :max="1" :step="0.1" v-model.number="globalMinScore" class="range range-xs flex-1" />
|
|
|
|
|
- <span class="w-8 text-center">{{ globalMinScore.toFixed(1) }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
<!-- 分步设置 -->
|
|
<!-- 分步设置 -->
|
|
|
- <div v-else class="space-y-2">
|
|
|
|
|
|
|
+ <div class="space-y-2">
|
|
|
<div v-for="step in walkSteps" :key="step" class="pl-4 space-y-1 border-l-2 border-secondary/30">
|
|
<div v-for="step in walkSteps" :key="step" class="pl-4 space-y-1 border-l-2 border-secondary/30">
|
|
|
- <div class="font-medium text-secondary">第 {{ step }} 步</div>
|
|
|
|
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
|
|
+ <span class="font-medium text-secondary">第 {{ step }} 步</span>
|
|
|
|
|
+ <button @click="selectAllEdgeTypes(step-1)" class="btn btn-ghost btn-xs text-base-content/50">全选</button>
|
|
|
|
|
+ <button @click="clearEdgeTypes(step-1)" class="btn btn-ghost btn-xs text-base-content/50">清空</button>
|
|
|
|
|
+ <button @click="resetEdgeTypes(step-1)" class="btn btn-ghost btn-xs text-base-content/50">默认</button>
|
|
|
|
|
+ </div>
|
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
|
<span class="text-base-content/60 w-14">边类型:</span>
|
|
<span class="text-base-content/60 w-14">边类型:</span>
|
|
|
<label v-for="et in allEdgeTypes" :key="et" class="flex items-center gap-1 cursor-pointer">
|
|
<label v-for="et in allEdgeTypes" :key="et" class="flex items-center gap-1 cursor-pointer">
|
|
@@ -95,36 +71,67 @@ const dimColors = {
|
|
|
'关键点': '#9b59b6'
|
|
'关键点': '#9b59b6'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 边类型颜色
|
|
|
|
|
-const edgeColors = {
|
|
|
|
|
- '属于': '#9b59b6',
|
|
|
|
|
- '包含': '#3498db',
|
|
|
|
|
- '分类共现': '#2ecc71',
|
|
|
|
|
- '分类共现_点内': '#27ae60',
|
|
|
|
|
- '标签共现': '#f39c12'
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// 边类型颜色(动态分配)
|
|
|
|
|
+const edgeColorPalette = ['#9b59b6', '#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#1abc9c']
|
|
|
|
|
+const edgeColors = computed(() => {
|
|
|
|
|
+ const colors = {}
|
|
|
|
|
+ allEdgeTypes.value.forEach((et, i) => {
|
|
|
|
|
+ colors[et] = edgeColorPalette[i % edgeColorPalette.length]
|
|
|
|
|
+ })
|
|
|
|
|
+ return colors
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
-// 所有边类型
|
|
|
|
|
-const allEdgeTypes = ['属于', '包含', '标签共现', '分类共现', '分类共现_点内']
|
|
|
|
|
|
|
+// 从数据中动态获取所有边类型
|
|
|
|
|
+const allEdgeTypes = computed(() => {
|
|
|
|
|
+ const types = new Set()
|
|
|
|
|
+ const edges = store.graphData.edges || {}
|
|
|
|
|
+ for (const edge of Object.values(edges)) {
|
|
|
|
|
+ if (edge.type) types.add(edge.type)
|
|
|
|
|
+ }
|
|
|
|
|
+ return Array.from(types)
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
// 游走配置
|
|
// 游走配置
|
|
|
const showConfig = ref(false)
|
|
const showConfig = ref(false)
|
|
|
const walkSteps = ref(2)
|
|
const walkSteps = ref(2)
|
|
|
-const configMode = ref('global') // 'global' | 'step'
|
|
|
|
|
-
|
|
|
|
|
-// 整体设置
|
|
|
|
|
-const globalEdgeTypes = ref([...allEdgeTypes])
|
|
|
|
|
-const globalMinScore = ref(0)
|
|
|
|
|
|
|
|
|
|
// 分步设置(最多5步)
|
|
// 分步设置(最多5步)
|
|
|
|
|
+// 默认:第1步全选,第2步及以后只选"属于"
|
|
|
const stepConfigs = reactive([
|
|
const stepConfigs = reactive([
|
|
|
- { edgeTypes: [...allEdgeTypes], minScore: 0 },
|
|
|
|
|
- { edgeTypes: [...allEdgeTypes], minScore: 0 },
|
|
|
|
|
- { edgeTypes: [...allEdgeTypes], minScore: 0 },
|
|
|
|
|
- { edgeTypes: [...allEdgeTypes], minScore: 0 },
|
|
|
|
|
- { edgeTypes: [...allEdgeTypes], minScore: 0 }
|
|
|
|
|
|
|
+ { edgeTypes: [], minScore: 0 },
|
|
|
|
|
+ { edgeTypes: ['属于'], minScore: 0 },
|
|
|
|
|
+ { edgeTypes: ['属于'], minScore: 0 },
|
|
|
|
|
+ { edgeTypes: ['属于'], minScore: 0 },
|
|
|
|
|
+ { edgeTypes: ['属于'], minScore: 0 }
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
|
|
+// 监听边类型变化,初始化第1步为全选
|
|
|
|
|
+watch(allEdgeTypes, (types) => {
|
|
|
|
|
+ if (stepConfigs[0].edgeTypes.length === 0) {
|
|
|
|
|
+ stepConfigs[0].edgeTypes = [...types]
|
|
|
|
|
+ }
|
|
|
|
|
+}, { immediate: true })
|
|
|
|
|
+
|
|
|
|
|
+// 全选边类型
|
|
|
|
|
+function selectAllEdgeTypes(stepIndex) {
|
|
|
|
|
+ stepConfigs[stepIndex].edgeTypes = [...allEdgeTypes.value]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 清空边类型
|
|
|
|
|
+function clearEdgeTypes(stepIndex) {
|
|
|
|
|
+ stepConfigs[stepIndex].edgeTypes = []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 重置为默认边类型(第1步全选,第2步及以后只选"属于")
|
|
|
|
|
+function resetEdgeTypes(stepIndex) {
|
|
|
|
|
+ if (stepIndex === 0) {
|
|
|
|
|
+ stepConfigs[stepIndex].edgeTypes = [...allEdgeTypes.value]
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stepConfigs[stepIndex].edgeTypes = ['属于']
|
|
|
|
|
+ }
|
|
|
|
|
+ stepConfigs[stepIndex].minScore = 0
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 游走时记录的边
|
|
// 游走时记录的边
|
|
|
let walkedEdges = []
|
|
let walkedEdges = []
|
|
|
|
|
|
|
@@ -137,9 +144,7 @@ function executeWalk() {
|
|
|
walkedEdges = [] // 清空之前的边记录
|
|
walkedEdges = [] // 清空之前的边记录
|
|
|
|
|
|
|
|
for (let step = 0; step < walkSteps.value; step++) {
|
|
for (let step = 0; step < walkSteps.value; step++) {
|
|
|
- const config = configMode.value === 'global'
|
|
|
|
|
- ? { edgeTypes: globalEdgeTypes.value, minScore: globalMinScore.value }
|
|
|
|
|
- : stepConfigs[step]
|
|
|
|
|
|
|
+ const config = stepConfigs[step]
|
|
|
|
|
|
|
|
const nextFrontier = new Set()
|
|
const nextFrontier = new Set()
|
|
|
|
|
|
|
@@ -219,34 +224,27 @@ function renderGraph() {
|
|
|
|
|
|
|
|
if (!centerNode) return
|
|
if (!centerNode) return
|
|
|
|
|
|
|
|
- // 判断是否有游走高亮结果
|
|
|
|
|
- const hasWalkResult = store.highlightedNodeIds.size > 1
|
|
|
|
|
-
|
|
|
|
|
// 准备节点和边数据
|
|
// 准备节点和边数据
|
|
|
const nodes = []
|
|
const nodes = []
|
|
|
const links = []
|
|
const links = []
|
|
|
const nodeSet = new Set()
|
|
const nodeSet = new Set()
|
|
|
- const linkSet = new Set()
|
|
|
|
|
-
|
|
|
|
|
- if (hasWalkResult) {
|
|
|
|
|
- // 游走模式:显示所有高亮节点
|
|
|
|
|
- for (const nodeId of store.highlightedNodeIds) {
|
|
|
|
|
- const nodeData = store.getNode(nodeId)
|
|
|
|
|
- if (nodeData) {
|
|
|
|
|
- nodes.push({
|
|
|
|
|
- id: nodeId,
|
|
|
|
|
- ...nodeData,
|
|
|
|
|
- isCenter: nodeId === centerNodeId,
|
|
|
|
|
- isHighlighted: true
|
|
|
|
|
- })
|
|
|
|
|
- nodeSet.add(nodeId)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 始终使用游走模式:显示所有高亮节点和走过的边
|
|
|
|
|
+ for (const nodeId of store.highlightedNodeIds) {
|
|
|
|
|
+ const nodeData = store.getNode(nodeId)
|
|
|
|
|
+ if (nodeData) {
|
|
|
|
|
+ nodes.push({
|
|
|
|
|
+ id: nodeId,
|
|
|
|
|
+ ...nodeData,
|
|
|
|
|
+ isCenter: nodeId === centerNodeId,
|
|
|
|
|
+ isHighlighted: store.highlightedNodeIds.size > 1
|
|
|
|
|
+ })
|
|
|
|
|
+ nodeSet.add(nodeId)
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 使用游走时记录的边(只显示实际走过的路径)
|
|
|
|
|
- links.push(...walkedEdges)
|
|
|
|
|
- } else {
|
|
|
|
|
- // 普通模式:显示选中节点的直接邻居
|
|
|
|
|
|
|
+ // 如果高亮集合为空(不应该发生),至少显示中心节点
|
|
|
|
|
+ if (nodes.length === 0) {
|
|
|
nodes.push({
|
|
nodes.push({
|
|
|
id: centerNodeId,
|
|
id: centerNodeId,
|
|
|
...centerNode,
|
|
...centerNode,
|
|
@@ -254,28 +252,11 @@ function renderGraph() {
|
|
|
isHighlighted: false
|
|
isHighlighted: false
|
|
|
})
|
|
})
|
|
|
nodeSet.add(centerNodeId)
|
|
nodeSet.add(centerNodeId)
|
|
|
-
|
|
|
|
|
- const neighbors = store.getNeighbors(centerNodeId)
|
|
|
|
|
- for (const n of neighbors) {
|
|
|
|
|
- const nodeData = store.getNode(n.nodeId)
|
|
|
|
|
- if (nodeData && !nodeSet.has(n.nodeId)) {
|
|
|
|
|
- nodeSet.add(n.nodeId)
|
|
|
|
|
- nodes.push({
|
|
|
|
|
- id: n.nodeId,
|
|
|
|
|
- ...nodeData,
|
|
|
|
|
- isCenter: false,
|
|
|
|
|
- isHighlighted: false
|
|
|
|
|
- })
|
|
|
|
|
- links.push({
|
|
|
|
|
- source: n.direction === 'out' ? centerNodeId : n.nodeId,
|
|
|
|
|
- target: n.direction === 'out' ? n.nodeId : centerNodeId,
|
|
|
|
|
- type: n.edgeType,
|
|
|
|
|
- score: n.score
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 使用游走时记录的边(只显示实际走过的路径)
|
|
|
|
|
+ links.push(...walkedEdges)
|
|
|
|
|
+
|
|
|
const g = svg.append('g')
|
|
const g = svg.append('g')
|
|
|
|
|
|
|
|
// 缩放
|
|
// 缩放
|
|
@@ -297,18 +278,9 @@ function renderGraph() {
|
|
|
.data(links)
|
|
.data(links)
|
|
|
.join('line')
|
|
.join('line')
|
|
|
.attr('class', 'graph-link')
|
|
.attr('class', 'graph-link')
|
|
|
- .attr('stroke', d => edgeColors[d.type] || '#666')
|
|
|
|
|
|
|
+ .attr('stroke', d => edgeColors.value[d.type] || '#666')
|
|
|
.attr('stroke-width', 1.5)
|
|
.attr('stroke-width', 1.5)
|
|
|
|
|
|
|
|
- // 边标签
|
|
|
|
|
- const linkLabel = g.append('g')
|
|
|
|
|
- .selectAll('text')
|
|
|
|
|
- .data(links)
|
|
|
|
|
- .join('text')
|
|
|
|
|
- .attr('class', 'graph-link-label')
|
|
|
|
|
- .attr('text-anchor', 'middle')
|
|
|
|
|
- .text(d => d.type)
|
|
|
|
|
-
|
|
|
|
|
// 节点组
|
|
// 节点组
|
|
|
const node = g.append('g')
|
|
const node = g.append('g')
|
|
|
.selectAll('g')
|
|
.selectAll('g')
|
|
@@ -384,14 +356,19 @@ function renderGraph() {
|
|
|
.attr('x2', d => d.target.x)
|
|
.attr('x2', d => d.target.x)
|
|
|
.attr('y2', d => d.target.y)
|
|
.attr('y2', d => d.target.y)
|
|
|
|
|
|
|
|
- linkLabel
|
|
|
|
|
- .attr('x', d => (d.source.x + d.target.x) / 2)
|
|
|
|
|
- .attr('y', d => (d.source.y + d.target.y) / 2)
|
|
|
|
|
-
|
|
|
|
|
node.attr('transform', d => `translate(${d.x},${d.y})`)
|
|
node.attr('transform', d => `translate(${d.x},${d.y})`)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 点击空白取消激活
|
|
|
|
|
+function handleSvgClick(event) {
|
|
|
|
|
+ if (event.target.tagName === 'svg') {
|
|
|
|
|
+ store.clearSelection()
|
|
|
|
|
+ walkedEdges = []
|
|
|
|
|
+ renderGraph()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
watch(() => store.selectedNodeId, (nodeId) => {
|
|
watch(() => store.selectedNodeId, (nodeId) => {
|
|
|
if (nodeId) {
|
|
if (nodeId) {
|
|
|
executeWalk() // 点击节点自动执行游走
|
|
executeWalk() // 点击节点自动执行游走
|
|
@@ -400,6 +377,13 @@ watch(() => store.selectedNodeId, (nodeId) => {
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 监听配置变化,及时重新游走
|
|
|
|
|
+watch([walkSteps, stepConfigs], () => {
|
|
|
|
|
+ if (store.selectedNodeId) {
|
|
|
|
|
+ executeWalk()
|
|
|
|
|
+ }
|
|
|
|
|
+}, { deep: true })
|
|
|
|
|
+
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
renderGraph()
|
|
renderGraph()
|
|
|
})
|
|
})
|