phase 6
This commit is contained in:
@@ -16,8 +16,10 @@ import { usePipelineStore } from './stores/pipeline'
|
|||||||
import { useSSEConnection } from './composables/useSSEConnection'
|
import { useSSEConnection } from './composables/useSSEConnection'
|
||||||
import { useCheckpointLoader } from './composables/useCheckpointLoader'
|
import { useCheckpointLoader } from './composables/useCheckpointLoader'
|
||||||
import { useEditorState } from './composables/useEditorState'
|
import { useEditorState } from './composables/useEditorState'
|
||||||
|
import { useHashRouter } from './composables/useHashRouter'
|
||||||
|
|
||||||
const pipeline = usePipelineStore()
|
const pipeline = usePipelineStore()
|
||||||
|
useHashRouter()
|
||||||
const logPanel = ref<{ clear: () => void } | null>(null)
|
const logPanel = ref<{ clear: () => void } | null>(null)
|
||||||
|
|
||||||
// SSE connection + pipeline status
|
// SSE connection + pipeline status
|
||||||
|
|||||||
70
ui/detection-app/src/composables/useHashRouter.ts
Normal file
70
ui/detection-app/src/composables/useHashRouter.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { watch, onUnmounted } from 'vue'
|
||||||
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bidirectional sync between URL hash and pipeline store layout state.
|
||||||
|
*
|
||||||
|
* Hash format:
|
||||||
|
* #/ → normal dashboard
|
||||||
|
* #/editor/<stage> → bbox/region editor for that stage
|
||||||
|
* #/config/<stage> → stage config editor
|
||||||
|
* #/source → source selector
|
||||||
|
*
|
||||||
|
* Call once in App.vue. Handles popstate (back/forward) and
|
||||||
|
* updates the hash when store state changes.
|
||||||
|
*/
|
||||||
|
export function useHashRouter() {
|
||||||
|
const pipeline = usePipelineStore()
|
||||||
|
|
||||||
|
function parseHash(hash: string) {
|
||||||
|
const path = hash.replace(/^#\/?/, '')
|
||||||
|
if (!path || path === 'dashboard') {
|
||||||
|
return { mode: 'normal', stage: null as string | null }
|
||||||
|
}
|
||||||
|
if (path === 'source') {
|
||||||
|
return { mode: 'source_selector', stage: null as string | null }
|
||||||
|
}
|
||||||
|
const editorMatch = path.match(/^editor\/(.+)$/)
|
||||||
|
if (editorMatch) {
|
||||||
|
return { mode: 'bbox_editor', stage: editorMatch[1] }
|
||||||
|
}
|
||||||
|
const configMatch = path.match(/^config\/(.+)$/)
|
||||||
|
if (configMatch) {
|
||||||
|
return { mode: 'stage_editor', stage: configMatch[1] }
|
||||||
|
}
|
||||||
|
return { mode: 'normal', stage: null as string | null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHash() {
|
||||||
|
const { mode, stage } = parseHash(window.location.hash)
|
||||||
|
pipeline.layoutMode = mode
|
||||||
|
pipeline.editorStage = stage
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHash() {
|
||||||
|
let hash = '#/'
|
||||||
|
if (pipeline.layoutMode === 'bbox_editor' && pipeline.editorStage) {
|
||||||
|
hash = `#/editor/${pipeline.editorStage}`
|
||||||
|
} else if (pipeline.layoutMode === 'stage_editor' && pipeline.editorStage) {
|
||||||
|
hash = `#/config/${pipeline.editorStage}`
|
||||||
|
} else if (pipeline.layoutMode === 'source_selector') {
|
||||||
|
hash = '#/source'
|
||||||
|
}
|
||||||
|
if (window.location.hash !== hash) {
|
||||||
|
window.history.pushState(null, '', hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync hash → state on load
|
||||||
|
applyHash()
|
||||||
|
|
||||||
|
// Sync hash → state on back/forward
|
||||||
|
window.addEventListener('popstate', applyHash)
|
||||||
|
onUnmounted(() => window.removeEventListener('popstate', applyHash))
|
||||||
|
|
||||||
|
// Sync state → hash when layout changes
|
||||||
|
watch(
|
||||||
|
() => [pipeline.layoutMode, pipeline.editorStage],
|
||||||
|
updateHash,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Pipeline store — run state, transport controls, checkpoint status.
|
* Pipeline store — run state, transport controls, checkpoint status.
|
||||||
*
|
*
|
||||||
* Layout is driven by URL hash:
|
|
||||||
* #/ → normal dashboard
|
|
||||||
* #/editor/<stage> → bbox/region editor for that stage
|
|
||||||
* #/config/<stage> → stage config editor
|
|
||||||
* #/source → source selector
|
|
||||||
*
|
|
||||||
* State shape defined in types/store-state.ts.
|
* State shape defined in types/store-state.ts.
|
||||||
|
* Hash routing is handled by useHashRouter composable, not here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { NodeState } from '../types/store-state'
|
import type { NodeState } from '../types/store-state'
|
||||||
import type { CheckpointInfo } from '../types/sse-contract'
|
import type { CheckpointInfo } from '../types/sse-contract'
|
||||||
|
|
||||||
@@ -26,7 +21,6 @@ export const usePipelineStore = defineStore('pipeline', () => {
|
|||||||
const checkpoints = ref<CheckpointInfo[]>([])
|
const checkpoints = ref<CheckpointInfo[]>([])
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
// Layout mode — synced with URL hash
|
|
||||||
const layoutMode = ref<string>('normal')
|
const layoutMode = ref<string>('normal')
|
||||||
const editorStage = ref<string | null>(null)
|
const editorStage = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -35,56 +29,6 @@ export const usePipelineStore = defineStore('pipeline', () => {
|
|||||||
const canReplay = computed(() => checkpoints.value.length > 0)
|
const canReplay = computed(() => checkpoints.value.length > 0)
|
||||||
const isEditing = computed(() => layoutMode.value !== 'normal')
|
const isEditing = computed(() => layoutMode.value !== 'normal')
|
||||||
|
|
||||||
// --- Hash routing ---
|
|
||||||
|
|
||||||
function parseHash(hash: string) {
|
|
||||||
const path = hash.replace(/^#\/?/, '')
|
|
||||||
if (!path || path === 'dashboard') {
|
|
||||||
return { mode: 'normal', stage: null }
|
|
||||||
}
|
|
||||||
if (path === 'source') {
|
|
||||||
return { mode: 'source_selector', stage: null }
|
|
||||||
}
|
|
||||||
const editorMatch = path.match(/^editor\/(.+)$/)
|
|
||||||
if (editorMatch) {
|
|
||||||
return { mode: 'bbox_editor', stage: editorMatch[1] }
|
|
||||||
}
|
|
||||||
const configMatch = path.match(/^config\/(.+)$/)
|
|
||||||
if (configMatch) {
|
|
||||||
return { mode: 'stage_editor', stage: configMatch[1] }
|
|
||||||
}
|
|
||||||
return { mode: 'normal', stage: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyHash() {
|
|
||||||
const { mode, stage } = parseHash(window.location.hash)
|
|
||||||
layoutMode.value = mode
|
|
||||||
editorStage.value = stage
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHash() {
|
|
||||||
let hash = '#/'
|
|
||||||
if (layoutMode.value === 'bbox_editor' && editorStage.value) {
|
|
||||||
hash = `#/editor/${editorStage.value}`
|
|
||||||
} else if (layoutMode.value === 'stage_editor' && editorStage.value) {
|
|
||||||
hash = `#/config/${editorStage.value}`
|
|
||||||
} else if (layoutMode.value === 'source_selector') {
|
|
||||||
hash = '#/source'
|
|
||||||
}
|
|
||||||
if (window.location.hash !== hash) {
|
|
||||||
window.history.pushState(null, '', hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync hash → state on load and popstate (back/forward)
|
|
||||||
applyHash()
|
|
||||||
window.addEventListener('popstate', applyHash)
|
|
||||||
|
|
||||||
// Sync state → hash when layout changes
|
|
||||||
watch([layoutMode, editorStage], updateHash)
|
|
||||||
|
|
||||||
// --- Actions ---
|
|
||||||
|
|
||||||
function setJob(id: string) {
|
function setJob(id: string) {
|
||||||
jobId.value = id
|
jobId.value = id
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user