更新若干功能

This commit is contained in:
李琦
2026-08-15 17:18:00 +08:00
parent 4954295961
commit 6a81e479ef
77 changed files with 8165 additions and 2372 deletions

View File

@@ -2,7 +2,7 @@
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, Home, ListTodo, TicketCheck, CalendarDays, CalendarCheck2, Sparkles, UserRound, ClipboardList, Wrench, Rocket, ChevronDown, Bell, StickyNote, ListFilter, Palette, Bot, Users, NotebookPen, CloudUpload, Power } from 'lucide-vue-next'
import { LayoutDashboard, ScrollText, Settings, X, Database, SlidersHorizontal, Home, ListTodo, TicketCheck, CalendarDays, CalendarCheck2, Sparkles, UserRound, ClipboardList, Wrench, Rocket, ChevronDown, Bell, StickyNote, ListFilter, Palette, Bot, Users, NotebookPen, CloudUpload, Power, Image, Activity, Package, Shield } from 'lucide-vue-next'
import { useAppStore } from './store'
import DatabaseSetup from './components/DatabaseSetup.vue'
import BrowserBlocked from './components/BrowserBlocked.vue'
@@ -14,6 +14,7 @@ import AboutModal from './components/AboutModal.vue'
import MessageBell from './components/MessageBell.vue'
import TaskCenter from './components/TaskCenter.vue'
import NoteCenter from './components/NoteCenter.vue'
import LocalPackCenter from './components/LocalPackCenter.vue'
import TeamSwitcher from './components/TeamSwitcher.vue'
import TitleBar from './components/TitleBar.vue'
import { call, isNative, on } from './api'
@@ -25,6 +26,9 @@ const { t, locale } = useI18n()
const native = isNative()
const quickOpen = ref(false)
const aboutOpen = ref(false)
const exitOpen = ref(false)
const updateInfo = ref(null)
const updateBusy = ref(false)
const appVersion = ref('1.0.0')
const displayedTask = ref(null)
let off
@@ -47,9 +51,17 @@ function openSettings() {
router.push('/settings')
}
// 侧边栏底部退出:与原生菜单/托盘「退出」一致,绕过最小化到托盘直接退出应用
async function quitApp() {
if (!confirm(t('quitConfirm'))) return
// 侧栏头像下退出:模态框选择「退出登录」或「退出程序」
async function exitLogout() {
exitOpen.value = false
try {
await call('SyncLogout')
await store.refreshSyncStatus()
store.showToast({ type: 'success', key: 'logoutToast' })
} catch { /* ignore */ }
}
async function exitQuit() {
exitOpen.value = false
try { await call('QuitApp') } catch { /* 预览模式无原生桥 */ }
}
@@ -79,14 +91,18 @@ const navGroups = [
] },
{ id: 'system', icon: Wrench, label: 'navSystem', items: [
{ to: '/launchpad', icon: Rocket, label: 'launchpad' },
{ to: '/ports', icon: Activity, label: 'portMonitor' },
{ to: '/pack-tasks', icon: Package, label: 'packTasksPage' },
{ to: '/logs', icon: ScrollText, label: 'logs' }
] },
{ id: 'settings', icon: Settings, label: 'settings', items: [
{ to: '/admin', icon: Shield, label: 'adminTitle', adminOnly: true, match: r => r.path === '/admin' },
{ to: '/settings?tab=rules', icon: ListFilter, label: 'tabRules', match: r => r.path === '/settings' && settingsTab(r) === 'rules' },
{ to: '/settings?tab=appearance', icon: Palette, label: 'tabAppearance', match: r => r.path === '/settings' && settingsTab(r) === 'appearance' },
{ to: '/settings?tab=ai', icon: Bot, label: 'aiAnalysis', match: r => r.path === '/settings' && settingsTab(r) === 'ai' },
// 全局文件存储配置:权威数据在服务器,仅管理员(云端账号 id=1可见可改
{ to: '/settings?tab=filestorage', icon: CloudUpload, label: 'tabFileStorage', adminOnly: true, match: r => r.path === '/settings' && settingsTab(r) === 'filestorage' },
{ to: '/settings?tab=kindicons', icon: Image, label: 'tabKindIcons', adminOnly: true, match: r => r.path === '/settings' && settingsTab(r) === 'kindicons' },
{ to: '/settings?tab=database', icon: Database, label: 'tabDatabase', match: r => r.path === '/settings' && settingsTab(r) === 'database' }
] }
]
@@ -161,12 +177,28 @@ onMounted(async () => {
store.refreshSyncStatus()
}
}),
on('menu:set', p => p?.key && updateSetting(p.key, p.value))
on('menu:set', p => p?.key && updateSetting(p.key, p.value)),
on('app:update-available', p => {
if (p && !p.upToDate && !p.skipped) updateInfo.value = p
})
]
await store.boot()
locale.value = store.settings.locale || 'zh-CN'
try { appVersion.value = await call('GetAppVersion') } catch { /* keep fallback */ }
})
async function installUpdate() {
updateBusy.value = true
try {
await call('DownloadAndInstallUpdate')
} catch (e) {
store.showToast({ type: 'error', text: String(e) })
updateBusy.value = false
}
}
async function skipUpdate() {
try { await call('SkipAppUpdate', updateInfo.value?.latest || '') } catch {}
updateInfo.value = null
}
onUnmounted(() => {
removeEventListener('keydown', onGlobalKey)
removeEventListener('click', onFlyoutAway)
@@ -211,7 +243,7 @@ watch(activeTask, task => {
<MessageBell />
<TeamSwitcher />
</div>
<button class="rail-user" :class="{ active: route.path === '/profile' }" :title="store.syncStatus.loggedIn ? store.syncStatus.username : t('loginNow')" @click="store.openAccount(router)">
<button class="rail-user" :title="store.syncStatus.loggedIn ? store.syncStatus.username : t('loginNow')" @click="store.openAccount(router)">
<span class="user-avatar">
<img v-if="store.avatarSrc" :src="store.avatarSrc" alt="" />
<b v-else-if="store.syncStatus.username">{{ store.syncStatus.username[0].toUpperCase() }}</b>
@@ -220,6 +252,10 @@ watch(activeTask, task => {
</span>
<em v-if="store.syncStatus.loggedIn && store.syncStatus.pending > 0" class="user-pending rail-pending" :title="t('pendingSync', { n: store.syncStatus.pending })">{{ store.syncStatus.pending }}</em>
</button>
<div class="rail-foot">
<LocalPackCenter />
<button type="button" class="rail-quit" :title="t('exitMenu')" :disabled="!native" @click="exitOpen = true"><Power /></button>
</div>
</div>
<div class="side-sub">
<div class="sub-brand"><b>{{ t('app') }}</b></div>
@@ -239,7 +275,6 @@ watch(activeTask, task => {
</nav>
<div class="sidebar-bottom">
<span class="version"><i />v{{ appVersion }}</span>
<button class="quick-settings-btn quit-app-btn" :title="t('quitApp')" :disabled="!native" @click="quitApp"><Power /></button>
<button class="quick-settings-btn" :title="t('quickSettings')" @click="quickOpen = !quickOpen"><SlidersHorizontal /></button>
<section v-if="quickOpen" class="quick-settings popover-glass">
<header>
@@ -281,6 +316,37 @@ watch(activeTask, task => {
</template>
</div>
</aside>
<Teleport to="body">
<div v-if="updateInfo" class="overlay" @click.self="skipUpdate">
<section class="modal exit-modal" @click.stop>
<header class="modal-head">
<h2>{{ t('appUpdateTitle') }}</h2>
<button type="button" class="nm-close" :title="t('close')" @click="skipUpdate"><X /></button>
</header>
<p class="exit-hint">{{ t('appUpdateBody', { current: updateInfo.current, latest: updateInfo.latest }) }}</p>
<pre v-if="updateInfo.changelog" style="white-space:pre-wrap;font-size:.85rem;opacity:.8;max-height:160px;overflow:auto">{{ updateInfo.changelog }}</pre>
<div class="exit-actions">
<button type="button" class="btn secondary" :disabled="updateBusy" @click="skipUpdate">{{ t('appUpdateSkip') }}</button>
<button type="button" class="btn primary" :disabled="updateBusy" @click="installUpdate">{{ updateBusy ? t('appUpdateDownloading') : t('appUpdateInstall') }}</button>
</div>
</section>
</div>
</Teleport>
<Teleport to="body">
<div v-if="exitOpen" class="overlay" @click.self="exitOpen = false">
<section class="modal exit-modal" @click.stop>
<header class="modal-head">
<h2><Power />{{ t('exitMenu') }}</h2>
<button type="button" class="nm-close" :title="t('close')" @click="exitOpen = false"><X /></button>
</header>
<p class="exit-hint">{{ t('exitMenuHint') }}</p>
<div class="exit-actions">
<button v-if="store.syncStatus.loggedIn" type="button" class="btn secondary" @click="exitLogout">{{ t('logoutBtn') }}</button>
<button type="button" class="btn danger" :disabled="!native" @click="exitQuit">{{ t('quitApp') }}</button>
</div>
</section>
</div>
</Teleport>
<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>