143 lines
6.4 KiB
Vue
143 lines
6.4 KiB
Vue
|
|
<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>
|