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 />
|
<AppToolbar />
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<TtsPlayerBar />
|
<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' }" />
|
<AgentsView v-if="visited.nyx" :class="{ 'view-hidden': route.name !== 'nyx' }" />
|
||||||
<ViewerView v-if="ViewerView && visited.viewer" :class="{ 'view-hidden': route.name !== 'viewer' }" />
|
<ViewerView v-if="ViewerView && visited.viewer" :class="{ 'view-hidden': route.name !== 'viewer' }" />
|
||||||
<!-- Non-socket views: scrollable -->
|
<TestsView v-if="visited.tests" :class="{ 'view-hidden': route.name !== 'tests' }" />
|
||||||
<div v-if="routerReady && !isSocketRoute" class="page-scroll">
|
<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 />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -28,15 +38,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute } from 'vue-router';
|
||||||
import { ws, auth, agents } from './store';
|
import { ws, auth, agents } from './store';
|
||||||
import { THEME_LOGOS, useTheme } from './composables/useTheme';
|
import { THEME_LOGOS, useTheme } from './composables/useTheme';
|
||||||
import { useChatStore } from './store/chat';
|
import { useChatStore } from './store/chat';
|
||||||
import tenant from './tenant';
|
import tenant from './tenant';
|
||||||
|
|
||||||
import { defineAsyncComponent } from 'vue';
|
|
||||||
|
|
||||||
import GridOverlay from './components/GridOverlay.vue';
|
import GridOverlay from './components/GridOverlay.vue';
|
||||||
import BreakpointBadge from './components/BreakpointBadge.vue';
|
import BreakpointBadge from './components/BreakpointBadge.vue';
|
||||||
import AppSidebar from './components/AppSidebar.vue';
|
import AppSidebar from './components/AppSidebar.vue';
|
||||||
@ -44,18 +52,18 @@ import AppToolbar from './components/AppToolbar.vue';
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
||||||
import TtsPlayerBar from './components/TtsPlayerBar.vue';
|
import TtsPlayerBar from './components/TtsPlayerBar.vue';
|
||||||
|
import { HomePage, ImpressumPage, DatenschutzPage } from './tenantPages';
|
||||||
const AgentsView = defineAsyncComponent(() => import('./views/AgentsView.vue'));
|
const AgentsView = defineAsyncComponent(() => import('./views/AgentsView.vue'));
|
||||||
|
const TestsView = defineAsyncComponent(() => import('./views/TestsView.vue'));
|
||||||
const ViewerView = tenant.features.viewer
|
const ViewerView = tenant.features.viewer
|
||||||
? defineAsyncComponent(() => import('./views/ViewerView.vue'))
|
? defineAsyncComponent(() => import('./views/ViewerView.vue'))
|
||||||
: null;
|
: null;
|
||||||
|
const HomePageAsync = defineAsyncComponent(HomePage);
|
||||||
|
const ImpressumPageAsync = defineAsyncComponent(ImpressumPage);
|
||||||
|
const DatenschutzPageAsync = defineAsyncComponent(DatenschutzPage);
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const visited = reactive({ nyx: false, viewer: false });
|
const visited = reactive({ nyx: false, viewer: false, tests: false, home: false, impressum: false, datenschutz: 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 route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@ -68,7 +76,7 @@ const { theme } = useTheme();
|
|||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
doLogout(disconnect);
|
doLogout(disconnect);
|
||||||
visited.nyx = visited.viewer = false;
|
Object.keys(visited).forEach(k => (visited as any)[k] = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeConnect() {
|
function maybeConnect() {
|
||||||
|
|||||||
@ -58,6 +58,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { getApiBase } from '../utils/apiBase';
|
import { getApiBase } from '../utils/apiBase';
|
||||||
import { relativeTime } from '../utils/relativeTime';
|
import { relativeTime } from '../utils/relativeTime';
|
||||||
import { provideToolbar, useConnection } from '../composables/useToolbar';
|
import { provideToolbar, useConnection } from '../composables/useToolbar';
|
||||||
@ -90,8 +91,10 @@ const updatedAt = ref('');
|
|||||||
const expanded = ref<Set<string>>(new Set());
|
const expanded = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
// SSE connection — managed by useConnection, state surfaced in toolbar
|
// 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);
|
const testConn = useConnection(`${getApiBase()}/api/test-results`, handleResult);
|
||||||
provideToolbar({ groups: ['themes'], connection: testConn });
|
provideToolbar({ groups: ['themes'], connection: testConn }, _viewActive);
|
||||||
|
|
||||||
|
|
||||||
const displayResults = computed(() =>
|
const displayResults = computed(() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user