Переглянути джерело

fix: #6142 修复有头浏览器超时自动关闭,增加默认超时到60秒

ethanfly 4 днів тому
батько
коміт
260b81abed

+ 249 - 50
client/src/views/Login/index.vue

@@ -22,14 +22,14 @@
         </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="login-card">
       <div class="login-header">
         <div class="logo">
@@ -38,26 +38,46 @@
         <h1>智媒通</h1>
         <p>登录您的账号以继续</p>
       </div>
-      
+
+      <!-- 登录方式切换 -->
+      <div class="login-tabs">
+        <button
+          class="tab-btn"
+          :class="{ active: loginType === 'password' }"
+          @click="loginType = 'password'"
+        >
+          密码登录
+        </button>
+        <button
+          class="tab-btn"
+          :class="{ active: loginType === 'sms' }"
+          @click="loginType = 'sms'"
+        >
+          手机验证码登录
+        </button>
+      </div>
+
+      <!-- 密码登录表单 -->
       <el-form
-        ref="formRef"
-        :model="form"
-        :rules="rules"
+        v-if="loginType === 'password'"
+        ref="passwordFormRef"
+        :model="passwordForm"
+        :rules="passwordRules"
         class="login-form"
         @submit.prevent="handleLogin"
       >
         <el-form-item prop="username">
           <el-input
-            v-model="form.username"
+            v-model="passwordForm.username"
             placeholder="用户名或邮箱"
             size="large"
             :prefix-icon="User"
           />
         </el-form-item>
-        
+
         <el-form-item prop="password">
           <el-input
-            v-model="form.password"
+            v-model="passwordForm.password"
             type="password"
             placeholder="密码"
             size="large"
@@ -65,11 +85,14 @@
             show-password
           />
         </el-form-item>
-        
+
         <el-form-item class="remember-row">
-          <el-checkbox v-model="form.rememberMe">记住登录状态</el-checkbox>
+          <el-checkbox v-model="passwordForm.rememberMe">记住登录状态</el-checkbox>
+          <el-button type="primary" link @click="$router.push('/forgot-password')">
+            忘记密码?
+          </el-button>
         </el-form-item>
-        
+
         <el-form-item>
           <el-button
             type="primary"
@@ -82,14 +105,64 @@
           </el-button>
         </el-form-item>
       </el-form>
-      
+
+      <!-- 手机验证码登录表单 -->
+      <el-form
+        v-else
+        ref="smsFormRef"
+        :model="smsForm"
+        :rules="smsRules"
+        class="login-form"
+        @submit.prevent="handleSmsLogin"
+      >
+        <el-form-item prop="phone">
+          <el-input
+            v-model="smsForm.phone"
+            placeholder="手机号"
+            size="large"
+            :prefix-icon="Phone"
+          />
+        </el-form-item>
+
+        <el-form-item prop="code">
+          <div class="sms-code-row">
+            <el-input
+              v-model="smsForm.code"
+              placeholder="验证码"
+              size="large"
+              :prefix-icon="Message"
+            />
+            <el-button
+              size="large"
+              :disabled="smsCooldown > 0"
+              @click="sendSmsCode"
+              class="sms-code-btn"
+            >
+              {{ smsCooldown > 0 ? `${smsCooldown}s 后重发` : '获取验证码' }}
+            </el-button>
+          </div>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button
+            type="primary"
+            size="large"
+            :loading="loading"
+            class="login-btn"
+            native-type="submit"
+          >
+            登录
+          </el-button>
+        </el-form-item>
+      </el-form>
+
       <div class="login-footer">
         <span>还没有账号?</span>
         <el-button type="primary" link @click="$router.push('/register')">
           立即注册
         </el-button>
       </div>
-      
+
       <div class="server-info">
         <el-icon><Link /></el-icon>
         <span>{{ serverStore.currentServer?.name || '未配置服务器' }}</span>
@@ -102,9 +175,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue';
+import { ref, reactive, onMounted, onUnmounted } from 'vue';
 import { useRouter } from 'vue-router';
-import { User, Lock, Link, VideoPlay } from '@element-plus/icons-vue';
+import { User, Lock, Link, VideoPlay, Phone, Message } from '@element-plus/icons-vue';
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
 import { useAuthStore } from '@/stores/auth';
 import { useServerStore } from '@/stores/server';
@@ -113,9 +186,13 @@ const router = useRouter();
 const authStore = useAuthStore();
 const serverStore = useServerStore();
 
-const formRef = ref<FormInstance>();
+const passwordFormRef = ref<FormInstance>();
+const smsFormRef = ref<FormInstance>();
 const loading = ref(false);
 const isMaximized = ref(false);
+const loginType = ref<'password' | 'sms'>('password');
+const smsCooldown = ref(0);
+let cooldownTimer: ReturnType<typeof setInterval> | null = null;
 
 // 窗口控制
 function handleMinimize() {
@@ -140,29 +217,53 @@ onMounted(async () => {
   });
 });
 
-const form = reactive({
+onUnmounted(() => {
+  if (cooldownTimer) {
+    clearInterval(cooldownTimer);
+  }
+});
+
+// 密码登录表单
+const passwordForm = reactive({
   username: '',
   password: '',
   rememberMe: true,
 });
 
-const rules: FormRules = {
+const passwordRules: FormRules = {
   username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
   password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
 };
 
+// 手机验证码登录表单
+const smsForm = reactive({
+  phone: '',
+  code: '',
+});
+
+const smsRules: FormRules = {
+  phone: [
+    { required: true, message: '请输入手机号', trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
+  ],
+  code: [
+    { required: true, message: '请输入验证码', trigger: 'blur' },
+    { len: 6, message: '验证码为6位', trigger: 'blur' },
+  ],
+};
+
 async function handleLogin() {
-  if (!formRef.value) return;
-  
-  const valid = await formRef.value.validate().catch(() => false);
+  if (!passwordFormRef.value) return;
+
+  const valid = await passwordFormRef.value.validate().catch(() => false);
   if (!valid) return;
-  
+
   loading.value = true;
   try {
     await authStore.login({
-      username: form.username,
-      password: form.password,
-      rememberMe: form.rememberMe,
+      username: passwordForm.username,
+      password: passwordForm.password,
+      rememberMe: passwordForm.rememberMe,
     });
     ElMessage.success('登录成功');
     router.push('/');
@@ -172,6 +273,54 @@ async function handleLogin() {
     loading.value = false;
   }
 }
+
+async function handleSmsLogin() {
+  if (!smsFormRef.value) return;
+
+  const valid = await smsFormRef.value.validate().catch(() => false);
+  if (!valid) return;
+
+  loading.value = true;
+  try {
+    await authStore.login({
+      username: smsForm.phone,
+      password: smsForm.code,
+      rememberMe: false,
+    });
+    ElMessage.success('登录成功');
+    router.push('/');
+  } catch (error: any) {
+    // 错误已在拦截器中处理
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function sendSmsCode() {
+  if (!smsForm.phone) {
+    ElMessage.warning('请先输入手机号');
+    return;
+  }
+  if (!/^1[3-9]\d{9}$/.test(smsForm.phone)) {
+    ElMessage.warning('手机号格式不正确');
+    return;
+  }
+
+  try {
+    // TODO: 调用发送验证码 API
+    ElMessage.success('验证码已发送');
+    smsCooldown.value = 60;
+    cooldownTimer = setInterval(() => {
+      smsCooldown.value--;
+      if (smsCooldown.value <= 0 && cooldownTimer) {
+        clearInterval(cooldownTimer);
+        cooldownTimer = null;
+      }
+    }, 1000);
+  } catch {
+    ElMessage.error('发送验证码失败,请稍后重试');
+  }
+}
 </script>
 
 <style lang="scss" scoped>
@@ -205,7 +354,7 @@ async function handleLogin() {
   right: 0;
   display: flex;
   -webkit-app-region: no-drag;
-  
+
   .window-btn {
     width: 46px;
     height: 32px;
@@ -217,16 +366,16 @@ async function handleLogin() {
     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;
@@ -240,13 +389,13 @@ async function handleLogin() {
   inset: 0;
   overflow: hidden;
   pointer-events: none;
-  
+
   .circle {
     position: absolute;
     border-radius: 50%;
     opacity: 0.5;
   }
-  
+
   .circle-1 {
     width: 400px;
     height: 400px;
@@ -254,7 +403,7 @@ async function handleLogin() {
     top: -100px;
     right: -100px;
   }
-  
+
   .circle-2 {
     width: 300px;
     height: 300px;
@@ -262,7 +411,7 @@ async function handleLogin() {
     bottom: -80px;
     left: -80px;
   }
-  
+
   .circle-3 {
     width: 200px;
     height: 200px;
@@ -286,8 +435,8 @@ async function handleLogin() {
 
 .login-header {
   text-align: center;
-  margin-bottom: 36px;
-  
+  margin-bottom: 28px;
+
   .logo {
     width: 64px;
     height: 64px;
@@ -298,20 +447,20 @@ async function handleLogin() {
     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 12px;
     font-size: 24px;
     font-weight: 700;
     color: $text-primary;
   }
-  
+
   p {
     margin: 0;
     color: $text-secondary;
@@ -319,38 +468,72 @@ async function handleLogin() {
   }
 }
 
+// 登录方式切换
+.login-tabs {
+  display: flex;
+  gap: 0;
+  margin-bottom: 24px;
+  border-bottom: 1px solid $border-light;
+
+  .tab-btn {
+    flex: 1;
+    padding: 10px 0;
+    border: none;
+    background: transparent;
+    font-size: 14px;
+    color: $text-secondary;
+    cursor: pointer;
+    transition: all 0.2s;
+    border-bottom: 2px solid transparent;
+    margin-bottom: -1px;
+
+    &:hover {
+      color: $text-primary;
+    }
+
+    &.active {
+      color: $primary-color;
+      border-bottom-color: $primary-color;
+      font-weight: 600;
+    }
+  }
+}
+
 .login-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;
   }
-  
+
   .remember-row {
     margin-bottom: 24px;
-    
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
     :deep(.el-checkbox__label) {
       color: $text-secondary;
     }
   }
-  
+
   .login-btn {
     width: 100%;
     height: 48px;
@@ -361,16 +544,32 @@ async function handleLogin() {
     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);
     }
   }
+
+  .sms-code-row {
+    display: flex;
+    gap: 12px;
+    width: 100%;
+
+    .el-input {
+      flex: 1;
+    }
+
+    .sms-code-btn {
+      width: 120px;
+      flex-shrink: 0;
+      font-size: 13px;
+    }
+  }
 }
 
 .login-footer {
@@ -378,7 +577,7 @@ async function handleLogin() {
   color: $text-secondary;
   font-size: 14px;
   margin-top: 8px;
-  
+
   .el-button {
     font-weight: 500;
   }
@@ -394,7 +593,7 @@ async function handleLogin() {
   gap: 8px;
   color: $text-secondary;
   font-size: 13px;
-  
+
   .el-icon {
     color: $primary-color;
   }

+ 3 - 0
server/python/platforms/base.py

@@ -398,6 +398,9 @@ class BasePublisher(ABC):
 
         self.page = await self.context.new_page()
 
+        # 增加默认超时时间(等同 Selenium implicitlyWait),避免有头浏览器操作时过早超时自动关闭
+        await self.page.set_default_timeout(60000)  # 60 秒
+
         # 设置额外的页面属性
         await self.page.set_extra_http_headers(
             {

+ 12 - 0
server/src/services/CommentService.ts

@@ -223,6 +223,8 @@ export class CommentService {
 
     let totalSynced = 0;
     let syncedAccounts = 0;
+    const MAX_CONSECUTIVE_FAILURES = 3;
+    let consecutiveFailures = 0;
 
     for (const account of accounts) {
       try {
@@ -483,12 +485,22 @@ export class CommentService {
         if (accountSynced > 0) {
           totalSynced += accountSynced;
           syncedAccounts++;
+          consecutiveFailures = 0;
           logger.info(`Synced ${accountSynced} comments for account ${account.id}`);
           // 注意:不在这里发送 COMMENT_SYNCED,而是由 syncCommentsAsync 统一发送
+        } else {
+          consecutiveFailures++;
         }
       } catch (accountError) {
+        consecutiveFailures++;
         logger.error(`Failed to sync comments for account ${account.id}:`, accountError);
       }
+
+      // 连续失败次数达到上限,停止同步避免反复打开浏览器
+      if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
+        logger.warn(`Comment sync stopped: ${MAX_CONSECUTIVE_FAILURES} consecutive failures`);
+        break;
+      }
     }
 
     // 尝试修复没有 workId 的现有评论