Private
Public Access
1
0
Files
u-desk/web/src/App.vue
绝尘 691e38604f 发布:v0.3.3 版本历史模块 + 域名迁移 + 站点版本信息修正
- 版本号更新至 0.3.3(version.go/wails.json/README.md)
- 更新检查域名迁移 img.1216.top → c.1216.top
- 新增 views/version 版本历史 Tab 页面(时间线 UI)
- 设置面板新增版本历史入口按钮
- CHANGELOG 补全 0.3.3 全部 17 个提交记录
- 站点 HTML 修正(删除错误 v0.4.0,v0.3.3 为最新)
- 生成 last-version.json / versions.json 发布数据
2026-04-14 00:43:21 +08:00

380 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<a-layout class="layout">
<a-layout-header class="header">
<div class="header-content">
<div class="header-left">
<h2>U-Desk</h2>
</div>
<a-tabs v-model:active-key="activeTab" class="header-tabs">
<a-tab-pane
v-for="tab in visibleTabs"
:key="tab.key"
:title="tab.title"
/>
</a-tabs>
<div class="header-actions">
<a-tooltip content="设置">
<a-button type="text" @click="showSettings = true">
<template #icon>
<IconSettings/>
</template>
</a-button>
</a-tooltip>
<a-tooltip :content="isPinned ? '取消置顶' : '窗口置顶'">
<a-button type="text" :class="{ 'pin-active': isPinned }" @click="handleTogglePin">
<template #icon>
<IconPushpin :class="{ pinned: isPinned }"/>
</template>
</a-button>
</a-tooltip>
<ThemeToggle/>
<!-- 窗口控制按钮 -->
<div class="window-controls">
<div class="window-control-btn" @click="handleMinimize" title="最小化">
<svg width="12" height="12" viewBox="0 0 12 12">
<rect x="0" y="5" width="12" height="2" fill="currentColor"/>
</svg>
</div>
<div class="window-control-btn" @click="handleMaximize" :title="isMaximized ? '还原' : '最大化'">
<svg v-if="!isMaximized" width="12" height="12" viewBox="0 0 12 12">
<rect x="1" y="1" width="10" height="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>
<svg v-else width="12" height="12" viewBox="0 0 12 12">
<rect x="2" y="0" width="10" height="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="0" y="2" width="10" height="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>
</div>
<div class="window-control-btn close-btn" @click="handleClose" title="关闭">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M1 1L11 11M11 1L1 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
</div>
</div>
</div>
</a-layout-header>
<a-layout-content class="content">
<!-- 动态渲染 Tab 内容 -->
<!-- 使用 KeepAlive 缓存组件状态避免切换时重新加载 -->
<KeepAlive include="FileSystem,DbCli">
<component :is="getComponent(activeTab)"/>
</KeepAlive>
</a-layout-content>
<!-- 设置抽屉 -->
<SettingsPanel
v-model="showSettings"
:config="configStore.appConfig"
@save="handleSaveConfig"
@open-version-history="showVersionHistory = true"
/>
<!-- 版本历史抽屉 -->
<a-drawer
v-model:visible="showVersionHistory"
:width="720"
:footer="false"
:unmount-on-close="false"
title="版本历史"
>
<VersionHistory />
</a-drawer>
<!-- 升级提示弹窗 -->
<UpdateNotification
v-model="updateStore.showUpdate"
:update-info="updateStore.updateInfo"
@install="updateStore.installUpdate"
/>
</a-layout>
</template>
<script setup lang="ts">
import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
import {IconSettings, IconPushpin} from '@arco-design/web-vue/es/icon'
import MarkdownEditor from './views/markdown-editor/index.vue'
import DbCli from './views/db-cli/index.vue'
import VersionHistory from './views/version/index.vue'
import ThemeToggle from './components/ThemeToggle.vue'
import FileSystem from './components/FileSystem/index.vue'
import SettingsPanel from './components/SettingsPanel.vue'
import UpdateNotification from './components/UpdateNotification.vue'
import {useUpdateStore} from './stores/update'
import {useConfigStore} from './stores/config'
// 存储键
const ACTIVE_TAB_STORAGE_KEY = 'app-active-tab'
// 从 localStorage 恢复上次打开的区域,默认为 'file-system'
const savedTab = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
const activeTab = ref((savedTab === 'user' ? 'file-system' : savedTab) || 'file-system')
const showSettings = ref(false)
const showVersionHistory = ref(false)
const isMaximized = ref(false)
const isPinned = ref(false)
// 使用 stores
const updateStore = useUpdateStore()
const configStore = useConfigStore()
// 应用配置(从 store 获取)
const appConfig = computed(() => configStore.appConfig)
// 可见 Tabs从 store 获取)
const visibleTabs = computed(() => configStore.visibleTabs)
// 保存配置
const handleSaveConfig = async (config) => {
try {
await configStore.saveConfig(config)
showSettings.value = false
// 如果当前激活的 Tab 被隐藏,切换到默认 Tab
if (!config.visibleTabs.includes(activeTab.value)) {
activeTab.value = config.defaultTab
}
} catch (error) {
// 错误已在 store 中处理
console.error('保存配置失败:', error)
}
}
// 加载配置(调用 store 方法)
const loadConfig = async () => {
await configStore.loadConfig()
// 设置默认 Tab
activeTab.value = configStore.defaultTab
}
// 获取组件
const getComponent = (key) => {
const components = {
'file-system': FileSystem,
'db-cli': DbCli,
'markdown-editor': MarkdownEditor
}
return components[key] || null
}
// 组件挂载时加载配置
// 禁止 Ctrl+滚轮缩放
const preventZoom = (e: WheelEvent) => {
if (e.ctrlKey) e.preventDefault()
}
onMounted(() => {
loadConfig()
// 设置更新事件监听
updateStore.setupEventListeners()
// 禁止 Ctrl+滚轮缩放
document.addEventListener('wheel', preventZoom, { passive: false })
// 延迟检查更新(启动后 3 秒,静默模式)
setTimeout(() => {
updateStore.checkForUpdates(true)
}, 3000)
})
// 组件卸载时清理事件监听
onUnmounted(() => {
document.removeEventListener('wheel', preventZoom)
updateStore.removeEventListeners()
// 兜底清除所有 Wails 事件监听器,防止泄漏
window.runtime?.EventsOffAll?.()
})
// 窗口控制方法
const handleMinimize = async () => {
try {
if (window.go?.main?.App?.WindowMinimize) {
await window.go.main.App.WindowMinimize()
}
} catch (error) {
console.error('最小化窗口失败:', error)
}
}
const handleTogglePin = async () => {
try {
if (window.go?.main?.App?.WindowToggleAlwaysOnTop) {
isPinned.value = await window.go.main.App.WindowToggleAlwaysOnTop()
}
} catch (error) {
console.error('切换置顶失败:', error)
}
}
const handleMaximize = async () => {
try {
if (window.go?.main?.App?.WindowMaximize) {
await window.go.main.App.WindowMaximize()
isMaximized.value = await window.go.main.App.WindowIsMaximized()
}
} catch (error) {
console.error('最大化窗口失败:', error)
}
}
const handleClose = async () => {
try {
if (window.go?.main?.App?.WindowClose) {
await window.go.main.App.WindowClose()
}
} catch (error) {
console.error('关闭窗口失败:', error)
}
}
// 监听 activeTab 变化,如果当前 Tab 不在可见列表中,切换到默认 Tab
watch(activeTab, (newTab) => {
// 保存到 localStorage
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, newTab)
// 检查一级 Tab 是否在可见列表中
const isVisible = appConfig.value.visibleTabs.includes(newTab)
if (!isVisible && appConfig.value.visibleTabs.length > 0 && newTab !== appConfig.value.defaultTab) {
activeTab.value = appConfig.value.defaultTab
}
})
</script>
<style scoped>
.layout {
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
user-select: none;
--wails-draggable: drag; /* Wails 拖拽属性 */
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 20px;
}
.header-left {
display: flex;
align-items: center;
min-width: 150px;
--wails-draggable: drag; /* 左侧标题区域可拖拽 */
}
.header-content h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
color: var(--color-text-1);
}
.header-tabs {
flex: 1;
margin-left: 40px;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: 20px;
min-width: 200px;
justify-content: flex-end;
--wails-draggable: no-drag; /* 按钮区域不响应拖拽 */
}
.window-controls {
display: flex;
align-items: center;
margin-left: 12px;
padding-left: 12px;
border-left: 1px solid var(--color-border);
}
.window-control-btn {
width: 40px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.15s;
color: var(--color-text-2);
--wails-draggable: no-drag; /* 窗口控制按钮不响应拖拽 */
}
.window-control-btn:hover {
background: var(--color-fill-2);
color: var(--color-text-1);
}
.window-control-btn.close-btn:hover {
background: #e81123;
color: #ffffff;
}
.pin-active {
color: rgb(var(--primary-6)) !important;
}
.pin-active :deep(svg) {
transform: none !important;
opacity: 1 !important;
}
.header-actions :deep(.arco-icon-pushpin) {
transform: rotate(45deg);
opacity: 0.6;
}
.header-actions :deep(.arco-icon-pushpin.pinned) {
transform: none;
opacity: 1;
}
.window-control-btn svg {
display: block;
pointer-events: none;
}
.content {
flex: 1;
overflow: auto;
background: var(--color-bg-1);
}
</style>
<!-- Wails 拖拽样式 -->
<style>
.header {
--wails-draggable: drag;
}
/* 所有按钮类元素都不可拖拽 */
.header-actions,
.window-control-btn {
--wails-draggable: no-drag;
}
/* tabs 的具体 tab 项不可拖拽,但空白区域可以拖拽 */
.arco-tabs-tab {
--wails-draggable: no-drag;
}
/* Arco Design 按钮不可拖拽 */
.arco-btn,
.arco-select,
.arco-tooltip {
--wails-draggable: no-drag;
}
</style>