初始化
This commit is contained in:
142
frontend/src/App.vue
Normal file
142
frontend/src/App.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user