Files
code-utils/frontend/src/App.vue

143 lines
6.4 KiB
Vue
Raw Normal View History

2026-08-11 19:07:05 +08:00
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { LayoutDashboard, ScrollText, Settings, X, Database, SlidersHorizontal } from 'lucide-vue-next'
import { useAppStore } from './store'
import DatabaseSetup from './components/DatabaseSetup.vue'
import BrowserBlocked from './components/BrowserBlocked.vue'
import AnalysisCanvas from './components/AnalysisCanvas.vue'
import { isNative } from './api'
const route = useRoute()
const router = useRouter()
const store = useAppStore()
const { t, locale } = useI18n()
const native = isNative()
const quickOpen = ref(false)
const displayedTask = ref(null)
let off
let loadingHoldTimer
const activeTask = computed(() => Object.values(store.tasks).find(x => !['completed', 'error', 'cancelled'].includes(x.stage)))
const visibleTask = computed(() => activeTask.value || displayedTask.value)
const activeTaskProject = computed(() => visibleTask.value?.params?.project || store.projects.find(p => p.id === visibleTask.value?.projectId)?.name || '')
const loadingStyle = computed(() => store.settings.loadingStyle === 'fullscreen' ? 'fullscreen-orbit' : (store.settings.loadingStyle || 'fullscreen-orbit'))
const useFullscreenLoading = computed(() => loadingStyle.value !== 'bar')
async function updateSetting(key, value) {
const next = await store.saveSettings({ [key]: value })
if (key === 'locale') locale.value = next.locale
}
function openSettings() {
quickOpen.value = false
router.push('/settings')
}
onMounted(async () => {
if (!native) return
off = store.listen()
await store.boot()
locale.value = store.settings.locale || 'zh-CN'
})
onUnmounted(() => {
clearTimeout(loadingHoldTimer)
off?.()
})
watch(() => store.settings.locale, v => {
if (v) locale.value = v
})
watch(activeTask, task => {
clearTimeout(loadingHoldTimer)
if (task) {
displayedTask.value = task
return
}
loadingHoldTimer = setTimeout(() => {
displayedTask.value = null
}, 900)
}, { immediate: true })
</script>
<template>
<BrowserBlocked v-if="!native" />
<DatabaseSetup v-else-if="store.bootstrap.state !== 'ready' && store.bootstrap.state !== 'loading'" :status="store.bootstrap" />
<div v-else-if="store.bootstrap.state === 'ready'" class="shell">
<aside class="sidebar">
<div class="brand">
<span class="brand-mark animated-logo" aria-hidden="true">
<svg viewBox="0 0 48 48" role="img">
<defs>
<linearGradient id="logoGlow" x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stop-color="#6ee7ff" />
<stop offset="48%" stop-color="#8b5cf6" />
<stop offset="100%" stop-color="#34d399" />
</linearGradient>
</defs>
<rect class="logo-frame" x="6" y="6" width="36" height="36" rx="9" />
<path class="logo-track" d="M17 18l-6 6 6 6M31 18l6 6-6 6M27 14l-6 20" />
<path class="logo-spark" d="M12 10h8M28 38h8" />
</svg>
</span>
<b>{{ t('app') }}</b>
</div>
<nav aria-label="Primary">
<RouterLink to="/" :class="{ active: route.path === '/' }"><LayoutDashboard /><span>{{ t('dashboard') }}</span></RouterLink>
<RouterLink to="/logs" :class="{ active: route.path === '/logs' }"><ScrollText /><span>{{ t('logs') }}</span></RouterLink>
<RouterLink to="/settings" :class="{ active: route.path === '/settings' }"><Settings /><span>{{ t('settings') }}</span></RouterLink>
</nav>
<div class="sidebar-bottom">
<span class="version"><i />v1.0.0</span>
<button class="quick-settings-btn" :title="t('quickSettings')" @click="quickOpen = !quickOpen"><SlidersHorizontal /></button>
<section v-if="quickOpen" class="quick-settings popover-glass">
<header>
<b>{{ t('quickSettings') }}</b>
<button @click="quickOpen = false" aria-label="Close"><X /></button>
</header>
<label>
<span>{{ t('language') }}</span>
<select :value="store.settings.locale" @change="updateSetting('locale', $event.target.value)">
<option value="zh-CN">中文</option>
<option value="en">English</option>
</select>
</label>
<label>
<span>{{ t('theme') }}</span>
<select :value="store.settings.theme" @change="updateSetting('theme', $event.target.value)">
<option value="dark">{{ t('themeDark') }}</option>
<option value="light">{{ t('themeLight') }}</option>
<option value="system">{{ t('themeSystem') }}</option>
</select>
</label>
<label>
<span>{{ t('glassOpacity') }} · {{ store.settings.glassOpacity }}%</span>
<input type="range" min="30" max="75" :value="store.settings.glassOpacity" @input="updateSetting('glassOpacity', Number($event.target.value))" />
</label>
<button class="btn secondary full" @click="openSettings"><Settings />{{ t('openSettings') }}</button>
</section>
</div>
</aside>
<main><RouterView /></main>
<div v-if="visibleTask && !useFullscreenLoading" class="taskbar">
<div><b>{{ activeTaskProject || visibleTask.stage }}</b><span>{{ t(visibleTask.messageKey || 'task.start', visibleTask.params || {}) }}</span></div>
<div class="progress"><i :style="{ width: visibleTask.progress + '%' }" /></div>
<b>{{ visibleTask.progress }}%</b>
</div>
<div v-if="visibleTask && useFullscreenLoading" class="analysis-loading" :class="[loadingStyle, { holding: !activeTask }]">
<AnalysisCanvas :progress="visibleTask.progress" :variant="loadingStyle" />
<section>
<b>{{ activeTaskProject || t('analyzingProject') }}</b>
<span>{{ t(visibleTask.messageKey || 'task.start', visibleTask.params || {}) }}</span>
<div class="progress"><i :style="{ width: visibleTask.progress + '%' }" /></div>
<strong>{{ visibleTask.progress }}%</strong>
</section>
</div>
<div v-if="store.toast" class="toast" :class="[store.toast.type, { muted: store.toast.muted, leaving: store.toast.leaving }]">
{{ store.toast.key ? t(store.toast.key, store.toast.params || {}) : store.toast.text }}
<button @click="store.closeToast()" aria-label="Close"><X /></button>
</div>
</div>
<div v-else class="boot-loading"><Database class="spin" />正在检查数据库...</div>
</template>