feat: add scene thumbnails to TreeFlow nodes with auto-generated demo thumbs

This commit is contained in:
2026-06-12 12:08:39 +08:00
parent ac0a6e2cd6
commit 9baa7b5ab3
14 changed files with 230 additions and 58 deletions

View File

@@ -205,6 +205,7 @@ const tree = computed(() => buildTreeForChapter(currentChapterId.value))
<TreeFlow
v-if="tree"
:node="tree"
:scenes="scenes"
:key="currentChapterId"
@select-scene="onSelectScene"
/>

View File

@@ -5,6 +5,7 @@ import dagre from 'dagre'
const props = defineProps<{
node: PlayerTreeNode | null
scenes?: Record<string, { thumbnail?: string }>
}>()
const emit = defineEmits<{
@@ -15,6 +16,7 @@ interface FlowNode {
id: string
sceneId: string
label: string
thumbnail?: string
visited: boolean
isMystery: boolean
locked: boolean
@@ -38,7 +40,7 @@ const containerW = ref(800)
const containerH = ref(400)
function buildFlow(root: PlayerTreeNode) {
const dagreNodes: { id: string; sceneId: string; parent: string | null; label: string; visited: boolean; isMystery: boolean; locked: boolean; lockHint?: string }[] = []
const dagreNodes: { id: string; sceneId: string; thumbnail?: string; parent: string | null; label: string; visited: boolean; isMystery: boolean; locked: boolean; lockHint?: string }[] = []
const dagreEdges: { from: string; to: string; visited: boolean }[] = []
function walk(node: PlayerTreeNode, parentId: string | null) {
@@ -47,6 +49,7 @@ function buildFlow(root: PlayerTreeNode) {
dagreNodes.push({
id: dagreId,
sceneId: node.sceneId,
thumbnail: props.scenes?.[node.sceneId]?.thumbnail,
parent: parentId,
label: node.label,
visited: true,
@@ -91,11 +94,13 @@ function buildFlow(root: PlayerTreeNode) {
g.setGraph({ rankdir: 'LR', nodesep: 40, ranksep: 80, marginx: 24, marginy: 24 })
g.setDefaultEdgeLabel(() => ({}))
const nodeW = 120
const nodeH = 44
const baseW = 128
const baseH = 40
const thumbH = 78
for (const n of dagreNodes) {
g.setNode(n.id, { width: nodeW, height: nodeH })
const hasThumb = !!n.thumbnail
g.setNode(n.id, { width: hasThumb ? baseW + 20 : baseW, height: hasThumb ? baseH + thumbH + 8 : baseH })
}
for (const e of dagreEdges) {
g.setEdge(e.from, e.to)
@@ -105,18 +110,22 @@ function buildFlow(root: PlayerTreeNode) {
const resultNodes: FlowNode[] = dagreNodes.map((n) => {
const pos = g.node(n.id)
const hasThumb = !!n.thumbnail
const nw = hasThumb ? baseW + 20 : baseW
const nh = hasThumb ? baseH + thumbH + 8 : baseH
return {
id: n.id,
sceneId: n.sceneId,
thumbnail: n.thumbnail,
label: n.label,
visited: n.visited,
isMystery: n.isMystery,
locked: n.locked,
lockHint: n.lockHint,
x: pos.x - nodeW / 2,
y: pos.y - nodeH / 2,
w: nodeW,
h: nodeH,
x: pos.x - nw / 2,
y: pos.y - nh / 2,
w: nw,
h: nh,
}
})
@@ -234,6 +243,7 @@ const svgH = computed(() => containerH.value)
:title="n.lockHint || ''"
@click="n.visited && n.sceneId && emit('selectScene', n.sceneId)"
>
<img v-if="n.thumbnail" :src="n.thumbnail" class="node-thumb" />
<span class="node-icon">{{ n.visited ? '✦' : n.isMystery ? '?' : '⬜' }}</span>
<span class="node-label">{{ n.label }}</span>
</div>
@@ -264,15 +274,29 @@ const svgH = computed(() => containerH.value)
.flow-node {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 0 10px;
justify-content: center;
gap: 4px;
padding: 4px 8px 6px;
border-radius: 4px;
font-size: 12px;
transition: all 0.15s;
overflow: hidden;
}
.flow-node:has(.node-thumb) {
padding: 4px 4px 6px;
}
.node-thumb {
width: 128px;
height: 72px;
object-fit: cover;
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.06);
}
.flow-node.clickable {
cursor: pointer;
}