重构: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>
This commit is contained in:
373
frontend/src/App.vue
Normal file
373
frontend/src/App.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user