feat(app): keep all views alive after first visit, preserve scroll state
All route views now mount once and hide via CSS (.view-hidden) instead of unmounting. Tenant pages wrapped in .page-scroll divs for scrollability. Only LoginView stays in RouterView (no state to preserve). TestsView updated to use active ref in provideToolbar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d48612665b
commit
ef5080783e
34
src/App.vue
34
src/App.vue
@ -12,11 +12,21 @@
|
||||
<AppToolbar />
|
||||
<div class="content-area">
|
||||
<TtsPlayerBar />
|
||||
<!-- Socket views: v-if=visited (lazy first mount), class-based hide (preserve scroll) -->
|
||||
<!-- All views kept alive after first visit — CSS hide preserves scroll/state -->
|
||||
<AgentsView v-if="visited.nyx" :class="{ 'view-hidden': route.name !== 'nyx' }" />
|
||||
<ViewerView v-if="ViewerView && visited.viewer" :class="{ 'view-hidden': route.name !== 'viewer' }" />
|
||||
<!-- Non-socket views: scrollable -->
|
||||
<div v-if="routerReady && !isSocketRoute" class="page-scroll">
|
||||
<TestsView v-if="visited.tests" :class="{ 'view-hidden': route.name !== 'tests' }" />
|
||||
<div v-if="visited.home" :class="['page-scroll', { 'view-hidden': route.name !== 'home' }]">
|
||||
<component :is="HomePageAsync" />
|
||||
</div>
|
||||
<div v-if="visited.impressum" :class="['page-scroll', { 'view-hidden': route.name !== 'impressum' }]">
|
||||
<component :is="ImpressumPageAsync" />
|
||||
</div>
|
||||
<div v-if="visited.datenschutz" :class="['page-scroll', { 'view-hidden': route.name !== 'datenschutz' }]">
|
||||
<component :is="DatenschutzPageAsync" />
|
||||
</div>
|
||||
<!-- Login: no state to preserve -->
|
||||
<div v-if="route.name === 'login'" class="page-scroll">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
@ -28,15 +38,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { ref, reactive, onMounted, computed, watch, defineAsyncComponent } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ws, auth, agents } from './store';
|
||||
import { THEME_LOGOS, useTheme } from './composables/useTheme';
|
||||
import { useChatStore } from './store/chat';
|
||||
import tenant from './tenant';
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
import GridOverlay from './components/GridOverlay.vue';
|
||||
import BreakpointBadge from './components/BreakpointBadge.vue';
|
||||
import AppSidebar from './components/AppSidebar.vue';
|
||||
@ -44,18 +52,18 @@ import AppToolbar from './components/AppToolbar.vue';
|
||||
import router from './router';
|
||||
|
||||
import TtsPlayerBar from './components/TtsPlayerBar.vue';
|
||||
import { HomePage, ImpressumPage, DatenschutzPage } from './tenantPages';
|
||||
const AgentsView = defineAsyncComponent(() => import('./views/AgentsView.vue'));
|
||||
const TestsView = defineAsyncComponent(() => import('./views/TestsView.vue'));
|
||||
const ViewerView = tenant.features.viewer
|
||||
? defineAsyncComponent(() => import('./views/ViewerView.vue'))
|
||||
: null;
|
||||
const HomePageAsync = defineAsyncComponent(HomePage);
|
||||
const ImpressumPageAsync = defineAsyncComponent(ImpressumPage);
|
||||
const DatenschutzPageAsync = defineAsyncComponent(DatenschutzPage);
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const visited = reactive({ nyx: false, viewer: false });
|
||||
const socketRoutes = ['nyx', ...(tenant.features.viewer ? ['viewer'] : [])];
|
||||
const isSocketRoute = computed(() => socketRoutes.includes(route.name as string));
|
||||
|
||||
const routerReady = ref(false);
|
||||
router.isReady().then(() => { routerReady.value = true; });
|
||||
const visited = reactive({ nyx: false, viewer: false, tests: false, home: false, impressum: false, datenschutz: false });
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@ -68,7 +76,7 @@ const { theme } = useTheme();
|
||||
|
||||
function handleLogout() {
|
||||
doLogout(disconnect);
|
||||
visited.nyx = visited.viewer = false;
|
||||
Object.keys(visited).forEach(k => (visited as any)[k] = false);
|
||||
}
|
||||
|
||||
function maybeConnect() {
|
||||
|
||||
@ -58,6 +58,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getApiBase } from '../utils/apiBase';
|
||||
import { relativeTime } from '../utils/relativeTime';
|
||||
import { provideToolbar, useConnection } from '../composables/useToolbar';
|
||||
@ -90,8 +91,10 @@ const updatedAt = ref('');
|
||||
const expanded = ref<Set<string>>(new Set());
|
||||
|
||||
// SSE connection — managed by useConnection, state surfaced in toolbar
|
||||
const _route = useRoute();
|
||||
const _viewActive = computed(() => _route.name === 'tests');
|
||||
const testConn = useConnection(`${getApiBase()}/api/test-results`, handleResult);
|
||||
provideToolbar({ groups: ['themes'], connection: testConn });
|
||||
provideToolbar({ groups: ['themes'], connection: testConn }, _viewActive);
|
||||
|
||||
|
||||
const displayResults = computed(() =>
|
||||
|
||||
Reference in New Issue
Block a user