更新若干功能
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user