Bläddra i källkod

Implement window control features and enhance UI in client views

- Added draggable title bar and window control buttons (minimize, maximize, close) to Login, Register, and ServerConfig views.
- Updated styles for window controls and background decorations for improved aesthetics.
- Integrated window state management with Electron API for maximizing and minimizing windows.
- Enhanced user experience with visual feedback on button interactions and improved layout consistency across views.
Ethanfly 17 timmar sedan
förälder
incheckning
b272d9d177

+ 6 - 11
client/dist-electron/main.js

@@ -7,19 +7,14 @@ let tray = null;
 let isQuitting = false;
 const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
 function getIconPath() {
-  return VITE_DEV_SERVER_URL ? join(__dirname, "../public/favicon.svg") : join(__dirname, "../dist/favicon.svg");
+  return VITE_DEV_SERVER_URL ? join(__dirname, "../public/icons/icon-256.png") : join(__dirname, "../dist/icons/icon-256.png");
+}
+function getTrayIconPath() {
+  return VITE_DEV_SERVER_URL ? join(__dirname, "../public/icons/tray-icon.png") : join(__dirname, "../dist/icons/tray-icon.png");
 }
 function createTrayIcon() {
-  const iconSize = 16;
-  const canvas = `
-    <svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 512 512">
-      <rect x="32" y="32" width="448" height="448" rx="96" fill="#4f8cff"/>
-      <circle cx="256" cy="256" r="100" fill="#fff"/>
-      <path d="M 228 190 L 228 322 L 330 256 Z" fill="#4f8cff"/>
-    </svg>
-  `;
-  const dataUrl = `data:image/svg+xml;base64,${Buffer.from(canvas).toString("base64")}`;
-  return nativeImage.createFromDataURL(dataUrl);
+  const trayIconPath = getTrayIconPath();
+  return nativeImage.createFromPath(trayIconPath);
 }
 function createTray() {
   const trayIcon = createTrayIcon();

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
client/dist-electron/main.js.map


+ 95 - 1
client/src/views/Login/index.vue

@@ -1,5 +1,28 @@
 <template>
   <div class="login-container">
+    <!-- 顶部可拖动标题栏 -->
+    <div class="drag-region">
+      <div class="window-controls">
+        <button class="window-btn minimize" @click="handleMinimize" title="最小化">
+          <svg viewBox="0 0 12 12"><rect y="5" width="12" height="2" fill="currentColor"/></svg>
+        </button>
+        <button class="window-btn maximize" @click="handleMaximize" :title="isMaximized ? '还原' : '最大化'">
+          <svg v-if="isMaximized" viewBox="0 0 12 12">
+            <rect x="1.5" y="3" width="7" height="7" stroke="currentColor" stroke-width="1.5" fill="none"/>
+            <path d="M3.5 3V1.5H11V9H9.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
+          </svg>
+          <svg v-else viewBox="0 0 12 12">
+            <rect x="1" y="1" width="10" height="10" stroke="currentColor" stroke-width="1.5" fill="none"/>
+          </svg>
+        </button>
+        <button class="window-btn close" @click="handleClose" title="关闭">
+          <svg viewBox="0 0 12 12">
+            <path d="M1 1L11 11M1 11L11 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+          </svg>
+        </button>
+      </div>
+    </div>
+    
     <!-- 背景装饰 -->
     <div class="bg-decoration">
       <div class="circle circle-1"></div>
@@ -79,7 +102,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { User, Lock, Link, VideoPlay } from '@element-plus/icons-vue';
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
@@ -92,6 +115,30 @@ const serverStore = useServerStore();
 
 const formRef = ref<FormInstance>();
 const loading = ref(false);
+const isMaximized = ref(false);
+
+// 窗口控制
+function handleMinimize() {
+  window.electronAPI?.minimizeWindow?.();
+}
+
+function handleMaximize() {
+  window.electronAPI?.maximizeWindow?.();
+}
+
+function handleClose() {
+  window.electronAPI?.closeWindow?.();
+}
+
+// 监听窗口最大化状态
+onMounted(async () => {
+  if (window.electronAPI?.isMaximized) {
+    isMaximized.value = await window.electronAPI.isMaximized();
+  }
+  window.electronAPI?.onMaximizedChange?.((maximized: boolean) => {
+    isMaximized.value = maximized;
+  });
+});
 
 const form = reactive({
   username: '',
@@ -140,6 +187,53 @@ async function handleLogin() {
   overflow: hidden;
 }
 
+// 顶部可拖动区域
+.drag-region {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 32px;
+  -webkit-app-region: drag;
+  z-index: 999;
+}
+
+// 窗口控制按钮
+.window-controls {
+  position: absolute;
+  top: 0;
+  right: 0;
+  display: flex;
+  -webkit-app-region: no-drag;
+  
+  .window-btn {
+    width: 46px;
+    height: 32px;
+    border: none;
+    background: transparent;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: background 0.15s;
+    color: $text-secondary;
+    
+    svg {
+      width: 12px;
+      height: 12px;
+    }
+    
+    &:hover {
+      background: rgba(0, 0, 0, 0.06);
+    }
+    
+    &.close:hover {
+      background: #e81123;
+      color: #fff;
+    }
+  }
+}
+
 // 背景装饰
 .bg-decoration {
   position: absolute;

+ 222 - 9
client/src/views/Register/index.vue

@@ -1,7 +1,40 @@
 <template>
   <div class="register-container">
+    <!-- 顶部可拖动标题栏 -->
+    <div class="drag-region">
+      <div class="window-controls">
+        <button class="window-btn minimize" @click="handleMinimize" title="最小化">
+          <svg viewBox="0 0 12 12"><rect y="5" width="12" height="2" fill="currentColor"/></svg>
+        </button>
+        <button class="window-btn maximize" @click="handleMaximize" :title="isMaximized ? '还原' : '最大化'">
+          <svg v-if="isMaximized" viewBox="0 0 12 12">
+            <rect x="1.5" y="3" width="7" height="7" stroke="currentColor" stroke-width="1.5" fill="none"/>
+            <path d="M3.5 3V1.5H11V9H9.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
+          </svg>
+          <svg v-else viewBox="0 0 12 12">
+            <rect x="1" y="1" width="10" height="10" stroke="currentColor" stroke-width="1.5" fill="none"/>
+          </svg>
+        </button>
+        <button class="window-btn close" @click="handleClose" title="关闭">
+          <svg viewBox="0 0 12 12">
+            <path d="M1 1L11 11M1 11L11 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+          </svg>
+        </button>
+      </div>
+    </div>
+    
+    <!-- 背景装饰 -->
+    <div class="bg-decoration">
+      <div class="circle circle-1"></div>
+      <div class="circle circle-2"></div>
+      <div class="circle circle-3"></div>
+    </div>
+    
     <div class="register-card">
       <div class="register-header">
+        <div class="logo">
+          <el-icon><UserFilled /></el-icon>
+        </div>
         <h1>创建账号</h1>
         <p>注册新账号以使用系统</p>
       </div>
@@ -77,9 +110,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
-import { User, Lock, Message } from '@element-plus/icons-vue';
+import { User, Lock, Message, UserFilled } from '@element-plus/icons-vue';
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
 import { useAuthStore } from '@/stores/auth';
 
@@ -88,6 +121,30 @@ const authStore = useAuthStore();
 
 const formRef = ref<FormInstance>();
 const loading = ref(false);
+const isMaximized = ref(false);
+
+// 窗口控制
+function handleMinimize() {
+  window.electronAPI?.minimizeWindow?.();
+}
+
+function handleMaximize() {
+  window.electronAPI?.maximizeWindow?.();
+}
+
+function handleClose() {
+  window.electronAPI?.closeWindow?.();
+}
+
+// 监听窗口最大化状态
+onMounted(async () => {
+  if (window.electronAPI?.isMaximized) {
+    isMaximized.value = await window.electronAPI.isMaximized();
+  }
+  window.electronAPI?.onMaximizedChange?.((maximized: boolean) => {
+    isMaximized.value = maximized;
+  });
+});
 
 const form = reactive({
   username: '',
@@ -153,41 +210,197 @@ async function handleRegister() {
   display: flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
+  position: relative;
+  overflow: hidden;
+}
+
+// 顶部可拖动区域
+.drag-region {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 32px;
+  -webkit-app-region: drag;
+  z-index: 999;
+}
+
+// 窗口控制按钮
+.window-controls {
+  position: absolute;
+  top: 0;
+  right: 0;
+  display: flex;
+  -webkit-app-region: no-drag;
+  
+  .window-btn {
+    width: 46px;
+    height: 32px;
+    border: none;
+    background: transparent;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: background 0.15s;
+    color: $text-secondary;
+    
+    svg {
+      width: 12px;
+      height: 12px;
+    }
+    
+    &:hover {
+      background: rgba(0, 0, 0, 0.06);
+    }
+    
+    &.close:hover {
+      background: #e81123;
+      color: #fff;
+    }
+  }
+}
+
+// 背景装饰
+.bg-decoration {
+  position: absolute;
+  inset: 0;
+  overflow: hidden;
+  pointer-events: none;
+  
+  .circle {
+    position: absolute;
+    border-radius: 50%;
+    opacity: 0.5;
+  }
+  
+  .circle-1 {
+    width: 400px;
+    height: 400px;
+    background: linear-gradient(135deg, rgba(79, 140, 255, 0.2), rgba(79, 140, 255, 0.05));
+    top: -100px;
+    right: -100px;
+  }
+  
+  .circle-2 {
+    width: 300px;
+    height: 300px;
+    background: linear-gradient(135deg, rgba(250, 112, 154, 0.15), rgba(254, 225, 64, 0.1));
+    bottom: -80px;
+    left: -80px;
+  }
+  
+  .circle-3 {
+    width: 200px;
+    height: 200px;
+    background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.1));
+    top: 50%;
+    left: 10%;
+    transform: translateY(-50%);
+  }
 }
 
 .register-card {
-  width: 400px;
-  padding: 40px;
+  width: 420px;
+  padding: 48px 40px;
   background: #fff;
-  border-radius: 12px;
-  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
+  border-radius: $radius-xl;
+  box-shadow: $shadow-lg;
+  position: relative;
+  z-index: 1;
+  border: 1px solid rgba(255, 255, 255, 0.8);
 }
 
 .register-header {
   text-align: center;
-  margin-bottom: 30px;
+  margin-bottom: 36px;
+  
+  .logo {
+    width: 64px;
+    height: 64px;
+    margin: 0 auto 20px;
+    border-radius: $radius-lg;
+    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 8px 24px rgba(79, 172, 254, 0.3);
+    
+    .el-icon {
+      font-size: 32px;
+      color: #fff;
+    }
+  }
   
   h1 {
-    margin: 0 0 10px;
+    margin: 0 0 12px;
     font-size: 24px;
+    font-weight: 700;
     color: $text-primary;
   }
   
   p {
     margin: 0;
     color: $text-secondary;
+    font-size: 14px;
   }
 }
 
 .register-form {
+  :deep(.el-input__wrapper) {
+    border-radius: $radius-base;
+    box-shadow: 0 0 0 1px $border-light inset;
+    transition: all 0.2s;
+    
+    &:hover {
+      box-shadow: 0 0 0 1px $primary-color inset;
+    }
+    
+    &.is-focus {
+      box-shadow: 0 0 0 1px $primary-color inset, 0 0 0 3px rgba($primary-color, 0.1);
+    }
+  }
+  
+  :deep(.el-input__inner) {
+    height: 44px;
+    font-size: 15px;
+  }
+  
+  :deep(.el-input__prefix) {
+    color: $text-secondary;
+  }
+  
   .register-btn {
     width: 100%;
+    height: 48px;
+    font-size: 16px;
+    font-weight: 600;
+    border-radius: $radius-base;
+    background: linear-gradient(135deg, $primary-color 0%, #3a7bd5 100%);
+    border: none;
+    box-shadow: 0 4px 16px rgba($primary-color, 0.3);
+    transition: all 0.3s;
+    
+    &:hover {
+      transform: translateY(-1px);
+      box-shadow: 0 6px 20px rgba($primary-color, 0.4);
+    }
+    
+    &:active {
+      transform: translateY(0);
+    }
   }
 }
 
 .register-footer {
   text-align: center;
   color: $text-secondary;
+  font-size: 14px;
+  margin-top: 8px;
+  
+  .el-button {
+    font-weight: 500;
+  }
 }
 </style>

+ 333 - 30
client/src/views/ServerConfig/index.vue

@@ -1,8 +1,43 @@
 <template>
   <div class="server-config-container">
+    <!-- 顶部可拖动标题栏 -->
+    <div class="drag-region">
+      <div class="window-controls">
+        <button class="window-btn minimize" @click="handleMinimize" title="最小化">
+          <svg viewBox="0 0 12 12"><rect y="5" width="12" height="2" fill="currentColor"/></svg>
+        </button>
+        <button class="window-btn maximize" @click="handleMaximize" :title="isMaximized ? '还原' : '最大化'">
+          <svg v-if="isMaximized" viewBox="0 0 12 12">
+            <rect x="1.5" y="3" width="7" height="7" stroke="currentColor" stroke-width="1.5" fill="none"/>
+            <path d="M3.5 3V1.5H11V9H9.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
+          </svg>
+          <svg v-else viewBox="0 0 12 12">
+            <rect x="1" y="1" width="10" height="10" stroke="currentColor" stroke-width="1.5" fill="none"/>
+          </svg>
+        </button>
+        <button class="window-btn close" @click="handleClose" title="关闭">
+          <svg viewBox="0 0 12 12">
+            <path d="M1 1L11 11M1 11L11 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+          </svg>
+        </button>
+      </div>
+    </div>
+    
+    <!-- 背景装饰 -->
+    <div class="bg-decoration">
+      <div class="circle circle-1"></div>
+      <div class="circle circle-2"></div>
+      <div class="circle circle-3"></div>
+    </div>
+    
     <div class="config-card">
-      <h1>服务器配置</h1>
-      <p class="subtitle">配置后端服务器地址以连接系统</p>
+      <div class="config-header">
+        <div class="logo">
+          <el-icon><Setting /></el-icon>
+        </div>
+        <h1>服务器配置</h1>
+        <p>配置后端服务器地址以连接系统</p>
+      </div>
       
       <!-- 服务器列表 -->
       <div class="server-list" v-if="serverStore.servers.length > 0">
@@ -44,28 +79,37 @@
         class="config-form"
       >
         <el-form-item label="服务器名称" prop="name">
-          <el-input v-model="form.name" placeholder="例如:本地服务器" />
+          <el-input 
+            v-model="form.name" 
+            placeholder="例如:本地服务器" 
+            size="large"
+            :prefix-icon="Connection"
+          />
         </el-form-item>
         
         <el-form-item label="服务器地址" prop="url">
-          <el-input v-model="form.url" placeholder="例如:http://localhost:3000">
-            <template #append>
-              <el-button @click="testConnection" :loading="testing">
-                测试连接
-              </el-button>
-            </template>
-          </el-input>
+          <div class="url-input-row">
+            <el-input 
+              v-model="form.url" 
+              placeholder="例如:http://localhost:3000" 
+              size="large"
+              :prefix-icon="Link"
+            />
+            <el-button @click="testConnection" :loading="testing" class="test-btn">
+              测试连接
+            </el-button>
+          </div>
         </el-form-item>
         
-        <el-form-item>
+        <el-form-item class="checkbox-row">
           <el-checkbox v-model="form.isDefault">设为默认服务器</el-checkbox>
         </el-form-item>
         
-        <el-form-item>
-          <el-button type="primary" @click="addServer" :loading="loading">
+        <el-form-item class="action-row">
+          <el-button type="primary" @click="addServer" :loading="loading" class="primary-btn">
             添加服务器
           </el-button>
-          <el-button v-if="serverStore.isConfigured" @click="goBack">
+          <el-button v-if="serverStore.isConfigured" @click="goBack" class="back-btn">
             返回
           </el-button>
         </el-form-item>
@@ -84,8 +128,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
+import { Setting, Connection, Link } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import { useServerStore } from '@/stores/server';
 import { useAuthStore } from '@/stores/auth';
@@ -98,6 +143,30 @@ const formRef = ref<FormInstance>();
 const loading = ref(false);
 const testing = ref(false);
 const connectionResult = ref<boolean | null>(null);
+const isMaximized = ref(false);
+
+// 窗口控制
+function handleMinimize() {
+  window.electronAPI?.minimizeWindow?.();
+}
+
+function handleMaximize() {
+  window.electronAPI?.maximizeWindow?.();
+}
+
+function handleClose() {
+  window.electronAPI?.closeWindow?.();
+}
+
+// 监听窗口最大化状态
+onMounted(async () => {
+  if (window.electronAPI?.isMaximized) {
+    isMaximized.value = await window.electronAPI.isMaximized();
+  }
+  window.electronAPI?.onMaximizedChange?.((maximized: boolean) => {
+    isMaximized.value = maximized;
+  });
+});
 
 const form = reactive({
   name: '',
@@ -215,28 +284,142 @@ function goBack() {
   display: flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
+  position: relative;
+  overflow: hidden;
   padding: 20px;
 }
 
+// 顶部可拖动区域
+.drag-region {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 32px;
+  -webkit-app-region: drag;
+  z-index: 999;
+}
+
+// 窗口控制按钮
+.window-controls {
+  position: absolute;
+  top: 0;
+  right: 0;
+  display: flex;
+  -webkit-app-region: no-drag;
+  
+  .window-btn {
+    width: 46px;
+    height: 32px;
+    border: none;
+    background: transparent;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: background 0.15s;
+    color: $text-secondary;
+    
+    svg {
+      width: 12px;
+      height: 12px;
+    }
+    
+    &:hover {
+      background: rgba(0, 0, 0, 0.06);
+    }
+    
+    &.close:hover {
+      background: #e81123;
+      color: #fff;
+    }
+  }
+}
+
+// 背景装饰
+.bg-decoration {
+  position: absolute;
+  inset: 0;
+  overflow: hidden;
+  pointer-events: none;
+  
+  .circle {
+    position: absolute;
+    border-radius: 50%;
+    opacity: 0.5;
+  }
+  
+  .circle-1 {
+    width: 400px;
+    height: 400px;
+    background: linear-gradient(135deg, rgba(79, 140, 255, 0.2), rgba(79, 140, 255, 0.05));
+    top: -100px;
+    right: -100px;
+  }
+  
+  .circle-2 {
+    width: 300px;
+    height: 300px;
+    background: linear-gradient(135deg, rgba(250, 112, 154, 0.15), rgba(254, 225, 64, 0.1));
+    bottom: -80px;
+    left: -80px;
+  }
+  
+  .circle-3 {
+    width: 200px;
+    height: 200px;
+    background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.1));
+    top: 50%;
+    left: 10%;
+    transform: translateY(-50%);
+  }
+}
+
 .config-card {
-  width: 500px;
+  width: 480px;
   max-width: 100%;
-  padding: 40px;
+  padding: 48px 40px;
   background: #fff;
-  border-radius: 12px;
-  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
+  border-radius: $radius-xl;
+  box-shadow: $shadow-lg;
+  position: relative;
+  z-index: 1;
+  border: 1px solid rgba(255, 255, 255, 0.8);
+}
+
+.config-header {
+  text-align: center;
+  margin-bottom: 32px;
+  
+  .logo {
+    width: 64px;
+    height: 64px;
+    margin: 0 auto 20px;
+    border-radius: $radius-lg;
+    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 8px 24px rgba(79, 172, 254, 0.3);
+    
+    .el-icon {
+      font-size: 32px;
+      color: #fff;
+    }
+  }
   
   h1 {
-    margin: 0 0 8px;
+    margin: 0 0 12px;
     font-size: 24px;
-    text-align: center;
+    font-weight: 700;
+    color: $text-primary;
   }
   
-  .subtitle {
-    margin: 0 0 24px;
-    text-align: center;
+  p {
+    margin: 0;
     color: $text-secondary;
+    font-size: 14px;
   }
 }
 
@@ -247,26 +430,30 @@ function goBack() {
     display: flex;
     align-items: center;
     justify-content: space-between;
-    padding: 12px 16px;
+    padding: 14px 18px;
     border: 1px solid $border-light;
-    border-radius: 8px;
-    margin-bottom: 8px;
+    border-radius: $radius-base;
+    margin-bottom: 10px;
     cursor: pointer;
     transition: all 0.2s;
+    background: $bg-light;
     
     &:hover {
       border-color: $primary-color;
+      background: #fff;
     }
     
     &.active {
       border-color: $primary-color;
       background: rgba($primary-color, 0.05);
+      box-shadow: 0 0 0 3px rgba($primary-color, 0.08);
     }
     
     .server-info {
       .server-name {
-        font-weight: 500;
+        font-weight: 600;
         color: $text-primary;
+        font-size: 15px;
       }
       
       .server-url {
@@ -284,7 +471,123 @@ function goBack() {
   }
 }
 
+.config-form {
+  :deep(.el-form-item__label) {
+    font-weight: 500;
+    color: $text-primary;
+  }
+  
+  :deep(.el-input__wrapper) {
+    border-radius: $radius-base;
+    box-shadow: 0 0 0 1px $border-light inset;
+    transition: all 0.2s;
+    
+    &:hover {
+      box-shadow: 0 0 0 1px $primary-color inset;
+    }
+    
+    &.is-focus {
+      box-shadow: 0 0 0 1px $primary-color inset, 0 0 0 3px rgba($primary-color, 0.1);
+    }
+  }
+  
+  :deep(.el-input__inner) {
+    height: 44px;
+    font-size: 15px;
+  }
+  
+  :deep(.el-input__prefix) {
+    color: $text-secondary;
+  }
+  
+  // URL输入行样式
+  .url-input-row {
+    display: flex;
+    gap: 12px;
+    width: 100%;
+    
+    .el-input {
+      flex: 1;
+    }
+    
+    .test-btn {
+      height: 44px;
+      padding: 0 20px;
+      border-radius: $radius-base;
+      background: $bg-base;
+      border: 1px solid $border-light;
+      color: $primary-color;
+      font-weight: 500;
+      white-space: nowrap;
+      
+      &:hover {
+        background: $primary-color-light;
+        border-color: $primary-color;
+      }
+    }
+  }
+  
+  .checkbox-row {
+    margin-bottom: 24px;
+    
+    :deep(.el-checkbox__label) {
+      color: $text-secondary;
+    }
+  }
+  
+  .action-row {
+    margin-bottom: 0;
+    
+    .primary-btn {
+      height: 44px;
+      padding: 0 28px;
+      font-size: 15px;
+      font-weight: 600;
+      border-radius: $radius-base;
+      background: linear-gradient(135deg, $primary-color 0%, #3a7bd5 100%);
+      border: none;
+      box-shadow: 0 4px 16px rgba($primary-color, 0.3);
+      transition: all 0.3s;
+      
+      &:hover {
+        transform: translateY(-1px);
+        box-shadow: 0 6px 20px rgba($primary-color, 0.4);
+      }
+      
+      &:active {
+        transform: translateY(0);
+      }
+    }
+    
+    .back-btn {
+      height: 44px;
+      padding: 0 28px;
+      font-size: 15px;
+      font-weight: 500;
+      border-radius: $radius-base;
+      border: 1px solid $border-base;
+      background: #fff;
+      color: $text-regular;
+      transition: all 0.2s;
+      
+      &:hover {
+        border-color: $primary-color;
+        color: $primary-color;
+      }
+    }
+  }
+}
+
+:deep(.el-divider) {
+  margin: 24px 0;
+  border-color: $border-lighter;
+}
+
 .connection-status {
-  margin-top: 16px;
+  margin-top: 20px;
+  
+  :deep(.el-alert) {
+    border-radius: $radius-base;
+  }
 }
 </style>

+ 10 - 0
package.json

@@ -4,6 +4,16 @@
   "description": "多自媒体平台账号管理系统 - 支持视频发布、评论管理、数据分析",
   "private": true,
   "type": "module",
+  "pnpm": {
+    "onlyBuiltDependencies": [
+      "@parcel/watcher",
+      "electron",
+      "esbuild",
+      "sharp",
+      "vue-demi",
+      "vue-echarts"
+    ]
+  },
   "scripts": {
     "dev": "concurrently \"pnpm --filter server dev\" \"pnpm --filter client dev\"",
     "dev:server": "pnpm --filter server dev",

+ 296 - 2
pnpm-lock.yaml

@@ -81,6 +81,9 @@ importers:
       sass:
         specifier: ^1.69.7
         version: 1.97.2
+      sharp:
+        specifier: ^0.34.5
+        version: 0.34.5
       typescript:
         specifier: ^5.3.3
         version: 5.9.3
@@ -298,6 +301,9 @@ packages:
     peerDependencies:
       vue: ^3.2.0
 
+  '@emnapi/runtime@1.8.1':
+    resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
+
   '@esbuild/aix-ppc64@0.21.5':
     resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
     engines: {node: '>=12'}
@@ -632,6 +638,159 @@ packages:
     resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
     deprecated: Use @eslint/object-schema instead
 
+  '@img/colour@1.0.0':
+    resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
+    engines: {node: '>=18'}
+
+  '@img/sharp-darwin-arm64@0.34.5':
+    resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@img/sharp-darwin-x64@0.34.5':
+    resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [darwin]
+
+  '@img/sharp-libvips-darwin-arm64@1.2.4':
+    resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@img/sharp-libvips-darwin-x64@1.2.4':
+    resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@img/sharp-libvips-linux-arm64@1.2.4':
+    resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-libvips-linux-arm@1.2.4':
+    resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-libvips-linux-ppc64@1.2.4':
+    resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-libvips-linux-riscv64@1.2.4':
+    resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-libvips-linux-s390x@1.2.4':
+    resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-libvips-linux-x64@1.2.4':
+    resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+    resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+    resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@img/sharp-linux-arm64@0.34.5':
+    resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-linux-arm@0.34.5':
+    resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-linux-ppc64@0.34.5':
+    resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-linux-riscv64@0.34.5':
+    resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-linux-s390x@0.34.5':
+    resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-linux-x64@0.34.5':
+    resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@img/sharp-linuxmusl-arm64@0.34.5':
+    resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@img/sharp-linuxmusl-x64@0.34.5':
+    resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@img/sharp-wasm32@0.34.5':
+    resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [wasm32]
+
+  '@img/sharp-win32-arm64@0.34.5':
+    resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [win32]
+
+  '@img/sharp-win32-ia32@0.34.5':
+    resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [ia32]
+    os: [win32]
+
+  '@img/sharp-win32-x64@0.34.5':
+    resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [win32]
+
   '@intlify/core-base@9.14.5':
     resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==}
     engines: {node: '>= 16'}
@@ -2972,6 +3131,10 @@ packages:
     engines: {node: '>= 0.10'}
     hasBin: true
 
+  sharp@0.34.5:
+    resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
   shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -3645,6 +3808,11 @@ snapshots:
     dependencies:
       vue: 3.5.26(typescript@5.9.3)
 
+  '@emnapi/runtime@1.8.1':
+    dependencies:
+      tslib: 2.8.1
+    optional: true
+
   '@esbuild/aix-ppc64@0.21.5':
     optional: true
 
@@ -3838,6 +4006,102 @@ snapshots:
 
   '@humanwhocodes/object-schema@2.0.3': {}
 
+  '@img/colour@1.0.0': {}
+
+  '@img/sharp-darwin-arm64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-arm64': 1.2.4
+    optional: true
+
+  '@img/sharp-darwin-x64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-x64': 1.2.4
+    optional: true
+
+  '@img/sharp-libvips-darwin-arm64@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-darwin-x64@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linux-arm64@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linux-arm@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linux-ppc64@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linux-riscv64@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linux-s390x@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linux-x64@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+    optional: true
+
+  '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+    optional: true
+
+  '@img/sharp-linux-arm64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm64': 1.2.4
+    optional: true
+
+  '@img/sharp-linux-arm@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm': 1.2.4
+    optional: true
+
+  '@img/sharp-linux-ppc64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-ppc64': 1.2.4
+    optional: true
+
+  '@img/sharp-linux-riscv64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-riscv64': 1.2.4
+    optional: true
+
+  '@img/sharp-linux-s390x@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-s390x': 1.2.4
+    optional: true
+
+  '@img/sharp-linux-x64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-x64': 1.2.4
+    optional: true
+
+  '@img/sharp-linuxmusl-arm64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+    optional: true
+
+  '@img/sharp-linuxmusl-x64@0.34.5':
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+    optional: true
+
+  '@img/sharp-wasm32@0.34.5':
+    dependencies:
+      '@emnapi/runtime': 1.8.1
+    optional: true
+
+  '@img/sharp-win32-arm64@0.34.5':
+    optional: true
+
+  '@img/sharp-win32-ia32@0.34.5':
+    optional: true
+
+  '@img/sharp-win32-x64@0.34.5':
+    optional: true
+
   '@intlify/core-base@9.14.5':
     dependencies:
       '@intlify/message-compiler': 9.14.5
@@ -4954,8 +5218,7 @@ snapshots:
 
   destroy@1.2.0: {}
 
-  detect-libc@2.1.2:
-    optional: true
+  detect-libc@2.1.2: {}
 
   detect-node@2.1.0:
     optional: true
@@ -6438,6 +6701,37 @@ snapshots:
       safe-buffer: 5.2.1
       to-buffer: 1.2.2
 
+  sharp@0.34.5:
+    dependencies:
+      '@img/colour': 1.0.0
+      detect-libc: 2.1.2
+      semver: 7.7.3
+    optionalDependencies:
+      '@img/sharp-darwin-arm64': 0.34.5
+      '@img/sharp-darwin-x64': 0.34.5
+      '@img/sharp-libvips-darwin-arm64': 1.2.4
+      '@img/sharp-libvips-darwin-x64': 1.2.4
+      '@img/sharp-libvips-linux-arm': 1.2.4
+      '@img/sharp-libvips-linux-arm64': 1.2.4
+      '@img/sharp-libvips-linux-ppc64': 1.2.4
+      '@img/sharp-libvips-linux-riscv64': 1.2.4
+      '@img/sharp-libvips-linux-s390x': 1.2.4
+      '@img/sharp-libvips-linux-x64': 1.2.4
+      '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+      '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+      '@img/sharp-linux-arm': 0.34.5
+      '@img/sharp-linux-arm64': 0.34.5
+      '@img/sharp-linux-ppc64': 0.34.5
+      '@img/sharp-linux-riscv64': 0.34.5
+      '@img/sharp-linux-s390x': 0.34.5
+      '@img/sharp-linux-x64': 0.34.5
+      '@img/sharp-linuxmusl-arm64': 0.34.5
+      '@img/sharp-linuxmusl-x64': 0.34.5
+      '@img/sharp-wasm32': 0.34.5
+      '@img/sharp-win32-arm64': 0.34.5
+      '@img/sharp-win32-ia32': 0.34.5
+      '@img/sharp-win32-x64': 0.34.5
+
   shebang-command@2.0.0:
     dependencies:
       shebang-regex: 3.0.0

Vissa filer visades inte eftersom för många filer har ändrats