|
|
@@ -0,0 +1,769 @@
|
|
|
+<template>
|
|
|
+ <div class="browser-tab-container">
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <div class="browser-toolbar">
|
|
|
+ <div class="toolbar-left">
|
|
|
+ <el-button-group size="small">
|
|
|
+ <el-button :icon="ArrowLeft" :disabled="!canGoBack" @click="goBack" />
|
|
|
+ <el-button :icon="ArrowRight" :disabled="!canGoForward" @click="goForward" />
|
|
|
+ <el-button :icon="Refresh" :loading="isLoading" @click="reload" />
|
|
|
+ </el-button-group>
|
|
|
+ <div class="url-bar">
|
|
|
+ <el-icon v-if="isLoading" class="loading-icon"><Loading /></el-icon>
|
|
|
+ <el-icon v-else class="lock-icon"><Lock /></el-icon>
|
|
|
+ <span class="url-text">{{ currentUrl }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="toolbar-right">
|
|
|
+ <el-tag :type="statusType" size="small">
|
|
|
+ {{ statusText }}
|
|
|
+ </el-tag>
|
|
|
+ <el-button
|
|
|
+ v-if="loginStatus === 'success'"
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ @click="handleSaveAccount"
|
|
|
+ :loading="saving"
|
|
|
+ >
|
|
|
+ 保存账号
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @click="handleCheckLogin"
|
|
|
+ :loading="checking"
|
|
|
+ >
|
|
|
+ 检测登录
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 内嵌浏览器 -->
|
|
|
+ <div class="browser-content">
|
|
|
+ <webview
|
|
|
+ ref="webviewRef"
|
|
|
+ :src="initialUrl"
|
|
|
+ :partition="webviewPartition"
|
|
|
+ class="embedded-browser"
|
|
|
+ allowpopups
|
|
|
+ @did-start-loading="handleStartLoading"
|
|
|
+ @did-stop-loading="handleStopLoading"
|
|
|
+ @did-navigate="handleNavigate"
|
|
|
+ @did-navigate-in-page="handleNavigateInPage"
|
|
|
+ @page-title-updated="handleTitleUpdate"
|
|
|
+ @dom-ready="handleDomReady"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 登录成功遮罩 -->
|
|
|
+ <div v-if="loginStatus === 'success'" class="success-overlay">
|
|
|
+ <div class="success-content">
|
|
|
+ <el-icon class="success-icon"><CircleCheck /></el-icon>
|
|
|
+ <h3>登录成功!</h3>
|
|
|
+ <div v-if="accountInfo" class="account-preview">
|
|
|
+ <el-avatar :size="56" :src="accountInfo.avatarUrl || undefined">
|
|
|
+ {{ accountInfo.accountName?.[0] }}
|
|
|
+ </el-avatar>
|
|
|
+ <div class="account-preview-info">
|
|
|
+ <div class="account-preview-name">{{ accountInfo.accountName }}</div>
|
|
|
+ <div class="account-preview-id">{{ accountInfo.accountId }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <p>点击"保存账号"完成添加</p>
|
|
|
+ <el-button type="primary" @click="handleSaveAccount" :loading="saving">
|
|
|
+ 保存账号
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
|
|
+import { ArrowLeft, ArrowRight, Refresh, Loading, Lock, CircleCheck } from '@element-plus/icons-vue';
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
+import { accountsApi } from '@/api/accounts';
|
|
|
+import { useTabsStore, type Tab } from '@/stores/tabs';
|
|
|
+import { PLATFORMS } from '@media-manager/shared';
|
|
|
+import type { PlatformType } from '@media-manager/shared';
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ tab: Tab;
|
|
|
+}>();
|
|
|
+
|
|
|
+const emit = defineEmits<{
|
|
|
+ (e: 'accountSaved'): void;
|
|
|
+ (e: 'close'): void;
|
|
|
+}>();
|
|
|
+
|
|
|
+const tabsStore = useTabsStore();
|
|
|
+
|
|
|
+// Refs
|
|
|
+const webviewRef = ref<Electron.WebviewTag | null>(null);
|
|
|
+const isLoading = ref(true);
|
|
|
+const currentUrl = ref('');
|
|
|
+const canGoBack = ref(false);
|
|
|
+const canGoForward = ref(false);
|
|
|
+const saving = ref(false);
|
|
|
+const checking = ref(false);
|
|
|
+const loginStatus = ref<'pending' | 'checking' | 'success' | 'failed'>('pending');
|
|
|
+const accountInfo = ref<{
|
|
|
+ accountId: string;
|
|
|
+ accountName: string;
|
|
|
+ avatarUrl: string;
|
|
|
+ fansCount: number;
|
|
|
+ worksCount: number;
|
|
|
+} | null>(null);
|
|
|
+const cookieData = ref('');
|
|
|
+
|
|
|
+// 计时器和标志位
|
|
|
+let checkTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
+let isVerifying = false; // 防止重复验证
|
|
|
+let hasShownSuccessMessage = false; // 防止重复显示成功消息
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const platform = computed(() => props.tab.browserData?.platform as PlatformType);
|
|
|
+
|
|
|
+const initialUrl = computed(() => {
|
|
|
+ const platformInfo = PLATFORMS[platform.value];
|
|
|
+ return platformInfo?.loginUrl || 'about:blank';
|
|
|
+});
|
|
|
+
|
|
|
+// 每个标签页使用独立的 partition 来隔离 cookie
|
|
|
+const webviewPartition = computed(() => `persist:browser-${props.tab.id}`);
|
|
|
+
|
|
|
+const statusType = computed(() => {
|
|
|
+ switch (loginStatus.value) {
|
|
|
+ case 'success': return 'success';
|
|
|
+ case 'failed': return 'danger';
|
|
|
+ case 'checking': return 'warning';
|
|
|
+ default: return 'info';
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const statusText = computed(() => {
|
|
|
+ switch (loginStatus.value) {
|
|
|
+ case 'success': return '已登录';
|
|
|
+ case 'failed': return '未登录';
|
|
|
+ case 'checking': return '检测中...';
|
|
|
+ default: return '等待登录';
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 方法
|
|
|
+function goBack() {
|
|
|
+ webviewRef.value?.goBack();
|
|
|
+}
|
|
|
+
|
|
|
+function goForward() {
|
|
|
+ webviewRef.value?.goForward();
|
|
|
+}
|
|
|
+
|
|
|
+function reload() {
|
|
|
+ webviewRef.value?.reload();
|
|
|
+}
|
|
|
+
|
|
|
+function handleStartLoading() {
|
|
|
+ isLoading.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function handleStopLoading() {
|
|
|
+ isLoading.value = false;
|
|
|
+ updateNavigation();
|
|
|
+}
|
|
|
+
|
|
|
+function handleNavigate(event: Electron.DidNavigateEvent) {
|
|
|
+ currentUrl.value = event.url;
|
|
|
+ updateNavigation();
|
|
|
+
|
|
|
+ // 页面跳转后立即检测一次登录状态(加快响应)
|
|
|
+ if (loginStatus.value !== 'success') {
|
|
|
+ setTimeout(() => checkLoginSilently(), 500);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleNavigateInPage(event: Electron.DidNavigateInPageEvent) {
|
|
|
+ currentUrl.value = event.url;
|
|
|
+ updateNavigation();
|
|
|
+}
|
|
|
+
|
|
|
+function handleTitleUpdate(event: Electron.PageTitleUpdatedEvent) {
|
|
|
+ // 更新标签页标题
|
|
|
+ const platformName = PLATFORMS[platform.value]?.name || platform.value;
|
|
|
+ tabsStore.updateTab(props.tab.id, {
|
|
|
+ title: `${platformName} - ${event.title}`
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function handleDomReady() {
|
|
|
+ updateNavigation();
|
|
|
+ currentUrl.value = webviewRef.value?.getURL() || '';
|
|
|
+
|
|
|
+ // 开始自动检测登录状态
|
|
|
+ startAutoCheck();
|
|
|
+}
|
|
|
+
|
|
|
+function updateNavigation() {
|
|
|
+ if (webviewRef.value) {
|
|
|
+ canGoBack.value = webviewRef.value.canGoBack();
|
|
|
+ canGoForward.value = webviewRef.value.canGoForward();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 开始自动检测登录状态
|
|
|
+function startAutoCheck() {
|
|
|
+ stopAutoCheck();
|
|
|
+
|
|
|
+ // 立即执行一次检测
|
|
|
+ checkLoginSilently();
|
|
|
+
|
|
|
+ // 每 1.5 秒检测一次(加快检测速度)
|
|
|
+ checkTimer = setInterval(() => {
|
|
|
+ if (loginStatus.value !== 'success' && !isVerifying) {
|
|
|
+ checkLoginSilently();
|
|
|
+ } else if (loginStatus.value === 'success') {
|
|
|
+ stopAutoCheck();
|
|
|
+ }
|
|
|
+ }, 1500);
|
|
|
+}
|
|
|
+
|
|
|
+function stopAutoCheck() {
|
|
|
+ if (checkTimer) {
|
|
|
+ clearInterval(checkTimer);
|
|
|
+ checkTimer = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 检查当前 URL 是否表明已登录成功
|
|
|
+function isLoggedInByUrl(): boolean {
|
|
|
+ const url = currentUrl.value;
|
|
|
+
|
|
|
+ // 登录页面特征(这些页面不算已登录)
|
|
|
+ const loginPagePatterns: Record<string, RegExp[]> = {
|
|
|
+ douyin: [
|
|
|
+ /creator\.douyin\.com\/?$/, // 创作者中心首页(登录页)
|
|
|
+ /creator\.douyin\.com\/login/, // 登录页
|
|
|
+ /passport/, // 护照登录
|
|
|
+ /login/i,
|
|
|
+ ],
|
|
|
+ kuaishou: [/passport/, /login/i],
|
|
|
+ xiaohongshu: [/passport/, /login/i],
|
|
|
+ bilibili: [/passport/, /login/i],
|
|
|
+ toutiao: [/passport/, /login/i],
|
|
|
+ baijiahao: [/passport/, /login/i],
|
|
|
+ };
|
|
|
+
|
|
|
+ // 如果在登录页面,返回 false
|
|
|
+ const loginPatterns = loginPagePatterns[platform.value] || [/login/i, /passport/];
|
|
|
+ if (loginPatterns.some(pattern => pattern.test(url))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 各平台登录成功后的页面特征(必须是登录后才能访问的页面)
|
|
|
+ const loggedInPatterns: Record<string, RegExp[]> = {
|
|
|
+ douyin: [
|
|
|
+ /summon\.bytedance\.com\/web/, // 创作服务平台(登录后跳转)
|
|
|
+ /creator\.douyin\.com\/creator-micro/, // 创作者微服务
|
|
|
+ /creator\.douyin\.com\/content/, // 内容管理
|
|
|
+ /creator\.douyin\.com\/data/, // 数据中心
|
|
|
+ /douyin\.com\/user\/self/, // 个人主页
|
|
|
+ ],
|
|
|
+ kuaishou: [
|
|
|
+ /cp\.kuaishou\.com\/article/,
|
|
|
+ /cp\.kuaishou\.com\/profile/,
|
|
|
+ ],
|
|
|
+ xiaohongshu: [
|
|
|
+ /creator\.xiaohongshu\.com\/publish/,
|
|
|
+ /creator\.xiaohongshu\.com\/creator/,
|
|
|
+ ],
|
|
|
+ bilibili: [
|
|
|
+ /member\.bilibili\.com\/platform/,
|
|
|
+ /space\.bilibili\.com\/\d+/,
|
|
|
+ ],
|
|
|
+ toutiao: [
|
|
|
+ /mp\.toutiao\.com\/profile/,
|
|
|
+ ],
|
|
|
+ baijiahao: [
|
|
|
+ /baijiahao\.baidu\.com\/builder\/rc/,
|
|
|
+ ],
|
|
|
+ };
|
|
|
+
|
|
|
+ const patterns = loggedInPatterns[platform.value] || [];
|
|
|
+ return patterns.some(pattern => pattern.test(url));
|
|
|
+}
|
|
|
+
|
|
|
+// 静默检测登录
|
|
|
+async function checkLoginSilently() {
|
|
|
+ // 如果已经登录成功或正在验证中,跳过
|
|
|
+ if (loginStatus.value === 'success' || isVerifying) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const cookies = await getCookies();
|
|
|
+ if (cookies && cookies.length > 0) {
|
|
|
+ // 简单判断是否有登录相关的 cookie
|
|
|
+ const hasLoginCookie = checkHasLoginCookie(cookies);
|
|
|
+
|
|
|
+ if (hasLoginCookie && loginStatus.value !== 'success') {
|
|
|
+ // 检查 URL 是否已经在登录后的页面
|
|
|
+ const urlIndicatesLoggedIn = isLoggedInByUrl();
|
|
|
+
|
|
|
+ if (urlIndicatesLoggedIn) {
|
|
|
+ // URL 表明已登录,快速响应:先显示成功,再异步获取详情
|
|
|
+ await quickLoginSuccess(cookies);
|
|
|
+ } else {
|
|
|
+ // 还在登录页面,通过服务器验证
|
|
|
+ await verifyLoginWithServer(cookies, true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 静默失败,不处理
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 快速登录成功(URL 已表明登录成功时使用)
|
|
|
+async function quickLoginSuccess(cookies: Electron.Cookie[]) {
|
|
|
+ if (loginStatus.value === 'success' || isVerifying) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isVerifying = true;
|
|
|
+ const cookieString = formatCookies(cookies);
|
|
|
+ cookieData.value = cookieString;
|
|
|
+
|
|
|
+ // 先设置为成功状态,提升用户体验
|
|
|
+ loginStatus.value = 'success';
|
|
|
+ stopAutoCheck();
|
|
|
+
|
|
|
+ if (!hasShownSuccessMessage) {
|
|
|
+ hasShownSuccessMessage = true;
|
|
|
+ ElMessage.success('检测到登录成功!');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 异步获取账号详细信息
|
|
|
+ try {
|
|
|
+ const result = await accountsApi.verifyLoginCookie(platform.value, cookieString);
|
|
|
+ if (result.success && result.accountInfo) {
|
|
|
+ accountInfo.value = result.accountInfo;
|
|
|
+ } else {
|
|
|
+ // 即使获取详情失败,也保持登录成功状态
|
|
|
+ accountInfo.value = {
|
|
|
+ accountId: `${platform.value}_${Date.now()}`,
|
|
|
+ accountName: `${PLATFORMS[platform.value]?.name || platform.value}账号`,
|
|
|
+ avatarUrl: '',
|
|
|
+ fansCount: 0,
|
|
|
+ worksCount: 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 获取详情失败,使用默认值
|
|
|
+ accountInfo.value = {
|
|
|
+ accountId: `${platform.value}_${Date.now()}`,
|
|
|
+ accountName: `${PLATFORMS[platform.value]?.name || platform.value}账号`,
|
|
|
+ avatarUrl: '',
|
|
|
+ fansCount: 0,
|
|
|
+ worksCount: 0,
|
|
|
+ };
|
|
|
+ } finally {
|
|
|
+ isVerifying = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 检查是否有登录 cookie
|
|
|
+function checkHasLoginCookie(cookies: Electron.Cookie[]): boolean {
|
|
|
+ // 根据不同平台检查特定的 cookie
|
|
|
+ const platformCookieNames: Record<string, string[]> = {
|
|
|
+ douyin: ['sessionid', 'passport_csrf_token', 'sid_guard'],
|
|
|
+ kuaishou: ['passToken', 'userId'],
|
|
|
+ xiaohongshu: ['web_session', 'xsecappid'],
|
|
|
+ weixin_video: ['wxuin', 'pass_ticket'],
|
|
|
+ bilibili: ['SESSDATA', 'bili_jct'],
|
|
|
+ toutiao: ['sessionid', 'sso_uid'],
|
|
|
+ baijiahao: ['BAIDUID', 'BDUSS'],
|
|
|
+ qie: ['uin', 'skey'],
|
|
|
+ dayuhao: ['cna', 'login_aliyunid'],
|
|
|
+ };
|
|
|
+
|
|
|
+ const targetCookies = platformCookieNames[platform.value] || [];
|
|
|
+ const cookieNames = cookies.map(c => c.name);
|
|
|
+
|
|
|
+ return targetCookies.some(name => cookieNames.includes(name));
|
|
|
+}
|
|
|
+
|
|
|
+// 获取 webview 的 cookies
|
|
|
+async function getCookies(): Promise<Electron.Cookie[]> {
|
|
|
+ if (!window.electronAPI?.getWebviewCookies) {
|
|
|
+ console.warn('electronAPI.getWebviewCookies 不可用');
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ const platformInfo = PLATFORMS[platform.value];
|
|
|
+ if (!platformInfo) return [];
|
|
|
+
|
|
|
+ // 获取登录域名的 cookies
|
|
|
+ const url = platformInfo.loginUrl;
|
|
|
+ return await window.electronAPI.getWebviewCookies(webviewPartition.value, url);
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化 cookies 为字符串
|
|
|
+function formatCookies(cookies: Electron.Cookie[]): string {
|
|
|
+ return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
|
+}
|
|
|
+
|
|
|
+// 手动检测登录
|
|
|
+async function handleCheckLogin() {
|
|
|
+ checking.value = true;
|
|
|
+ loginStatus.value = 'checking';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const cookies = await getCookies();
|
|
|
+ if (cookies && cookies.length > 0) {
|
|
|
+ await verifyLoginWithServer(cookies, false); // 手动检测显示消息
|
|
|
+ } else {
|
|
|
+ loginStatus.value = 'failed';
|
|
|
+ ElMessage.warning('未检测到登录状态,请先完成登录');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ loginStatus.value = 'failed';
|
|
|
+ ElMessage.error('检测登录状态失败');
|
|
|
+ } finally {
|
|
|
+ checking.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 通过服务器验证登录
|
|
|
+async function verifyLoginWithServer(cookies: Electron.Cookie[], silent = false) {
|
|
|
+ // 防止重复验证
|
|
|
+ if (isVerifying || loginStatus.value === 'success') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isVerifying = true;
|
|
|
+ const cookieString = formatCookies(cookies);
|
|
|
+ cookieData.value = cookieString;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 调用服务器 API 验证并获取账号信息
|
|
|
+ const result = await accountsApi.verifyLoginCookie(platform.value, cookieString);
|
|
|
+
|
|
|
+ if (result.success && result.accountInfo) {
|
|
|
+ // 再次检查状态,防止并发问题
|
|
|
+ if (loginStatus.value !== 'success') {
|
|
|
+ loginStatus.value = 'success';
|
|
|
+ accountInfo.value = result.accountInfo;
|
|
|
+ stopAutoCheck();
|
|
|
+
|
|
|
+ // 只显示一次成功消息
|
|
|
+ if (!hasShownSuccessMessage) {
|
|
|
+ hasShownSuccessMessage = true;
|
|
|
+ ElMessage.success('检测到登录成功!');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (!silent && loginStatus.value === 'checking') {
|
|
|
+ loginStatus.value = 'failed';
|
|
|
+ ElMessage.warning(result.message || '登录验证失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (!silent && loginStatus.value === 'checking') {
|
|
|
+ loginStatus.value = 'failed';
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ isVerifying = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 保存账号
|
|
|
+async function handleSaveAccount() {
|
|
|
+ if (!cookieData.value) {
|
|
|
+ ElMessage.warning('请先完成登录');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ saving.value = true;
|
|
|
+ try {
|
|
|
+ // 构建账号信息,包含获取到的用户数据
|
|
|
+ const accountData: {
|
|
|
+ platform: string;
|
|
|
+ cookieData: string;
|
|
|
+ groupId?: number;
|
|
|
+ accountInfo?: {
|
|
|
+ accountId?: string;
|
|
|
+ accountName?: string;
|
|
|
+ avatarUrl?: string;
|
|
|
+ fansCount?: number;
|
|
|
+ worksCount?: number;
|
|
|
+ };
|
|
|
+ } = {
|
|
|
+ platform: platform.value,
|
|
|
+ cookieData: cookieData.value,
|
|
|
+ groupId: props.tab.browserData?.groupId,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 如果有获取到的账号信息,一并传递
|
|
|
+ if (accountInfo.value) {
|
|
|
+ accountData.accountInfo = {
|
|
|
+ accountId: accountInfo.value.accountId,
|
|
|
+ accountName: accountInfo.value.accountName,
|
|
|
+ avatarUrl: accountInfo.value.avatarUrl,
|
|
|
+ fansCount: accountInfo.value.fansCount,
|
|
|
+ worksCount: accountInfo.value.worksCount,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ await accountsApi.addAccount(accountData);
|
|
|
+
|
|
|
+ ElMessage.success('账号添加成功');
|
|
|
+ emit('accountSaved');
|
|
|
+
|
|
|
+ // 清除 webview cookies
|
|
|
+ if (window.electronAPI?.clearWebviewCookies) {
|
|
|
+ await window.electronAPI.clearWebviewCookies(webviewPartition.value);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭标签页
|
|
|
+ tabsStore.closeTab(props.tab.id);
|
|
|
+ } catch (error) {
|
|
|
+ // 错误已处理
|
|
|
+ } finally {
|
|
|
+ saving.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生命周期
|
|
|
+onMounted(() => {
|
|
|
+ currentUrl.value = initialUrl.value;
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ stopAutoCheck();
|
|
|
+
|
|
|
+ // 清理 webview session
|
|
|
+ if (window.electronAPI?.clearWebviewCookies) {
|
|
|
+ window.electronAPI.clearWebviewCookies(webviewPartition.value);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 监听标签页变化
|
|
|
+watch(() => props.tab.id, () => {
|
|
|
+ stopAutoCheck();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+@use '@/styles/variables.scss' as *;
|
|
|
+
|
|
|
+.browser-tab-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ background: #fff;
|
|
|
+ margin: -20px;
|
|
|
+ border-radius: $radius-lg;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.browser-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 10px 16px;
|
|
|
+ background: linear-gradient(180deg, #fafbfc 0%, #f3f4f6 100%);
|
|
|
+ border-bottom: 1px solid $border-light;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ .toolbar-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 14px;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+
|
|
|
+ :deep(.el-button-group) {
|
|
|
+ .el-button {
|
|
|
+ border-radius: $radius-base;
|
|
|
+ background: #fff;
|
|
|
+ border-color: $border-light;
|
|
|
+
|
|
|
+ &:hover:not(:disabled) {
|
|
|
+ background: $primary-color-light;
|
|
|
+ border-color: $primary-color;
|
|
|
+ color: $primary-color;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:first-child {
|
|
|
+ border-top-right-radius: 0;
|
|
|
+ border-bottom-right-radius: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-top-left-radius: 0;
|
|
|
+ border-bottom-left-radius: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:not(:first-child):not(:last-child) {
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ border-radius: $radius-base;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-tag {
|
|
|
+ border-radius: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.url-bar {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 8px 16px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid $border-light;
|
|
|
+ border-radius: 24px;
|
|
|
+ min-width: 0;
|
|
|
+ box-shadow: $shadow-sm;
|
|
|
+ transition: all 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: $primary-color;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading-icon {
|
|
|
+ color: $primary-color;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ font-size: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .lock-icon {
|
|
|
+ color: $success-color;
|
|
|
+ font-size: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .url-text {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-size: 13px;
|
|
|
+ color: $text-regular;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.browser-content {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.embedded-browser {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.success-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(255, 255, 255, 0.98);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 10;
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+}
|
|
|
+
|
|
|
+.success-content {
|
|
|
+ text-align: center;
|
|
|
+ padding: 48px;
|
|
|
+ max-width: 400px;
|
|
|
+
|
|
|
+ .success-icon {
|
|
|
+ font-size: 72px;
|
|
|
+ color: $success-color;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ filter: drop-shadow(0 4px 12px rgba(82, 196, 26, 0.3));
|
|
|
+ }
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ margin: 0 0 24px;
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: $text-primary;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 20px 0;
|
|
|
+ color: $text-secondary;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ min-width: 140px;
|
|
|
+ height: 40px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ border-radius: $radius-base;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.account-preview {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 18px;
|
|
|
+ padding: 20px 28px;
|
|
|
+ background: $primary-color-light;
|
|
|
+ border-radius: $radius-lg;
|
|
|
+ margin: 24px 0;
|
|
|
+ border: 1px solid $border-light;
|
|
|
+
|
|
|
+ :deep(.el-avatar) {
|
|
|
+ border: 3px solid #fff;
|
|
|
+ box-shadow: $shadow-sm;
|
|
|
+ background: linear-gradient(135deg, $primary-color-light, #fff);
|
|
|
+ color: $primary-color;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .account-preview-info {
|
|
|
+ text-align: left;
|
|
|
+
|
|
|
+ .account-preview-name {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 18px;
|
|
|
+ color: $text-primary;
|
|
|
+ }
|
|
|
+
|
|
|
+ .account-preview-id {
|
|
|
+ font-size: 13px;
|
|
|
+ color: $text-secondary;
|
|
|
+ margin-top: 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ from { transform: rotate(0deg); }
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+</style>
|