featlectron): 实(e现自定义窗口控制和无边框窗口

- 添加窗口最小化、最大化、关闭和状态检测 API- 实现无边框窗口模式并添加窗口控制按钮
- 更新导航栏 UI,添加登录按钮和窗口控制区域
- 调整 Amazon、Rakuten 和 Zebra 仪表板样式
- 移除面包屑导航并调整布局结构
- 更新用户头像样式和 VIP 状态卡片背景渐变
- 添加窗口拖拽区域和用户选择禁用样式
This commit is contained in:
2025-10-24 15:06:47 +08:00
parent 3a76aaa3c0
commit 35c9fc205a
10 changed files with 220 additions and 56 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

@@ -250,6 +250,7 @@ function createWindow() {
width: 1280, width: 1280,
height: 800, height: 800,
show: false, // show: false, //
frame: false,
autoHideMenuBar: true, autoHideMenuBar: true,
icon: getIconPath(), icon: getIconPath(),
backgroundColor: '#f5f5f5', backgroundColor: '#f5f5f5',
@@ -726,6 +727,33 @@ ipcMain.handle('set-launch-config', (event, launchConfig: { autoLaunch: boolean;
// 刷新页面 // 刷新页面
ipcMain.handle('reload', () => mainWindow?.webContents.reload()); ipcMain.handle('reload', () => mainWindow?.webContents.reload());
// 窗口控制 API
ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.minimize();
}
});
ipcMain.handle('window-maximize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
}
});
ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
});
ipcMain.handle('window-is-maximized', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false;
});
async function getFileSize(url: string): Promise<number> { async function getFileSize(url: string): Promise<number> {
return new Promise((resolve) => { return new Promise((resolve) => {

View File

@@ -37,6 +37,12 @@ const electronAPI = {
// 刷新页面 API // 刷新页面 API
reload: () => ipcRenderer.invoke('reload'), reload: () => ipcRenderer.invoke('reload'),
// 窗口控制 API
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
windowClose: () => ipcRenderer.invoke('window-close'),
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
onDownloadProgress: (callback: (progress: any) => void) => { onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.removeAllListeners('download-progress') ipcRenderer.removeAllListeners('download-progress')
ipcRenderer.on('download-progress', (event, progress) => callback(progress)) ipcRenderer.on('download-progress', (event, progress) => callback(progress))

View File

@@ -629,7 +629,8 @@ onUnmounted(() => {
@open-device="openDeviceManager" @open-device="openDeviceManager"
@open-settings="openSettings" @open-settings="openSettings"
@open-account-manager="openAccountManager" @open-account-manager="openAccountManager"
@check-update="handleCheckUpdate"/> @check-update="handleCheckUpdate"
@show-login="showAuthDialog = true"/>
<div class="content-body"> <div class="content-body">
<div <div
class="dashboard-home" class="dashboard-home"
@@ -803,14 +804,12 @@ onUnmounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid #e8eaec;
margin: 0 0 12px 0; margin: 0 0 12px 0;
} }
.user-avatar img { .user-avatar img {
width: 50px; width: 90px;
height: 50px;
border-radius: 50%;
object-fit: contain; object-fit: contain;
background: #ffffff; background: #ffffff;
} }
@@ -997,14 +996,13 @@ onUnmounted(() => {
.vip-status-card { .vip-status-card {
margin-top: auto; margin-top: auto;
width: 100%; width: 100%;
min-height: 64px;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 12px; padding: 10px 12px;
box-sizing: border-box; box-sizing: border-box;
background: linear-gradient(135deg, #FFFAF0 0%, #FFE4B5 50%, #FFD700 100%); background: linear-gradient(90deg, #FFF7CC 0%, #FFEB80 100%);
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15); box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
@@ -1025,7 +1023,7 @@ onUnmounted(() => {
.vip-status-card.vip-active, .vip-status-card.vip-active,
.vip-status-card.vip-normal, .vip-status-card.vip-normal,
.vip-status-card.vip-warning { .vip-status-card.vip-warning {
background: linear-gradient(135deg, #FFFAF0 0%, #FFE4B5 50%, #FFD700 100%); background: linear-gradient(90deg, #FFF7CC 0%, #FFEB80 100%);
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15); box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
} }

View File

@@ -492,8 +492,8 @@ onMounted(async () => {
</template> </template>
<style scoped> <style scoped>
.amazon-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; } .amazon-root { position: absolute; inset: 0; background: #fff; box-sizing: border-box; }
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; } .main-container { height: 100%; display: flex; flex-direction: column; padding: 12px; box-sizing: border-box; }
/* 顶部标签栏 */ /* 顶部标签栏 */
.top-tabs { display: flex; margin-bottom: 8px; } .top-tabs { display: flex; margin-bottom: 8px; }
@@ -587,7 +587,7 @@ onMounted(async () => {
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; } .table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; } .spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #fff; display: flex; justify-content: flex-end; margin-top: 8px; } .pagination-fixed { flex-shrink: 0; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end; }
.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; } .pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; }
.empty-tip { text-align: center; color: #909399; padding: 16px 0; } .empty-tip { text-align: center; color: #909399; padding: 16px 0; }
.import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; } .import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref, onMounted } from 'vue'
import { ArrowLeft, ArrowRight, Refresh, Setting } from '@element-plus/icons-vue' import { Refresh, Setting, Minus, CloseBold } from '@element-plus/icons-vue'
interface Props { interface Props {
canGoBack: boolean canGoBack: boolean
@@ -20,15 +20,31 @@ interface Emits {
(e: 'open-settings'): void (e: 'open-settings'): void
(e: 'open-account-manager'): void (e: 'open-account-manager'): void
(e: 'check-update'): void (e: 'check-update'): void
(e: 'show-login'): void
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const isMaximized = ref(false)
const displayUsername = computed(() => { const displayUsername = computed(() => {
return props.isAuthenticated ? props.currentUsername : '未登录' return props.isAuthenticated ? props.currentUsername : '未登录'
}) })
async function handleMinimize() {
await (window as any).electronAPI.windowMinimize()
}
async function handleMaximize() {
await (window as any).electronAPI.windowMaximize()
isMaximized.value = await (window as any).electronAPI.windowIsMaximized()
}
async function handleClose() {
await (window as any).electronAPI.windowClose()
}
function handleCommand(command: string) { function handleCommand(command: string) {
switch (command) { switch (command) {
case 'logout': case 'logout':
@@ -48,6 +64,10 @@ function handleCommand(command: string) {
break break
} }
} }
onMounted(async () => {
isMaximized.value = await (window as any).electronAPI.windowIsMaximized()
})
</script> </script>
<template> <template>
@@ -55,21 +75,17 @@ function handleCommand(command: string) {
<div class="navbar-left"> <div class="navbar-left">
<div class="nav-controls"> <div class="nav-controls">
<button class="nav-btn" title="后退" @click="$emit('go-back')" :disabled="!canGoBack"> <button class="nav-btn" title="后退" @click="$emit('go-back')" :disabled="!canGoBack">
<el-icon><ArrowLeft /></el-icon> <svg viewBox="0 0 24 24" class="arrow-icon">
<path d="M15 18l-6-6 6-6" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button> </button>
<button class="nav-btn" title="前进" @click="$emit('go-forward')" :disabled="!canGoForward"> <button class="nav-btn" title="前进" @click="$emit('go-forward')" :disabled="!canGoForward">
<el-icon><ArrowRight /></el-icon> <svg viewBox="0 0 24 24" class="arrow-icon">
<path d="M9 18l6-6-6-6" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button> </button>
</div> </div>
</div>
<div class="navbar-center">
<div class="breadcrumbs">
<span>首页</span>
<span class="separator">></span>
<span>{{ activeMenu }}</span>
</div>
</div>
<div class="navbar-right">
<button class="nav-btn-round" title="刷新" @click="$emit('reload')"> <button class="nav-btn-round" title="刷新" @click="$emit('reload')">
<el-icon><Refresh /></el-icon> <el-icon><Refresh /></el-icon>
</button> </button>
@@ -102,6 +118,34 @@ function handleCommand(command: string) {
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<!-- 登录/注册按钮 -->
<button v-if="!isAuthenticated" class="login-btn" @click="$emit('show-login')">
登录/注册
</button>
</div>
<div class="navbar-center">
<div class="breadcrumbs">
<span>首页</span>
<span class="separator">/</span>
<span>{{ activeMenu }}</span>
</div>
</div>
<div class="navbar-right">
<!-- 窗口控制按钮 -->
<div class="window-controls">
<button class="window-btn window-btn-minimize" title="最小化" @click="handleMinimize">
<el-icon><Minus /></el-icon>
</button>
<button class="window-btn window-btn-maximize" title="最大化" @click="handleMaximize">
<svg viewBox="0 0 12 12" class="maximize-icon">
<rect x="2" y="2" width="8" height="8" stroke="currentColor" fill="none" stroke-width="1"/>
</svg>
</button>
<button class="window-btn window-btn-close" title="关闭" @click="handleClose">
<el-icon><CloseBold /></el-icon>
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -116,12 +160,16 @@ function handleCommand(command: string) {
background: #ffffff; background: #ffffff;
border-bottom: 1px solid #e8eaec; border-bottom: 1px solid #e8eaec;
box-shadow: 0 1px 3px rgba(0,0,0,0.03); box-shadow: 0 1px 3px rgba(0,0,0,0.03);
-webkit-app-region: drag;
user-select: none;
} }
.navbar-left { .navbar-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
flex: 0 0 auto; flex: 0 0 auto;
-webkit-app-region: no-drag;
} }
.navbar-center { .navbar-center {
@@ -135,35 +183,49 @@ function handleCommand(command: string) {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex: 0 0 auto; flex: 0 0 auto;
-webkit-app-region: no-drag;
} }
.nav-controls { .nav-controls {
display: flex; display: flex;
border: 1px solid #dcdfe6; gap: 4px;
border-radius: 4px;
overflow: hidden;
} }
.nav-btn { .nav-btn {
width: 32px; width: 28px;
height: 28px; height: 28px;
border: none; border: none;
background: #fff; background: transparent;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 16px;
color: #606266; color: #606266;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s ease; transition: all 0.2s ease;
outline: none; outline: none;
border-radius: 4px;
}
.arrow-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.arrow-icon path {
stroke: currentColor;
} }
.nav-btn:hover:not(:disabled) { .nav-btn:hover:not(:disabled) {
background: #f5f7fa; background: rgba(0, 0, 0, 0.05);
color: #409EFF; color: #409EFF;
} }
.nav-btn:hover:not(:disabled) .arrow-icon path {
stroke: #409EFF;
}
.nav-btn:focus, .nav-btn:focus,
.nav-btn:active { .nav-btn:active {
outline: none; outline: none;
@@ -172,23 +234,22 @@ function handleCommand(command: string) {
.nav-btn:disabled { .nav-btn:disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; background: transparent;
background: #f5f5f5; color: #d0d0d0;
color: #c0c4cc;
} }
.nav-btn:not(:last-child) { .nav-btn:disabled .arrow-icon path {
border-right: 1px solid #dcdfe6; stroke: #d0d0d0;
} }
.nav-btn-round { .nav-btn-round {
width: 28px; width: 28px;
height: 28px; height: 28px;
border: 1px solid #dcdfe6; border: none;
border-radius: 50%; border-radius: 4px;
background: #fff; background: transparent;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 16px;
color: #606266; color: #606266;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -198,9 +259,8 @@ function handleCommand(command: string) {
} }
.nav-btn-round:hover { .nav-btn-round:hover {
background: #f5f7fa; background: rgba(0, 0, 0, 0.05);
color: #409EFF; color: #409EFF;
border-color: #c6e2ff;
} }
.nav-btn-round:focus, .nav-btn-round:focus,
@@ -216,9 +276,82 @@ function handleCommand(command: string) {
} }
.separator { .separator {
margin: 0 8px; margin: 0 6px;
color: #c0c4cc; color: #c0c4cc;
font-size: 12px; font-size: 14px;
}
/* 窗口控制按钮 */
.window-controls {
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
}
.window-btn {
width: 32px;
height: 28px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #606266;
transition: all 0.2s ease;
outline: none;
padding: 0;
margin: 0;
border-radius: 4px;
}
.window-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
.window-btn:active {
background: rgba(0, 0, 0, 0.1);
}
.window-btn-close:hover {
background: #e81123;
color: #ffffff;
}
.window-btn-close:active {
background: #f1707a;
}
.maximize-icon {
width: 12px;
height: 12px;
}
/* 登录/注册按钮 */
.login-btn {
height: 28px;
padding: 0 12px;
border: none;
border-radius: 4px;
background: #1677FF;
color: #fff;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
white-space: nowrap;
margin-left: 8px;
}
.login-btn:hover {
background: #0d5ed6;
}
.login-btn:active {
background: #0c54c2;
} }
</style> </style>
@@ -229,7 +362,6 @@ function handleCommand(command: string) {
padding: 4px 0 !important; padding: 4px 0 !important;
border-radius: 12px !important; border-radius: 12px !important;
margin-top: 4px !important; margin-top: 4px !important;
margin-right: 8px !important;
} }
.settings-dropdown .username-item { .settings-dropdown .username-item {

View File

@@ -651,19 +651,16 @@ onMounted(loadLatest)
.rakuten-root { .rakuten-root {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: #f5f5f5; background: #fff;
padding: 12px;
box-sizing: border-box; box-sizing: border-box;
} }
.main-container { .main-container {
background: #fff;
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 12px;
box-sizing: border-box;
} }
.body-layout { display: flex; gap: 12px; height: 100%; } .body-layout { display: flex; gap: 12px; height: 100%; }
@@ -859,11 +856,10 @@ onMounted(loadLatest)
.pagination-fixed { .pagination-fixed {
flex-shrink: 0; flex-shrink: 0;
padding: 8px 12px; padding: 8px 12px 0 12px;
background: #fff; background: #fff;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 8px;
} }
.pagination-fixed :deep(.el-pager li.is-active) { .pagination-fixed :deep(.el-pager li.is-active) {

View File

@@ -575,8 +575,8 @@ export default {
</script> </script>
<style scoped> <style scoped>
.zebra-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; } .zebra-root { position: absolute; inset: 0; background: #fff; box-sizing: border-box; }
.layout { background: #fff; border-radius: 4px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; } .layout { height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; padding: 12px; box-sizing: border-box; }
.aside { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease; } .aside { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease; }
.aside.collapsed { width: 56px; overflow: hidden; } .aside.collapsed { width: 56px; overflow: hidden; }
.aside-header { display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; } .aside-header { display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
@@ -648,7 +648,7 @@ export default {
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; } .table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; } .spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pagination-fixed { position: sticky; bottom: 0; z-index: 2; padding: 8px 12px; background: #fff; display: flex; justify-content: flex-end; margin-top: 8px; } .pagination-fixed { position: sticky; bottom: 0; z-index: 2; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end; }
.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; } .pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; }
.tag { display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; } .tag { display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; } .empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -14,6 +14,10 @@ export default interface ElectronApi {
getUpdateStatus: () => Promise<{ downloadedFilePath: string | null; isDownloading: boolean; downloadProgress: any; isPackaged: boolean }> getUpdateStatus: () => Promise<{ downloadedFilePath: string | null; isDownloading: boolean; downloadProgress: any; isPackaged: boolean }>
onUpdateLog: (callback: (log: string) => void) => void onUpdateLog: (callback: (log: string) => void) => void
removeUpdateLogListener: () => void removeUpdateLogListener: () => void
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<void>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
} }
declare global { declare global {