Private
Public Access
1
0
Files
u-desk/frontend/src/App.vue
绝尘 f54bf1c28d 重构:Wails v3 迁移 + 前端目录规范化 + Sidebar滚动优化
- web/ → frontend/ 目录重命名(Wails v3 标准结构)
- main.go: Middleware 修复 custom.js 404 + DevTools 延迟启动
- Sidebar: 收藏夹内部独立滚动 + 帮助区块固定底部
- useFavorites.ts: longPressTimer const→let 修复 TypeError
- App.vue: Arco Tabs padding-top 覆盖
- build: config.yml / Taskfile.yml 对齐官方模板,devtools build tag
- 新增 v3 bindings、vite.config.js、跨平台构建配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 11:03:53 +08:00

374 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" @dblclick="onHeaderDblClick">
<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">
<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 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, type AppConfig} from './stores/config'
import {
WindowMinimize, WindowToggleAlwaysOnTop,
WindowMaximize, WindowIsMaximized, WindowClose
} from './wailsjs/v3-bindings/u-desk/app'
import { OffAll } from '@wailsio/events'
// 存储键
const ACTIVE_TAB_STORAGE_KEY = 'app-active-tab'
// 从 localStorage 恢复上次打开的区域,默认为 'file-system'
// 兼容旧版:'user' 是 v0.2.x 之前的 tab key已废弃需迁移
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: AppConfig) => {
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: string) => {
const components = {
'file-system': FileSystem,
'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 事件监听器,防止泄漏
OffAll()
})
// 窗口控制方法
const handleMinimize = async () => {
try { await WindowMinimize() } catch (e) { console.error('最小化窗口失败:', e) }
}
const handleTogglePin = async () => {
try { isPinned.value = await WindowToggleAlwaysOnTop() } catch (e) { console.error('切换置顶失败:', e) }
}
const handleMaximize = async () => {
try {
await WindowMaximize()
isMaximized.value = await WindowIsMaximized()
} catch (e) { console.error('最大化窗口失败:', e) }
}
// 双击标题栏区域最大化(排除按钮/Tab 区域)
const onHeaderDblClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (target.closest('.window-controls, .header-actions, .arco-tabs')) return
handleMaximize()
}
const handleClose = async () => {
try { await WindowClose() } catch (e) { console.error('关闭窗口失败:', e) }
}
// 监听 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: rgb(var(--danger-6));
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-actions,
.window-control-btn {
--wails-draggable: no-drag;
}
/* tabs 的具体 tab 项不可拖拽,但空白区域可以拖拽 */
.arco-tabs-tab {
--wails-draggable: no-drag;
}
.arco-tabs-content {
padding-top: 0;
}
/* Arco Design 按钮不可拖拽 */
.arco-btn,
.arco-select,
.arco-tooltip {
--wails-draggable: no-drag;
}
/* 桌面应用:禁止 html/body 级别滚动条,所有滚动由内部组件自行处理 */
html, body {
overflow: hidden !important;
}
</style>